Add transport raid functionality to Falukant daemon: Introduced falukant_transport_raid module with SQL queries for managing transport raids, including candidate transports and user reputation updates. Updated documentation to reflect new WebSocket events and integrated the module into the UndergroundWorker for seamless processing of transport-related events.
This commit is contained in:
41
docs/FALUKANT_TRANSPORT_RAID_DAEMON.md
Normal file
41
docs/FALUKANT_TRANSPORT_RAID_DAEMON.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Falukant: Transportüberfälle & Wachen (Daemon)
|
||||
|
||||
## Migration
|
||||
|
||||
`migrations/008_falukant_transport_raid.sql`:
|
||||
|
||||
- `falukant_data.transport.guard_count` (Standard `0`)
|
||||
- Eintrag `falukant_type.underground.tr = 'raid_transport'`
|
||||
|
||||
## Code
|
||||
|
||||
| Komponente | Datei |
|
||||
|------------|--------|
|
||||
| Tick (ca. 60 s) | `src/worker/falukant_transport_raid.rs` → `run_tick` |
|
||||
| Aufruf | `src/worker/underground.rs` (`UndergroundWorker::tick`, vor den 1-Tage-Jobs) |
|
||||
| SQL | `src/worker/sql.rs` (`QUERY_RAID_*`) |
|
||||
|
||||
## Logik (V1)
|
||||
|
||||
1. Offene Untergrundaufträge `raid_transport` mit `result IS NULL` (ohne 24h-Wartezeit).
|
||||
2. Parameter: `regionId`, `bandSize` (JSON, auch `region_id` / `band_size`).
|
||||
3. Region muss `falukant_type.region.id IN (4,5)` und `label_tr <> 'town'` erfüllen.
|
||||
4. Kandidaten-Transporte: noch unterwegs, `size >= 2`, Fracht, Route berührt die Region (`source` oder `target`), **nicht** vom Auftraggeber.
|
||||
5. Begegnung: Zufall mit von Bandengröße und Ladungsgröße abhängiger Wahrscheinlichkeit.
|
||||
6. Kampf: `raidPower` vs. `guardPower` (Wachen aus `transport.guard_count`) → `repelled` | `partial_success` | `major_success`.
|
||||
7. Beute: Anteil der Menge, **nie** die volle Ladung; zusätzlicher Verlust beim Abtransport ins Lager (`lostDueToStorage`).
|
||||
8. Einlagerung: bevorzugt Niederlassung **in derselben Region**, sonst erste Branch-ID des Users.
|
||||
9. Opfer: reduzierte `reputation`, verkleinerte Transportmenge.
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Empfänger |
|
||||
|-------|-----------|
|
||||
| `falukantTransportRaid` | Opfer (`reason`: `transport_raided`) |
|
||||
| `falukantUndergroundUpdate` | Auftraggeber (`raid_*`) |
|
||||
| `falukantUpdateStatus` | beide |
|
||||
| `falukantBranchUpdate` | beide |
|
||||
|
||||
## Backend / UI
|
||||
|
||||
Projektseitig: API für `raid_transport`, `guardCount` am Transport, Formularfilter für Regionen. Dieses Dokument beschreibt nur den **Daemon**.
|
||||
@@ -190,6 +190,18 @@ Konkrete Routen stehen im **YourPart3**-Backend; das Frontend sollte eine zentra
|
||||
|
||||
---
|
||||
|
||||
### Transportüberfälle (`raid_transport`)
|
||||
|
||||
| Event | Payload (Auszug) |
|
||||
|-------|-------------------|
|
||||
| `falukantTransportRaid` | `user_id` (Opfer), `reason`: `transport_raided` |
|
||||
| `falukantUndergroundUpdate` | `user_id` (Auftraggeber), `reason`: `raid_repelled`, `raid_success`, `raid_partial_success`, `raid_loot_stored` |
|
||||
| `falukantUpdateStatus` / `falukantBranchUpdate` | beide Seiten nach Überfall |
|
||||
|
||||
Daemon: `src/worker/falukant_transport_raid.rs`, Doku: `docs/FALUKANT_TRANSPORT_RAID_DAEMON.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Bezug zum Code (YpDaemon)
|
||||
|
||||
- Worker: `src/worker/falukant_family.rs`
|
||||
|
||||
14
migrations/008_falukant_transport_raid.sql
Normal file
14
migrations/008_falukant_transport_raid.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Transportwachen + Untergrundtyp Überfall auf Transporte (Daemon-Auflösung).
|
||||
-- Siehe docs/FALUKANT_TRANSPORT_RAID_DAEMON.md
|
||||
|
||||
ALTER TABLE falukant_data.transport
|
||||
ADD COLUMN IF NOT EXISTS guard_count INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN falukant_data.transport.guard_count IS 'Mitgeschickte Wachen (reduzieren Überfallchance/Beute im Daemon)';
|
||||
|
||||
-- Untergrundtyp (falukant_type.underground: Spalte tr = Schlüssel in falukant_data.underground.underground_type_id)
|
||||
INSERT INTO falukant_type.underground (tr, name)
|
||||
SELECT 'raid_transport', 'Überfälle auf Transporte'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM falukant_type.underground WHERE tr = 'raid_transport'
|
||||
);
|
||||
505
src/worker/falukant_transport_raid.rs
Normal file
505
src/worker/falukant_transport_raid.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
//! Transportüberfälle (`raid_transport`) und Wachen (`transport.guard_count`).
|
||||
//! Siehe `docs/FALUKANT_TRANSPORT_RAID_DAEMON.md`.
|
||||
//!
|
||||
//! Läuft im Underground-Tick (ca. 60 s). Kein 1-Tage-Wartezeit wie andere Untergrundjobs.
|
||||
|
||||
use crate::db::{ConnectionPool, DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use rand::Rng;
|
||||
use serde_json::{json, Value as Json};
|
||||
|
||||
use crate::worker::sql::{
|
||||
QUERY_GET_AVAILABLE_STOCKS, QUERY_INSERT_INVENTORY, QUERY_RAID_ACTIVE_UNDERGROUND,
|
||||
QUERY_RAID_APP_USER_FOR_FALUKANT, QUERY_RAID_CANDIDATE_TRANSPORTS,
|
||||
QUERY_RAID_FALUKANT_USER_FOR_CHARACTER, QUERY_RAID_NEAREST_BRANCH_FOR_USER,
|
||||
QUERY_RAID_REGION_ALLOWED, QUERY_RAID_SCHEMA_READY, QUERY_RAID_SUBTRACT_REP_BY_USER,
|
||||
QUERY_RAID_UPDATE_UNDERGROUND_RESULT, QUERY_UPDATE_TRANSPORT_SIZE,
|
||||
};
|
||||
|
||||
/// Wird aus `UndergroundWorker::tick` aufgerufen.
|
||||
pub fn run_tick(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
if !schema_ready(pool) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("raid_active", QUERY_RAID_ACTIVE_UNDERGROUND)
|
||||
.map_err(|e| DbError::new(format!("prepare raid_active: {e}")))?;
|
||||
|
||||
let rows = conn
|
||||
.execute("raid_active", &[])
|
||||
.map_err(|e| DbError::new(format!("exec raid_active: {e}")))?;
|
||||
|
||||
drop(conn);
|
||||
|
||||
for row in rows {
|
||||
let ug_id = parse_i32(&row, "id", -1);
|
||||
let performer_cid = parse_i32(&row, "performer_id", -1);
|
||||
let params_str = row.get("parameters").cloned().unwrap_or_else(|| "{}".into());
|
||||
if ug_id < 0 || performer_cid < 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = try_resolve_one_raid(
|
||||
pool,
|
||||
broker,
|
||||
ug_id,
|
||||
performer_cid,
|
||||
¶ms_str,
|
||||
) {
|
||||
eprintln!("[falukant_transport_raid] underground_id={ug_id}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schema_ready(pool: &ConnectionPool) -> bool {
|
||||
let Ok(mut conn) = pool.get() else {
|
||||
return false;
|
||||
};
|
||||
if conn.prepare("rs", QUERY_RAID_SCHEMA_READY).is_err() {
|
||||
return false;
|
||||
}
|
||||
let Ok(rows) = conn.execute("rs", &[]) else {
|
||||
return false;
|
||||
};
|
||||
rows.first()
|
||||
.and_then(|r| r.get("ready"))
|
||||
.map(|v| v == "t" || v == "true")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn try_resolve_one_raid(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
underground_id: i32,
|
||||
performer_character_id: i32,
|
||||
params_json: &str,
|
||||
) -> Result<(), DbError> {
|
||||
let p: Json = serde_json::from_str(params_json).unwrap_or_else(|_| json!({}));
|
||||
let region_id = p
|
||||
.get("regionId")
|
||||
.or_else(|| p.get("region_id"))
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|x| x as i32)
|
||||
.unwrap_or(-1);
|
||||
let band_size = p
|
||||
.get("bandSize")
|
||||
.or_else(|| p.get("band_size"))
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|x| x as i32)
|
||||
.unwrap_or(3)
|
||||
.clamp(1, 50);
|
||||
|
||||
if region_id < 0 {
|
||||
return finish_invalid(
|
||||
pool,
|
||||
broker,
|
||||
underground_id,
|
||||
performer_character_id,
|
||||
"invalid_parameters",
|
||||
json!({"message": "regionId required"}),
|
||||
);
|
||||
}
|
||||
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("raid_reg", QUERY_RAID_REGION_ALLOWED)
|
||||
.map_err(|e| DbError::new(format!("prepare raid_reg: {e}")))?;
|
||||
let ok = conn
|
||||
.execute("raid_reg", &[®ion_id])
|
||||
.map_err(|e| DbError::new(format!("exec raid_reg: {e}")))?;
|
||||
if ok.is_empty() {
|
||||
return finish_invalid(
|
||||
pool,
|
||||
broker,
|
||||
underground_id,
|
||||
performer_character_id,
|
||||
"invalid_region",
|
||||
json!({"message": "region not allowed for raid_transport"}),
|
||||
);
|
||||
}
|
||||
|
||||
conn.prepare("raid_fu", QUERY_RAID_FALUKANT_USER_FOR_CHARACTER)
|
||||
.map_err(|e| DbError::new(format!("prepare raid_fu: {e}")))?;
|
||||
let fu_rows = conn
|
||||
.execute("raid_fu", &[&performer_character_id])
|
||||
.map_err(|e| DbError::new(format!("exec raid_fu: {e}")))?;
|
||||
let (performer_falukant_id, performer_app_uid) = fu_rows
|
||||
.first()
|
||||
.map(|r| {
|
||||
(
|
||||
parse_i32(r, "falukant_user_id", -1),
|
||||
parse_i32(r, "app_user_id", -1),
|
||||
)
|
||||
})
|
||||
.unwrap_or((-1, -1));
|
||||
if performer_falukant_id < 0 {
|
||||
return finish_invalid(
|
||||
pool,
|
||||
broker,
|
||||
underground_id,
|
||||
performer_character_id,
|
||||
"performer_not_found",
|
||||
json!({}),
|
||||
);
|
||||
}
|
||||
|
||||
drop(conn);
|
||||
|
||||
conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("raid_cand", QUERY_RAID_CANDIDATE_TRANSPORTS)
|
||||
.map_err(|e| DbError::new(format!("prepare raid_cand: {e}")))?;
|
||||
let candidates = conn
|
||||
.execute("raid_cand", &[®ion_id, &performer_falukant_id])
|
||||
.map_err(|e| DbError::new(format!("exec raid_cand: {e}")))?;
|
||||
|
||||
if candidates.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let cand = &candidates[0];
|
||||
let transport_id = parse_i32(cand, "transport_id", -1);
|
||||
let transport_size = parse_i32(cand, "transport_size", 0);
|
||||
let product_id = parse_i32(cand, "product_id", -1);
|
||||
let guard_count = parse_i32(cand, "guard_count", 0).max(0);
|
||||
let victim_fu = parse_i32(cand, "victim_falukant_user_id", -1);
|
||||
let vehicle_cap = parse_i32(cand, "vehicle_capacity", 1).max(1);
|
||||
|
||||
if transport_id < 0 || product_id < 0 || victim_fu < 0 || transport_size < 2 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// Begegnung (Bande + Sichtbarkeit großer Ladung)
|
||||
let visibility_penalty = ((transport_size as f64 / vehicle_cap as f64).min(2.0) - 1.0) * 0.04;
|
||||
let encounter_p = (0.08_f64 + band_size as f64 * 0.012 + visibility_penalty).clamp(0.05, 0.45);
|
||||
if !rng.gen_bool(encounter_p) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Kampf
|
||||
let raid_power = band_size + rng.gen_range(0..=band_size);
|
||||
let gmax = guard_count.max(1);
|
||||
let guard_power = guard_count + rng.gen_range(0..=gmax);
|
||||
|
||||
let outcome = if raid_power > (guard_power as f64 * 1.25) as i32 {
|
||||
"major_success"
|
||||
} else if raid_power > guard_power {
|
||||
"partial_success"
|
||||
} else {
|
||||
"repelled"
|
||||
};
|
||||
|
||||
if outcome == "repelled" {
|
||||
let result = json!({
|
||||
"status": "completed",
|
||||
"bandSize": band_size,
|
||||
"regionId": region_id,
|
||||
"attempts": 1,
|
||||
"successes": 0,
|
||||
"lastTargetTransportId": transport_id,
|
||||
"lastLoot": Json::Null,
|
||||
"lastOutcome": "repelled"
|
||||
});
|
||||
save_result(pool, underground_id, &result)?;
|
||||
publish_underground(broker, performer_app_uid, "raid_repelled");
|
||||
publish_status(broker, performer_app_uid);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Beuteanteil (nie Totalverlust)
|
||||
let base_loot = match outcome {
|
||||
"major_success" => rng.gen_range(0.35..0.60),
|
||||
_ => rng.gen_range(0.15..0.45),
|
||||
};
|
||||
let guard_mul = (1.0_f64 - 0.05_f64 * (guard_count.min(12) as f64)).max(0.35);
|
||||
let loot_share = (base_loot * guard_mul).clamp(0.05, 0.55);
|
||||
let mut stolen = ((transport_size as f64) * loot_share).floor() as i32;
|
||||
stolen = stolen.clamp(1, transport_size - 1);
|
||||
|
||||
let carry_loss = rng.gen_range(0.65..0.90);
|
||||
let mut to_store = (stolen as f64 * carry_loss).floor() as i32;
|
||||
to_store = to_store.max(0).min(stolen);
|
||||
|
||||
// Nächstgelegenes Lager
|
||||
let (branch_id, _) = nearest_branch(pool, region_id, performer_falukant_id)?;
|
||||
let (stored, lost_storage) = if branch_id > 0 && to_store > 0 {
|
||||
let remaining = add_to_branch_inventory(pool, branch_id, product_id, to_store)?;
|
||||
(to_store - remaining, remaining)
|
||||
} else {
|
||||
(0, to_store)
|
||||
};
|
||||
|
||||
let new_size = transport_size - stolen;
|
||||
if new_size < 1 {
|
||||
return Err(DbError::new("raid: invalid new transport size"));
|
||||
}
|
||||
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("upd_sz", QUERY_UPDATE_TRANSPORT_SIZE)
|
||||
.map_err(|e| DbError::new(format!("prepare upd_sz: {e}")))?;
|
||||
conn.execute("upd_sz", &[&transport_id, &new_size])
|
||||
.map_err(|e| DbError::new(format!("exec upd_sz: {e}")))?;
|
||||
|
||||
// Opfer: kleiner Rufabzug
|
||||
conn.prepare("raid_rep", QUERY_RAID_SUBTRACT_REP_BY_USER)
|
||||
.map_err(|e| DbError::new(format!("prepare raid_rep: {e}")))?;
|
||||
let _ = conn.execute("raid_rep", &[&victim_fu, &2_i32]);
|
||||
|
||||
drop(conn);
|
||||
|
||||
let victim_app_uid = app_user_for_falukant(pool, victim_fu)?;
|
||||
|
||||
let result = json!({
|
||||
"status": "completed",
|
||||
"bandSize": band_size,
|
||||
"regionId": region_id,
|
||||
"attempts": 1,
|
||||
"successes": 1,
|
||||
"lastTargetTransportId": transport_id,
|
||||
"lastLoot": {
|
||||
"productId": product_id,
|
||||
"stolenFromTransport": stolen,
|
||||
"stored": stored,
|
||||
"lostDueToStorage": lost_storage,
|
||||
"branchId": branch_id
|
||||
},
|
||||
"lastOutcome": outcome
|
||||
});
|
||||
|
||||
save_result(pool, underground_id, &result)?;
|
||||
|
||||
if victim_app_uid > 0 {
|
||||
publish_transport_raid(broker, victim_app_uid, "transport_raided");
|
||||
publish_status(broker, victim_app_uid);
|
||||
publish_branch(broker, victim_app_uid);
|
||||
}
|
||||
|
||||
let ug_reason = match outcome {
|
||||
"major_success" => "raid_success",
|
||||
_ => "raid_partial_success",
|
||||
};
|
||||
publish_underground(broker, performer_app_uid, ug_reason);
|
||||
if stored > 0 {
|
||||
publish_underground(broker, performer_app_uid, "raid_loot_stored");
|
||||
}
|
||||
publish_status(broker, performer_app_uid);
|
||||
publish_branch(broker, performer_app_uid);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_invalid(
|
||||
pool: &ConnectionPool,
|
||||
broker: &MessageBroker,
|
||||
underground_id: i32,
|
||||
performer_character_id: i32,
|
||||
code: &str,
|
||||
detail: Json,
|
||||
) -> Result<(), DbError> {
|
||||
let performer_app_uid = falukant_user_row(pool, performer_character_id)
|
||||
.map(|(_, app)| app)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let result = json!({
|
||||
"status": "error",
|
||||
"code": code,
|
||||
"detail": detail
|
||||
});
|
||||
save_result(pool, underground_id, &result)?;
|
||||
if performer_app_uid > 0 {
|
||||
publish_underground(broker, performer_app_uid, "raid_repelled");
|
||||
publish_status(broker, performer_app_uid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn falukant_user_row(pool: &ConnectionPool, character_id: i32) -> Result<(i32, i32), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("fu", QUERY_RAID_FALUKANT_USER_FOR_CHARACTER)
|
||||
.map_err(|e| DbError::new(format!("prepare fu: {e}")))?;
|
||||
let rows = conn
|
||||
.execute("fu", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec fu: {e}")))?;
|
||||
rows.first()
|
||||
.map(|r| {
|
||||
(
|
||||
parse_i32(r, "falukant_user_id", -1),
|
||||
parse_i32(r, "app_user_id", -1),
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| DbError::new("character not found"))
|
||||
}
|
||||
|
||||
fn app_user_for_falukant(pool: &ConnectionPool, falukant_user_id: i32) -> Result<i32, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("au", QUERY_RAID_APP_USER_FOR_FALUKANT)
|
||||
.map_err(|e| DbError::new(format!("prepare au: {e}")))?;
|
||||
let rows = conn
|
||||
.execute("au", &[&falukant_user_id])
|
||||
.map_err(|e| DbError::new(format!("exec au: {e}")))?;
|
||||
Ok(rows
|
||||
.first()
|
||||
.map(|r| parse_i32(r, "user_id", -1))
|
||||
.unwrap_or(-1))
|
||||
}
|
||||
|
||||
fn nearest_branch(
|
||||
pool: &ConnectionPool,
|
||||
raid_region_id: i32,
|
||||
performer_falukant_id: i32,
|
||||
) -> Result<(i32, i32), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("nb", QUERY_RAID_NEAREST_BRANCH_FOR_USER)
|
||||
.map_err(|e| DbError::new(format!("prepare nb: {e}")))?;
|
||||
let rows = conn
|
||||
.execute("nb", &[&raid_region_id, &performer_falukant_id])
|
||||
.map_err(|e| DbError::new(format!("exec nb: {e}")))?;
|
||||
let branch_id = rows
|
||||
.first()
|
||||
.map(|r| parse_i32(r, "branch_id", -1))
|
||||
.unwrap_or(-1);
|
||||
let reg = rows
|
||||
.first()
|
||||
.map(|r| parse_i32(r, "region_id", -1))
|
||||
.unwrap_or(-1);
|
||||
Ok((branch_id, reg))
|
||||
}
|
||||
|
||||
fn add_to_branch_inventory(
|
||||
pool: &ConnectionPool,
|
||||
branch_id: i32,
|
||||
product_id: i32,
|
||||
quantity: i32,
|
||||
) -> Result<i32, DbError> {
|
||||
use std::cmp::min;
|
||||
|
||||
let stocks = load_stocks(pool, branch_id)?;
|
||||
let mut remaining = quantity;
|
||||
for s in stocks {
|
||||
if remaining <= 0 {
|
||||
break;
|
||||
}
|
||||
let free = s.total_capacity - s.filled;
|
||||
if free <= 0 {
|
||||
continue;
|
||||
}
|
||||
let to_store = min(remaining, free);
|
||||
store_in_stock(pool, s.stock_id, product_id, to_store)?;
|
||||
remaining -= to_store;
|
||||
}
|
||||
Ok(remaining)
|
||||
}
|
||||
|
||||
struct StockInfo {
|
||||
stock_id: i32,
|
||||
total_capacity: i32,
|
||||
filled: i32,
|
||||
}
|
||||
|
||||
fn load_stocks(pool: &ConnectionPool, branch_id: i32) -> Result<Vec<StockInfo>, DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("gs", QUERY_GET_AVAILABLE_STOCKS)
|
||||
.map_err(|e| DbError::new(format!("prepare gs: {e}")))?;
|
||||
let rows = conn
|
||||
.execute("gs", &[&branch_id])
|
||||
.map_err(|e| DbError::new(format!("exec gs: {e}")))?;
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
let stock_id = parse_i32(&row, "id", -1);
|
||||
let total_capacity = parse_i32(&row, "total_capacity", 0);
|
||||
let filled = parse_i32(&row, "filled", 0);
|
||||
if stock_id >= 0 {
|
||||
out.push(StockInfo {
|
||||
stock_id,
|
||||
total_capacity,
|
||||
filled,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn store_in_stock(pool: &ConnectionPool, stock_id: i32, product_id: i32, quantity: i32) -> Result<(), DbError> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ins", QUERY_INSERT_INVENTORY)
|
||||
.map_err(|e| DbError::new(format!("prepare ins: {e}")))?;
|
||||
let quality: i32 = 100;
|
||||
conn.execute("ins", &[&stock_id, &product_id, &quantity, &quality])
|
||||
.map_err(|e| DbError::new(format!("exec ins: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_result(pool: &ConnectionPool, underground_id: i32, result: &Json) -> Result<(), DbError> {
|
||||
let s = result.to_string();
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("ur", QUERY_RAID_UPDATE_UNDERGROUND_RESULT)
|
||||
.map_err(|e| DbError::new(format!("prepare ur: {e}")))?;
|
||||
conn.execute("ur", &[&underground_id, &s])
|
||||
.map_err(|e| DbError::new(format!("exec ur: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn publish_transport_raid(broker: &MessageBroker, user_id: i32, reason: &str) {
|
||||
let m = format!(
|
||||
r#"{{"event":"falukantTransportRaid","user_id":{},"reason":"{}"}}"#,
|
||||
user_id, reason
|
||||
);
|
||||
broker.publish(m);
|
||||
}
|
||||
|
||||
fn publish_underground(broker: &MessageBroker, user_id: i32, reason: &str) {
|
||||
if user_id <= 0 {
|
||||
return;
|
||||
}
|
||||
let m = format!(
|
||||
r#"{{"event":"falukantUndergroundUpdate","user_id":{},"reason":"{}"}}"#,
|
||||
user_id, reason
|
||||
);
|
||||
broker.publish(m);
|
||||
}
|
||||
|
||||
fn publish_status(broker: &MessageBroker, user_id: i32) {
|
||||
if user_id <= 0 {
|
||||
return;
|
||||
}
|
||||
let m = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(m);
|
||||
}
|
||||
|
||||
fn publish_branch(broker: &MessageBroker, user_id: i32) {
|
||||
if user_id <= 0 {
|
||||
return;
|
||||
}
|
||||
let m = format!(r#"{{"event":"falukantBranchUpdate","user_id":{}}}"#, user_id);
|
||||
broker.publish(m);
|
||||
}
|
||||
|
||||
fn parse_i32(row: &Row, key: &str, default: i32) -> i32 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ mod falukant_family;
|
||||
mod falukant_certificate;
|
||||
mod falukant_servants;
|
||||
mod falukant_debtors;
|
||||
mod falukant_transport_raid;
|
||||
mod sql;
|
||||
|
||||
pub use base::Worker;
|
||||
|
||||
@@ -276,6 +276,106 @@ UPDATE falukant_data.transport
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
// --- Falukant: Transportüberfälle (docs/FALUKANT_TRANSPORT_RAID_DAEMON.md) ---
|
||||
pub const QUERY_RAID_SCHEMA_READY: &str = r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'transport'
|
||||
AND column_name = 'guard_count'
|
||||
) AS ready;
|
||||
"#;
|
||||
|
||||
pub const QUERY_RAID_ACTIVE_UNDERGROUND: &str = r#"
|
||||
SELECT u.id,
|
||||
u.performer_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 t.tr = 'raid_transport'
|
||||
ORDER BY u.created_at ASC
|
||||
LIMIT 100;
|
||||
"#;
|
||||
|
||||
pub const QUERY_RAID_REGION_ALLOWED: &str = r#"
|
||||
SELECT r.id
|
||||
FROM falukant_data.region r
|
||||
JOIN falukant_type.region rt ON rt.id = r.region_type_id
|
||||
WHERE r.id = $1::int
|
||||
AND rt.id IN (4, 5)
|
||||
AND COALESCE(rt.label_tr, '') <> 'town';
|
||||
"#;
|
||||
|
||||
pub const QUERY_RAID_FALUKANT_USER_FOR_CHARACTER: &str = r#"
|
||||
SELECT fu.id AS falukant_user_id, fu.user_id AS app_user_id
|
||||
FROM falukant_data.character c
|
||||
JOIN falukant_data.falukant_user fu ON fu.user_id = c.user_id
|
||||
WHERE c.id = $1::int
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
/// Aktive (noch unterwegs) Transporte mit Fracht, Route berührt Region, nicht vom Auftraggeber.
|
||||
pub const QUERY_RAID_CANDIDATE_TRANSPORTS: &str = r#"
|
||||
SELECT
|
||||
t.id AS transport_id,
|
||||
t.size AS transport_size,
|
||||
t.product_id,
|
||||
COALESCE(t.guard_count, 0)::int AS guard_count,
|
||||
v.falukant_user_id AS victim_falukant_user_id,
|
||||
vt.capacity AS vehicle_capacity
|
||||
FROM falukant_data.transport t
|
||||
JOIN falukant_data.vehicle v ON v.id = t.vehicle_id
|
||||
JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id
|
||||
JOIN falukant_data.region_distance rd
|
||||
ON (
|
||||
(rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id)
|
||||
OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id)
|
||||
)
|
||||
AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL)
|
||||
WHERE t.product_id IS NOT NULL
|
||||
AND t.size > 0
|
||||
AND vt.speed > 0
|
||||
AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' > NOW()
|
||||
AND (t.source_region_id = $1::int OR t.target_region_id = $1::int)
|
||||
AND v.falukant_user_id <> $2::int
|
||||
ORDER BY RANDOM()
|
||||
LIMIT 20;
|
||||
"#;
|
||||
|
||||
/// Bevorzugt eine Niederlassung in der Überfallregion, sonst kleinste branch_id.
|
||||
pub const QUERY_RAID_NEAREST_BRANCH_FOR_USER: &str = r#"
|
||||
SELECT b.id AS branch_id, b.region_id
|
||||
FROM falukant_data.branch b
|
||||
WHERE b.falukant_user_id = $2::int
|
||||
ORDER BY CASE WHEN b.region_id = $1::int THEN 0 ELSE 1 END, b.id ASC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
pub const QUERY_RAID_APP_USER_FOR_FALUKANT: &str = r#"
|
||||
SELECT fu.user_id
|
||||
FROM falukant_data.falukant_user fu
|
||||
WHERE fu.id = $1::int
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
pub const QUERY_RAID_UPDATE_UNDERGROUND_RESULT: &str = r#"
|
||||
UPDATE falukant_data.underground
|
||||
SET result = $2::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1::int;
|
||||
"#;
|
||||
|
||||
pub const QUERY_RAID_SUBTRACT_REP_BY_USER: &str = r#"
|
||||
UPDATE falukant_data.character c
|
||||
SET reputation = GREATEST(0::numeric, COALESCE(c.reputation, 50::numeric) - $2::numeric),
|
||||
updated_at = NOW()
|
||||
FROM falukant_data.falukant_user fu
|
||||
WHERE fu.id = $1::int
|
||||
AND fu.user_id = c.user_id
|
||||
AND c.health > 0;
|
||||
"#;
|
||||
|
||||
pub const QUERY_GET_REGION_WORTH_FOR_PRODUCT: &str = r#"
|
||||
SELECT tpw.region_id, tpw.product_id, tpw.worth_percent FROM falukant_data.town_product_worth tpw JOIN falukant_data.branch b ON b.region_id = tpw.region_id WHERE b.falukant_user_id = $1 AND tpw.product_id = $2;
|
||||
"#;
|
||||
|
||||
@@ -231,6 +231,10 @@ impl UndergroundWorker {
|
||||
}
|
||||
|
||||
fn tick(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
if let Err(err) = super::falukant_transport_raid::run_tick(pool, broker) {
|
||||
eprintln!("[UndergroundWorker] falukant_transport_raid::run_tick: {err}");
|
||||
}
|
||||
|
||||
let rows = Self::fetch_pending(pool)?;
|
||||
|
||||
for row in rows {
|
||||
|
||||
Reference in New Issue
Block a user