commit d0ec363f093444f42bbd7d8c713dbddcd576421b Author: Torsten Schulz (local) Date: Fri Nov 21 23:05:34 2025 +0100 Initial commit: Rust YpDaemon diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4073758 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1364 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "YpDaemon" +version = "0.1.0" +dependencies = [ + "ctrlc", + "futures-util", + "libsystemd", + "postgres", + "rand 0.8.5", + "rustls-pemfile", + "serde", + "serde_json", + "tokio", + "tokio-rustls", + "tokio-tungstenite", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctrlc" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +dependencies = [ + "dispatch2", + "nix 0.30.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsystemd" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c97a761fc86953c5b885422b22c891dbf5bcb9dcc99d0110d6ce4c052759f0" +dependencies = [ + "hmac", + "libc", + "log", + "nix 0.29.0", + "nom", + "once_cell", + "serde", + "sha2", + "thiserror 2.0.17", + "uuid", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "postgres" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c48ece1c6cda0db61b058c1721378da76855140e9214339fa1317decacb176" +dependencies = [ + "bytes", + "fallible-iterator", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.9.2", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4605b7c057056dd35baeb6ac0c0338e4975b1f2bef0f65da953285eb007095" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b40d66d9b2cfe04b628173409368e58247e8eddbbd3b0e6c6ba1d09f20f6c9e" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.2", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7e990a9 --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..3015ae8 --- /dev/null +++ b/src/config.rs @@ -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, +} + +impl Config { + pub fn from_file>(path: P) -> Result> { + 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 { + self.values + .get(key) + .cloned() + .ok_or_else(|| format!("Konfigurationsschlüssel nicht gefunden: {key}")) + } +} + + diff --git a/src/db/connection.rs b/src/db/connection.rs new file mode 100644 index 0000000..0ce5c62 --- /dev/null +++ b/src/db/connection.rs @@ -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; +pub type Rows = Vec; + +#[derive(Debug)] +pub struct DbError { + message: String, +} + +impl DbError { + pub fn new(message: impl Into) -> 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 for DbError { + fn from(err: PgError) -> Self { + DbError::new(format!("Postgres-Fehler: {err}")) + } +} + +struct Database { + client: Client, + // Name -> SQL + prepared: HashMap, +} + +impl Database { + fn connect(conn_str: &str) -> Result { + 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 { + 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 { + 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 = row.get(idx); + map.insert(name, value.unwrap_or_default()); + } + map + } +} + +struct InnerPool { + connections: Mutex>, + available: Condvar, + conn_str: String, +} + +impl InnerPool { + fn new(conn_str: String, size: usize) -> Result { + 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 { + 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, +} + +impl ConnectionPool { + pub fn new(conn_str: String, size: usize) -> Result { + let inner = InnerPool::new(conn_str, size)?; + Ok(Self { + inner: Arc::new(inner), + }) + } + + pub fn get(&self) -> Result { + let db = self.inner.get()?; + Ok(DbConnection { + inner: self.inner.clone(), + db: Some(db), + }) + } +} + +pub struct DbConnection { + inner: Arc, + db: Option, +} + +impl DbConnection { + #[allow(dead_code)] + pub fn query(&mut self, sql: &str) -> Result { + 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 { + 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); + } + } +} + + diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..cda5e5e --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,5 @@ +mod connection; + +pub use connection::{ConnectionPool, DbConnection, DbError, Row, Rows}; + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1e11f8c --- /dev/null +++ b/src/main.rs @@ -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> { + 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> { + // 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, + key_path: Option, +} + +fn load_config() -> Result> { + // Pfad später ggf. konfigurierbar machen + let config = Config::from_file("/etc/yourpart/daemon.conf")?; + Ok(config) +} + +fn create_connection_pool(config: &Config) -> Result> { + 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> { + 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> { + 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], + 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], + 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)); + } +} + diff --git a/src/message_broker.rs b/src/message_broker.rs new file mode 100644 index 0000000..69ef152 --- /dev/null +++ b/src/message_broker.rs @@ -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; + +#[derive(Clone)] +pub struct MessageBroker { + inner: Arc, +} + +struct Inner { + subscribers: Mutex>, + sender: mpsc::Sender, + receiver: Mutex>>, + running: AtomicBool, + started: AtomicBool, +} + +impl MessageBroker { + pub fn new() -> Self { + let (tx, rx) = mpsc::channel::(); + 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(&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()); + } +} + + diff --git a/src/websocket_server.rs b/src/websocket_server.rs new file mode 100644 index 0000000..8988e8e --- /dev/null +++ b/src/websocket_server.rs @@ -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, + key_path: Option, + workers: Vec<*const dyn Worker>, + running: Arc, + runtime: Option, +} + +/// Einfache Registry, um Verbindungsstatistiken für `getConnections` zu liefern. +#[derive(Default)] +struct ConnectionRegistry { + total: usize, + unauthenticated: usize, + by_user: HashMap, +} + +fn create_tls_acceptor( + cert_path: Option<&str>, + key_path: Option<&str>, +) -> Result> { + 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> = 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> = pkcs8_private_keys(&mut key_reader) + .map(|res: Result, _>| res.map(PrivateKeyDer::Pkcs8)) + .collect::>()?; + + 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, _>| res.map(PrivateKeyDer::Pkcs1)) + .collect::>()?; + } + + 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, + key_path: Option, + ) -> 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]) { + 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::(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, + tx: broadcast::Sender, + _pool: ConnectionPool, + registry: Arc>, + tls_acceptor: Option, +) { + 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( + stream: S, + peer_addr: SocketAddr, + mut broker_rx: broadcast::Receiver, + registry: Arc>, +) 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::(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::::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::(&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::(&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); +} + diff --git a/src/worker/base.rs b/src/worker/base.rs new file mode 100644 index 0000000..ec46bf1 --- /dev/null +++ b/src/worker/base.rs @@ -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, +} + +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, + worker_thread: Option>, + watchdog_thread: Option>, +} + +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>(&self, step: S) { + if let Ok(mut guard) = self.state.current_step.lock() { + *guard = step.into(); + } + } + + pub(crate) fn start_worker_with_loop(&mut self, loop_fn: F) + where + F: Fn(Arc) + 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(()) + } +} + + diff --git a/src/worker/character_creation.rs b/src/worker/character_creation.rs new file mode 100644 index 0000000..5f42a78 --- /dev/null +++ b/src/worker/character_creation.rs @@ -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, + first_name_cache: HashMap>, + last_name_cache: HashSet, + death_check_running: Arc, + death_thread: Option>, +} + +// 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 { + 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 { + match self.load_town_region_ids() { + Ok(rows) => rows + .into_iter() + .filter_map(|row| row.get("id")?.parse::().ok()) + .collect(), + Err(err) => { + eprintln!( + "[CharacterCreationWorker] Fehler in getTownRegionIds: {err}" + ); + Vec::new() + } + } + } + + fn load_town_region_ids(&self) -> Result { + 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::().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::().ok()) { + self.last_name_cache.insert(id); + } + } + + Ok(()) + } + + fn get_random_from_set(set: &HashSet) -> 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| { + 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::().ok()) + .unwrap_or(-1); + let age = row + .get("age") + .and_then(|v| v.parse::().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::().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::().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::().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::().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(()) + } +} + + diff --git a/src/worker/director.rs b/src/worker/director.rs new file mode 100644 index 0000000..d3ba11e --- /dev/null +++ b/src/worker/director.rs @@ -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, +} + +// 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 = 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 { + 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 { + 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 = + 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 { + 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 = + 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 { + 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::().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| { + 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(); + } +} + + diff --git a/src/worker/house.rs b/src/worker/house.rs new file mode 100644 index 0000000..9e33bd8 --- /dev/null +++ b/src/worker/house.rs @@ -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) { + let mut last_hourly_run: Option = None; + let mut last_daily_run: Option = 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::().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| { + 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(); + } +} + + diff --git a/src/worker/mod.rs b/src/worker/mod.rs new file mode 100644 index 0000000..7e45991 --- /dev/null +++ b/src/worker/mod.rs @@ -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; + diff --git a/src/worker/politics.rs b/src/worker/politics.rs new file mode 100644 index 0000000..68a3dcf --- /dev/null +++ b/src/worker/politics.rs @@ -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) { + let mut last_execution: Option = 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 = + 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, 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, 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, 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, + ) -> Result, 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::().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::().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::().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::().ok()) { + let msg = + format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); + broker.publish(msg); + } + } + + Ok(()) + } + + fn process_elections(pool: &ConnectionPool) -> Result, 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| { + 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::().ok()) + .unwrap_or(default) +} + +fn map_row_to_office(row: Row) -> Option { + 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()?, + }) +} + + diff --git a/src/worker/produce.rs b/src/worker/produce.rs new file mode 100644 index 0000000..c0150b8 --- /dev/null +++ b/src/worker/produce.rs @@ -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, +} + +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, 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, 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 { + 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 { + 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 { + 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 { + 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| { + 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(); + } +} diff --git a/src/worker/simple.rs b/src/worker/simple.rs new file mode 100644 index 0000000..6d9949a --- /dev/null +++ b/src/worker/simple.rs @@ -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| { + // 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(); + } + } + }; +} + + diff --git a/src/worker/stockage_manager.rs b/src/worker/stockage_manager.rs new file mode 100644 index 0000000..7dc646e --- /dev/null +++ b/src/worker/stockage_manager.rs @@ -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) { + let mut last_add_run: Option = None; + let mut last_cleanup_run: Option = 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, 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::().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, 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::().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| { + 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(); + } +} + + diff --git a/src/worker/underground.rs b/src/worker/underground.rs new file mode 100644 index 0000000..d3a5f28 --- /dev/null +++ b/src/worker/underground.rs @@ -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) { + 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::().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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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::().ok()) + .unwrap_or(-1)) + } + + fn get_house_by_user(pool: &ConnectionPool, user_id: i32) -> Result, 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 { + 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 = 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 = (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 { + 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::().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 { + 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::().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::().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 { + 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::().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::().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::().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| { + 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 { + let mut idx: Vec = (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::().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(()) +} + + diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs new file mode 100644 index 0000000..54e640a --- /dev/null +++ b/src/worker/user_character.rs @@ -0,0 +1,1179 @@ +use crate::db::{ConnectionPool, DbError, Rows}; +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}; + +/// Vereinfachtes Abbild eines Characters aus `QUERY_GET_USERS_TO_UPDATE`. +#[derive(Debug, Clone)] +struct Character { + id: i32, + age: i32, + health: i32, +} + +pub struct UserCharacterWorker { + base: BaseWorker, + rng: StdRng, + dist: Uniform, + last_hourly_run: Option, + last_pregnancy_run: Option, +} + +// SQL-Queries (1:1 aus der C++-Implementierung übernommen, gruppiert nach Themen) +const QUERY_GET_USERS_TO_UPDATE: &str = r#" + SELECT id, CURRENT_DATE - birthdate::date AS age, health + FROM falukant_data.character + WHERE user_id IS NOT NULL; +"#; + +const QUERY_UPDATE_CHARACTERS_HEALTH: &str = r#" + UPDATE falukant_data.character + SET health = $1 + WHERE id = $2; +"#; + +const QUERY_UPDATE_MOOD: &str = r#" + UPDATE falukant_data.character AS c + SET mood_id = falukant_data.get_random_mood_id() + WHERE c.health > 0; +"#; + +const QUERY_UPDATE_GET_ITEMS_TO_UPDATE: &str = r#" + SELECT id, product_id, producer_id, quantity + FROM falukant_log.production p + WHERE p.production_timestamp::date < current_date; +"#; + +const QUERY_UPDATE_GET_CHARACTER_IDS: &str = r#" + SELECT fu.id AS user_id, + c.id AS character_id, + c2.id AS director_id + FROM falukant_data.falukant_user fu + JOIN falukant_data.character c + ON c.user_id = fu.id + LEFT JOIN falukant_data.director d + ON d.employer_user_id = fu.id + LEFT JOIN falukant_data.character c2 + ON c2.id = d.director_character_id + WHERE fu.id = $1; +"#; + +const QUERY_UPDATE_KNOWLEDGE: &str = r#" + UPDATE falukant_data.knowledge + SET knowledge = LEAST(knowledge + $3, 100) + WHERE character_id = $1 + AND product_id = $2; +"#; + +const QUERY_DELETE_LOG_ENTRY: &str = r#" + DELETE FROM falukant_log.production + WHERE id = $1; +"#; + +// Kredit- und Vermögens-Queries +const QUERY_GET_OPEN_CREDITS: &str = r#" + SELECT + c.id AS credit_id, + c.amount, + c.remaining_amount, + c.interest_rate, + fu.id AS user_id, + fu.money, + c2.id AS character_id, + dp.created_at AS debitor_prism_start, + dp.created_at::date < current_date AS prism_started_previously + FROM falukant_data.credit c + JOIN falukant_data.falukant_user fu + ON fu.id = c.id + JOIN falukant_data.character c2 + ON c2.user_id = c.falukant_user_id + LEFT JOIN falukant_data.debtors_prism dp + ON dp.character_id = c2.id + WHERE c.remaining_amount > 0 + AND c.updated_at::date < current_date; +"#; + +const QUERY_UPDATE_CREDIT: &str = r#" + UPDATE falukant_data.credit c + SET remaining_amount = $1 + WHERE falukant_user_id = $2; +"#; + +const QUERY_CLEANUP_CREDITS: &str = r#" + DELETE FROM falukant_data.credit + WHERE remaining_amount >= 0.01; +"#; + +const QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM: &str = r#" + INSERT INTO falukant_data.debtors_prism (character_id) + VALUES ($1); +"#; + +const QUERY_GET_CURRENT_MONEY: &str = r#" + SELECT COALESCE(money, 0) AS sum + FROM falukant_data.falukant_user + WHERE user_id = $1; +"#; + +const QUERY_HOUSE_VALUE: &str = r#" + SELECT COALESCE(SUM(h.cost), 0) AS sum + FROM falukant_data.user_house AS uh + JOIN falukant_type.house AS h + ON uh.house_type_id = h.id + WHERE uh.user_id = $1; +"#; + +const QUERY_SETTLEMENT_VALUE: &str = r#" + SELECT COALESCE(SUM(b.base_cost), 0) AS sum + FROM falukant_data.branch AS br + JOIN falukant_type.branch AS b + ON br.branch_type_id = b.id + WHERE br.falukant_user_id = $1; +"#; + +const QUERY_INVENTORY_VALUE: &str = r#" + SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum + FROM falukant_data.inventory AS i + JOIN falukant_type.product AS p + ON i.product_id = p.id + JOIN falukant_data.branch AS br + ON i.stock_id = br.id + WHERE br.falukant_user_id = $1; +"#; + +const QUERY_CREDIT_DEBT: &str = r#" + SELECT COALESCE(SUM(remaining_amount), 0) AS sum + FROM falukant_data.credit + WHERE falukant_user_id = $1; +"#; + +const QUERY_COUNT_CHILDREN: &str = r#" + SELECT COUNT(*) AS cnt + FROM falukant_data.child_relation + WHERE father_character_id = $1 + OR mother_character_id = $1; +"#; + +// Vererbungs-Queries +const QUERY_GET_HEIR: &str = r#" + SELECT child_character_id + FROM falukant_data.child_relation + WHERE father_character_id = $1 + OR mother_character_id = $1 + ORDER BY (is_heir IS TRUE) DESC, + updated_at DESC + LIMIT 1; +"#; + +const QUERY_RANDOM_HEIR: &str = r#" + WITH chosen AS ( + SELECT + cr.id AS relation_id, + cr.child_character_id + FROM + falukant_data.child_relation AS cr + JOIN + falukant_data.character AS ch + ON ch.id = cr.child_character_id + WHERE + (cr.father_character_id = $1 OR cr.mother_character_id = $1) + AND ch.region_id = ( + SELECT region_id + FROM falukant_data.character + WHERE id = $1 + ) + AND ch.birthdate >= NOW() - INTERVAL '10 days' + AND ch.title_of_nobility = ( + SELECT id + FROM falukant_type.title + WHERE label_tr = 'noncivil' + ) + ORDER BY RANDOM() + LIMIT 1 + ) + UPDATE + falukant_data.child_relation AS cr2 + SET + is_heir = TRUE, + updated_at = NOW() + FROM + chosen + WHERE + cr2.id = chosen.relation_id + RETURNING + chosen.child_character_id; +"#; + +const QUERY_SET_CHARACTER_USER: &str = r#" + UPDATE falukant_data.character + SET user_id = $1, + updated_at = NOW() + WHERE id = $2; +"#; + +const QUERY_UPDATE_USER_MONEY: &str = r#" + UPDATE falukant_data.falukant_user + SET money = $1, + updated_at = NOW() + WHERE user_id = $2; +"#; + +const QUERY_GET_FALUKANT_USER_ID: &str = r#" + SELECT user_id + FROM falukant_data.character + WHERE id = $1 + LIMIT 1; +"#; + +// Schwangerschafts-Queries +const QUERY_AUTOBATISM: &str = r#" + UPDATE falukant_data.child_relation + SET name_set = TRUE + WHERE id IN ( + SELECT cr.id + FROM falukant_data.child_relation cr + JOIN falukant_data.character c + ON c.id = cr.child_character_id + WHERE cr.name_set = FALSE + AND c.birthdate < current_date - INTERVAL '5 days' + ); +"#; + +const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#" + SELECT + r.character1_id AS father_cid, + r.character2_id AS mother_cid, + c1.title_of_nobility, + c1.last_name, + c1.region_id, + fu1.id AS father_uid, + fu2.id AS mother_uid, + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 AS avg_age_days, + 100.0 / + (1 + EXP( + 0.0647 * ( + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 + ) - 0.0591 + )) AS prob_pct + FROM falukant_data.relationship r + JOIN falukant_type.relationship r2 + ON r2.id = r.relationship_type_id + AND r2.tr = 'married' + JOIN falukant_data.character c1 + ON c1.id = r.character1_id + JOIN falukant_data.character c2 + ON c2.id = r.character2_id + LEFT JOIN falukant_data.falukant_user fu1 + ON fu1.id = c1.user_id + LEFT JOIN falukant_data.falukant_user fu2 + ON fu2.id = c2.user_id + WHERE random() * 100 < ( + 100.0 / + (1 + EXP( + 0.11166347 * ( + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 + ) - 2.638267 + )) + ) / 2; +"#; + +const QUERY_INSERT_CHILD: &str = r#" + INSERT INTO falukant_data.character ( + user_id, + region_id, + first_name, + last_name, + birthdate, + gender, + title_of_nobility, + mood_id, + created_at, + updated_at + ) VALUES ( + NULL, + $1::int, + ( + SELECT id + FROM falukant_predefine.firstname + WHERE gender = $2 + ORDER BY RANDOM() + LIMIT 1 + ), + $3::int, + NOW(), + $2::varchar, + $4::int, + ( + SELECT id + FROM falukant_type.mood + ORDER BY RANDOM() + LIMIT 1 + ), + NOW(), + NOW() + ) + RETURNING id AS child_cid; +"#; + +const QUERY_INSERT_CHILD_RELATION: &str = r#" + INSERT INTO falukant_data.child_relation ( + father_character_id, + mother_character_id, + child_character_id, + name_set, + created_at, + updated_at + ) + VALUES ( + $1::int, + $2::int, + $3::int, + FALSE, + NOW(), NOW() + ); +"#; + +// Aufräum-Queries beim Tod eines Characters +const QUERY_DELETE_DIRECTOR: &str = r#" + DELETE FROM falukant_data.director + WHERE director_character_id = $1; +"#; + +const QUERY_DELETE_RELATIONSHIP: &str = r#" + DELETE FROM falukant_data.relationship + WHERE character1_id = $1 + OR character2_id = $1; +"#; + +const QUERY_DELETE_CHILD_RELATION: &str = r#" + DELETE FROM falukant_data.child_relation + WHERE child_character_id = $1 + OR father_character_id = $1 + OR mother_character_id = $1; +"#; + +const QUERY_DELETE_KNOWLEDGE: &str = r#" + DELETE FROM falukant_data.knowledge + WHERE character_id = $1; +"#; + +const QUERY_DELETE_DEBTORS_PRISM: &str = r#" + DELETE FROM falukant_data.debtors_prism + WHERE character_id = $1; +"#; + +const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" + DELETE FROM falukant_data.political_office + WHERE character_id = $1; +"#; + +const QUERY_DELETE_ELECTION_CANDIDATE: &str = r#" + DELETE FROM falukant_data.election_candidate + WHERE character_id = $1; +"#; + +impl UserCharacterWorker { + pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { + let base = BaseWorker::new("UserCharacterWorker", pool, broker); + let rng = StdRng::from_entropy(); + let dist = Uniform::from(0.0..1.0); + + Self { + base, + rng, + dist, + last_hourly_run: None, + last_pregnancy_run: None, + } + } + + fn run_iteration(&mut self, state: &WorkerState) { + self.base.set_current_step("UserCharacterWorker iteration"); + + self.maybe_run_hourly_tasks(); + self.maybe_run_daily_pregnancies(); + + // Entspricht in etwa der 1-Sekunden-Schleife im C++-Code + std::thread::sleep(Duration::from_secs(1)); + + if let Err(err) = self.recalculate_knowledge() { + eprintln!("[UserCharacterWorker] Fehler in recalculateKnowledge: {err}"); + } + + if !state.running_worker.load(Ordering::Relaxed) { + return; + } + } + + fn maybe_run_hourly_tasks(&mut self) { + let now = Instant::now(); + let should_run = match self.last_hourly_run { + None => true, + Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(3600), + }; + + if !should_run { + return; + } + + if let Err(err) = self.run_hourly_tasks() { + eprintln!("[UserCharacterWorker] Fehler in stündlichen Tasks: {err}"); + } + + self.last_hourly_run = Some(now); + } + + fn run_hourly_tasks(&mut self) -> Result<(), DbError> { + self.process_character_events()?; + self.update_characters_mood()?; + self.handle_credits()?; + Ok(()) + } + + fn maybe_run_daily_pregnancies(&mut self) { + let now = Instant::now(); + let should_run = match self.last_pregnancy_run { + None => true, + Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(24 * 3600), + }; + + if !should_run { + return; + } + + if let Err(err) = self.process_pregnancies() { + eprintln!("[UserCharacterWorker] Fehler in processPregnancies: {err}"); + } + self.last_pregnancy_run = Some(now); + } + + fn process_character_events(&mut self) -> Result<(), DbError> { + self.base.set_current_step("Get character data"); + + let rows = self.load_characters_to_update()?; + let mut characters: Vec = rows + .into_iter() + .filter_map(Self::map_row_to_character) + .collect(); + + for character in &mut characters { + self.update_character_health(character)?; + } + + Ok(()) + } + + fn load_characters_to_update(&mut self) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("get_users_to_update", QUERY_GET_USERS_TO_UPDATE)?; + conn.execute("get_users_to_update", &[]) + } + + fn map_row_to_character(row: crate::db::Row) -> Option { + Some(Character { + id: row.get("id")?.parse().ok()?, + age: row.get("age")?.parse().ok()?, + health: row.get("health")?.parse().ok()?, + }) + } + + fn update_character_health(&mut self, character: &mut Character) -> Result<(), DbError> { + let health_change = self.calculate_health_change(character.age); + if health_change == 0 { + return Ok(()); + } + + character.health = std::cmp::max(0, character.health + health_change); + + if character.health == 0 { + self.handle_character_death(character.id)?; + return Ok(()); + } + + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "update_characters_health", + QUERY_UPDATE_CHARACTERS_HEALTH, + )?; + conn.execute( + "update_characters_health", + &[&character.health, &character.id], + )?; + + Ok(()) + } + + fn calculate_health_change(&mut self, age: i32) -> i32 { + if age < 30 { + return 0; + } + + if age >= 45 { + let probability = (0.1 + (age - 45) as f64 * 0.02).min(1.0); + if self.dist.sample(&mut self.rng) < probability { + let damage_dist = Uniform::from(1..=10); + return -damage_dist.sample(&mut self.rng); + } + return 0; + } + + let probability = (age - 30) as f64 / 30.0; + if self.dist.sample(&mut self.rng) < probability { + -1 + } else { + 0 + } + } + + fn update_characters_mood(&mut self) -> Result<(), DbError> { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("update_mood", QUERY_UPDATE_MOOD)?; + conn.execute("update_mood", &[])?; + Ok(()) + } + + fn recalculate_knowledge(&mut self) -> Result<(), DbError> { + self.base.set_current_step("recalculate knowledge"); + + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare( + "get_items_to_update", + QUERY_UPDATE_GET_ITEMS_TO_UPDATE, + )?; + let update_rows = conn.execute("get_items_to_update", &[])?; + + for update_item in update_rows { + let quantity: i32 = match update_item.get("quantity").and_then(|v| v.parse().ok()) { + Some(q) => q, + None => continue, + }; + + if quantity < 10 { + self.delete_production_log_entry(&mut conn, &update_item)?; + continue; + } + + self.update_knowledge_for_production(&mut conn, &update_item)?; + self.delete_production_log_entry(&mut conn, &update_item)?; + + if let Some(producer_id) = update_item + .get("producer_id") + .and_then(|v| v.parse::().ok()) + { + self.send_knowledge_update(producer_id); + } + } + + Ok(()) + } + + fn update_knowledge_for_production( + &mut self, + conn: &mut crate::db::DbConnection, + update_item: &crate::db::Row, + ) -> Result<(), DbError> { + let producer_id = match update_item.get("producer_id").and_then(|v| v.parse::().ok()) + { + Some(id) => id, + None => return Ok(()), + }; + + let product_id = match update_item.get("product_id").and_then(|v| v.parse::().ok()) { + Some(id) => id, + None => return Ok(()), + }; + + conn.prepare( + "get_character_ids", + QUERY_UPDATE_GET_CHARACTER_IDS, + )?; + let characters_data = + conn.execute("get_character_ids", &[&producer_id])?; + + conn.prepare("update_knowledge", QUERY_UPDATE_KNOWLEDGE)?; + + for character_row in characters_data { + let character_id = match character_row + .get("character_id") + .and_then(|v| v.parse::().ok()) + { + Some(id) => id, + None => continue, + }; + + let director_id = character_row + .get("director_id") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + + if director_id == 0 { + conn.execute( + "update_knowledge", + &[&character_id, &product_id, &2_i32], + )?; + } else { + conn.execute( + "update_knowledge", + &[&character_id, &product_id, &1_i32], + )?; + conn.execute( + "update_knowledge", + &[&director_id, &product_id, &1_i32], + )?; + } + } + + Ok(()) + } + + fn delete_production_log_entry( + &mut self, + conn: &mut crate::db::DbConnection, + update_item: &crate::db::Row, + ) -> Result<(), DbError> { + let id = match update_item.get("id").and_then(|v| v.parse::().ok()) { + Some(id) => id, + None => return Ok(()), + }; + + conn.prepare("delete_log_entry", QUERY_DELETE_LOG_ENTRY)?; + conn.execute("delete_log_entry", &[&id])?; + Ok(()) + } + + fn send_knowledge_update(&self, producer_id: i32) { + let message = format!(r#"{{"event":"knowledge_update","user_id":{}}}"#, producer_id); + self.base.broker.publish(message); + } + + // Kredit-Logik (portiert aus handleCredits) + fn handle_credits(&mut self) -> Result<(), DbError> { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("get_open_credits", QUERY_GET_OPEN_CREDITS)?; + conn.prepare("update_credit", QUERY_UPDATE_CREDIT)?; + conn.prepare("cleanup_credits", QUERY_CLEANUP_CREDITS)?; + conn.prepare( + "add_character_to_debtors_prism", + QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM, + )?; + + let credits_rows = conn.execute("get_open_credits", &[])?; + for row in credits_rows { + if let Some(credit) = Self::map_row_to_credit(&row) { + self.process_single_credit(&mut conn, &credit)?; + } + } + + conn.execute("cleanup_credits", &[])?; + Ok(()) + } + + fn map_row_to_credit(row: &crate::db::Row) -> Option { + Some(Credit { + credit_id: row.get("credit_id")?.parse().ok()?, + amount: row.get("amount")?.parse().ok()?, + remaining_amount: row.get("remaining_amount")?.parse().ok()?, + interest_rate: row.get("interest_rate")?.parse().ok()?, + user_id: row.get("user_id")?.parse().ok()?, + money: row.get("money")?.parse().ok()?, + character_id: row.get("character_id")?.parse().ok()?, + prism_started_previously: row + .get("prism_started_previously") + .map(|v| v == "t" || v == "true") + .unwrap_or(false), + }) + } + + fn process_single_credit( + &mut self, + conn: &mut crate::db::DbConnection, + credit: &Credit, + ) -> Result<(), DbError> { + let Credit { + amount, + mut remaining_amount, + interest_rate, + user_id, + money, + character_id, + prism_started_previously, + .. + } = *credit; + + let pay_rate = amount / 10.0 + amount * interest_rate as f64 / 100.0; + remaining_amount -= pay_rate; + + // Kann der User zahlen? + if pay_rate <= money - (pay_rate * 3.0) { + if let Err(err) = self + .base + .change_falukant_user_money(user_id, -pay_rate, "credit pay rate") + { + eprintln!( + "[UserCharacterWorker] Fehler bei change_falukant_user_money (credit pay rate): {err}" + ); + } + } else if prism_started_previously { + if let Err(err) = self + .base + .change_falukant_user_money(user_id, pay_rate, "debitor_prism") + { + eprintln!( + "[UserCharacterWorker] Fehler bei change_falukant_user_money (debitor_prism): {err}" + ); + } + } else { + conn.execute("add_character_to_debtors_prism", &[&character_id])?; + } + + conn.execute("update_credit", &[&remaining_amount, &user_id])?; + Ok(()) + } + + // Schwangerschafts-Logik (portiert aus processPregnancies) + fn process_pregnancies(&mut self) -> Result<(), DbError> { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("autobatism", QUERY_AUTOBATISM)?; + conn.execute("autobatism", &[])?; + + conn.prepare("get_pregnancy_candidates", QUERY_GET_PREGNANCY_CANDIDATES)?; + let rows = conn.execute("get_pregnancy_candidates", &[])?; + + conn.prepare("insert_child", QUERY_INSERT_CHILD)?; + conn.prepare("insert_child_relation", QUERY_INSERT_CHILD_RELATION)?; + + for row in rows { + self.process_single_pregnancy_candidate(&mut conn, &row)?; + } + + Ok(()) + } + + fn process_single_pregnancy_candidate( + &mut self, + conn: &mut crate::db::DbConnection, + row: &crate::db::Row, + ) -> Result<(), DbError> { + let father_cid = parse_i32(row, "father_cid", -1); + let mother_cid = parse_i32(row, "mother_cid", -1); + if father_cid < 0 || mother_cid < 0 { + return Ok(()); + } + + let title_of_nobility = parse_i32(row, "title_of_nobility", 0); + let last_name = parse_i32(row, "last_name", 0); + let region_id = parse_i32(row, "region_id", 0); + + let father_uid = parse_opt_i32(row, "father_uid"); + let mother_uid = parse_opt_i32(row, "mother_uid"); + + let gender = if self.dist.sample(&mut self.rng) < 0.5 { + "male" + } else { + "female" + }; + + let inserted = + conn.execute("insert_child", &[®ion_id, &gender, &last_name, &title_of_nobility])?; + let child_cid = inserted + .get(0) + .and_then(|r| r.get("child_cid")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1); + if child_cid < 0 { + return Ok(()); + } + + conn.execute( + "insert_child_relation", + &[&father_cid, &mother_cid, &child_cid], + )?; + + if let Some(f_uid) = father_uid { + self.send_children_update_and_status(f_uid); + } + if let Some(m_uid) = mother_uid { + self.send_children_update_and_status(m_uid); + } + + Ok(()) + } + + fn send_children_update_and_status(&self, user_id: i32) { + let children_update = + format!(r#"{{"event":"children_update","user_id":{}}}"#, user_id); + self.base.broker.publish(children_update); + + let update_status = + format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); + self.base.broker.publish(update_status); + } + + // Todes- und Erb-Logik + fn handle_character_death(&mut self, character_id: i32) -> Result<(), DbError> { + self.set_heir(character_id)?; + + let death_event = format!( + r#"{{"event":"CharacterDeath","character_id":{}}}"#, + character_id + ); + self.base.broker.publish(death_event); + + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?; + conn.prepare("delete_relationship", QUERY_DELETE_RELATIONSHIP)?; + conn.prepare("delete_child_relation", QUERY_DELETE_CHILD_RELATION)?; + conn.prepare("delete_knowledge", QUERY_DELETE_KNOWLEDGE)?; + conn.prepare("delete_debtors_prism", QUERY_DELETE_DEBTORS_PRISM)?; + conn.prepare("delete_political_office", QUERY_DELETE_POLITICAL_OFFICE)?; + conn.prepare("delete_election_candidate", QUERY_DELETE_ELECTION_CANDIDATE)?; + + conn.execute("delete_director", &[&character_id])?; + conn.execute("delete_relationship", &[&character_id])?; + conn.execute("delete_child_relation", &[&character_id])?; + conn.execute("delete_knowledge", &[&character_id])?; + conn.execute("delete_debtors_prism", &[&character_id])?; + conn.execute("delete_political_office", &[&character_id])?; + conn.execute("delete_election_candidate", &[&character_id])?; + + // Character selbst löschen + conn.prepare( + "delete_character", + r#"DELETE FROM falukant_data.character WHERE id = $1"#, + )?; + conn.execute("delete_character", &[&character_id])?; + + Ok(()) + } + + fn set_heir(&mut self, character_id: i32) -> Result<(), DbError> { + let falukant_user_id = self.get_falukant_user_id(character_id)?; + if falukant_user_id < 0 { + return Ok(()); + } + + let mut heir_id = self.get_heir_from_children(character_id)?; + let mut new_money = self.calculate_new_money(falukant_user_id, heir_id > 0)?; + + if heir_id < 1 { + heir_id = self.get_random_heir(character_id)?; + new_money = self.calculate_new_money(falukant_user_id, heir_id > 0)?; + } + + if heir_id > 0 { + self.set_new_character(falukant_user_id, heir_id)?; + } + self.set_new_money(falukant_user_id, new_money)?; + + Ok(()) + } + + fn get_falukant_user_id(&mut self, character_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("get_falukant_user_id", QUERY_GET_FALUKANT_USER_ID)?; + let rows = conn.execute("get_falukant_user_id", &[&character_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("user_id")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1)) + } + + fn get_heir_from_children(&mut self, deceased_character_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("get_heir", QUERY_GET_HEIR)?; + let rows = conn.execute("get_heir", &[&deceased_character_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("child_character_id")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1)) + } + + fn get_random_heir(&mut self, deceased_character_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("random_heir", QUERY_RANDOM_HEIR)?; + let rows = conn.execute("random_heir", &[&deceased_character_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("child_character_id")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1)) + } + + fn set_new_character( + &mut self, + falukant_user_id: i32, + heir_character_id: i32, + ) -> Result<(), DbError> { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("set_character_user", QUERY_SET_CHARACTER_USER)?; + conn.execute( + "set_character_user", + &[&falukant_user_id, &heir_character_id], + )?; + Ok(()) + } + + fn set_new_money(&mut self, falukant_user_id: i32, new_amount: f64) -> Result<(), DbError> { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("update_user_money", QUERY_UPDATE_USER_MONEY)?; + conn.execute("update_user_money", &[&new_amount, &falukant_user_id])?; + Ok(()) + } + + fn get_current_money(&mut self, falukant_user_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("get_current_money", QUERY_GET_CURRENT_MONEY)?; + let rows = conn.execute("get_current_money", &[&falukant_user_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("sum")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0.0)) + } + + fn get_house_value(&mut self, falukant_user_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("house_value", QUERY_HOUSE_VALUE)?; + let rows = conn.execute("house_value", &[&falukant_user_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("sum")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0.0)) + } + + fn get_settlement_value(&mut self, falukant_user_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("settlement_value", QUERY_SETTLEMENT_VALUE)?; + let rows = conn.execute("settlement_value", &[&falukant_user_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("sum")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0.0)) + } + + fn get_inventory_value(&mut self, falukant_user_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("inventory_value", QUERY_INVENTORY_VALUE)?; + let rows = conn.execute("inventory_value", &[&falukant_user_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("sum")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0.0)) + } + + fn get_credit_debt(&mut self, falukant_user_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("credit_debt", QUERY_CREDIT_DEBT)?; + let rows = conn.execute("credit_debt", &[&falukant_user_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("sum")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0.0)) + } + + fn get_child_count(&mut self, deceased_user_id: i32) -> Result { + let mut conn = self + .base + .pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("count_children", QUERY_COUNT_CHILDREN)?; + let rows = conn.execute("count_children", &[&deceased_user_id])?; + + Ok(rows + .get(0) + .and_then(|r| r.get("cnt")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0)) + } + + fn calculate_new_money( + &mut self, + falukant_user_id: i32, + has_heir: bool, + ) -> Result { + if !has_heir { + return Ok(800.0); + } + + let cash = self.get_current_money(falukant_user_id)?; + let houses = self.get_house_value(falukant_user_id)?; + let settlements = self.get_settlement_value(falukant_user_id)?; + let inventory = self.get_inventory_value(falukant_user_id)?; + let debt = self.get_credit_debt(falukant_user_id)?; + + let total_assets = cash + houses + settlements + inventory - debt; + let child_count = self.get_child_count(falukant_user_id)?; + let single = child_count <= 1; + + let heir_share = if single { + total_assets + } else { + total_assets * 0.8 + }; + + let net = heir_share - (houses + settlements + inventory + debt); + if net <= 1000.0 { + Ok(1000.0) + } else { + Ok(net) + } + } +} + +/// Kleine Hilfsfunktionen für robustes Parsen aus `Row`. +fn parse_i32(row: &crate::db::Row, key: &str, default: i32) -> i32 { + row.get(key) + .and_then(|v| v.parse::().ok()) + .unwrap_or(default) +} + +fn parse_opt_i32(row: &crate::db::Row, key: &str) -> Option { + row.get(key).and_then(|v| v.parse::().ok()) +} + +#[derive(Debug, Clone)] +struct Credit { + credit_id: i32, + amount: f64, + remaining_amount: f64, + interest_rate: i32, + user_id: i32, + money: f64, + character_id: i32, + prism_started_previously: bool, +} + +impl Worker for UserCharacterWorker { + 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| { + let mut worker = UserCharacterWorker::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(); + } +} + + diff --git a/src/worker/value_recalculation.rs b/src/worker/value_recalculation.rs new file mode 100644 index 0000000..33756a7 --- /dev/null +++ b/src/worker/value_recalculation.rs @@ -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) { + // 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::().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::().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::().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::().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::().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::().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, 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::().ok())) + } + + fn calculate_studying_character( + pool: &ConnectionPool, + broker: &MessageBroker, + character_id: i32, + learn_all: bool, + product_id: Option, + 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| { + 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, + 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::().ok()) + .unwrap_or(default) +} + +fn study_scope(entry: &Row) -> (bool, Option) { + 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::().ok(); + match pid { + Some(id) => (false, Some(id)), + None => (true, None), + } + } +} + +