Compare commits
13 Commits
master
...
parsing_re
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c94356263 | |||
| 797e5ea192 | |||
| 38de4a7419 | |||
| f695f8a52e | |||
| e29ffb2a9c | |||
| c58bcd4aac | |||
| b1f0714483 | |||
| 9d3313995e | |||
| 150bd2f5cf | |||
| 519d0cd3a7 | |||
| 8632ba0a86 | |||
| 588790b9b4 | |||
| 5bd9aadc55 |
287
Cargo.lock
generated
287
Cargo.lock
generated
@ -78,6 +78,12 @@ version = "2.9.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@ -96,6 +102,73 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a48563284b67c003ba0fb7243c87fab68885e1532c605704228a80238512e31"
|
checksum = "1a48563284b67c003ba0fb7243c87fab68885e1532c605704228a80238512e31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"derive_more",
|
||||||
|
"document-features",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"rustix",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@ -109,13 +182,38 @@ dependencies = [
|
|||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||||
|
dependencies = [
|
||||||
|
"derive_more-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more-impl"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dev_tests"
|
name = "dev_tests"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chardet",
|
"chardet",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
"is-terminal",
|
||||||
|
"libc",
|
||||||
"rottlib",
|
"rottlib",
|
||||||
|
"sysinfo",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -130,6 +228,21 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
@ -139,6 +252,16 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -243,6 +366,12 @@ version = "0.14.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@ -367,6 +496,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -385,12 +525,24 @@ version = "0.2.174"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@ -401,6 +553,12 @@ dependencies = [
|
|||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "logos"
|
name = "logos"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@ -470,10 +628,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntapi"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.36.7"
|
||||||
@ -577,6 +745,26 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.17"
|
version = "0.5.17"
|
||||||
@ -596,6 +784,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
name = "rottlib"
|
name = "rottlib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"bumpalo",
|
||||||
|
"crossterm",
|
||||||
"logos",
|
"logos",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -623,6 +814,19 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
@ -693,6 +897,27 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.5"
|
version = "1.4.5"
|
||||||
@ -752,6 +977,21 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sysinfo"
|
||||||
|
version = "0.30.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"ntapi",
|
||||||
|
"once_cell",
|
||||||
|
"rayon",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@ -903,6 +1143,12 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
@ -937,6 +1183,22 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@ -946,6 +1208,31 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
|
|||||||
@ -20,7 +20,7 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations
|
|||||||
debug = false # strip all debug info
|
debug = false # strip all debug info
|
||||||
|
|
||||||
[profile.flamegraph]
|
[profile.flamegraph]
|
||||||
inherits = "release" # start from release
|
inherits = "dev" # start from release
|
||||||
strip = false
|
strip = false
|
||||||
debug = true # full DWARF info for unwinding
|
debug = true # full DWARF info for unwinding
|
||||||
split-debuginfo = "unpacked" # keep symbols inside the binary
|
split-debuginfo = "unpacked" # keep symbols inside the binary
|
||||||
|
|||||||
@ -3,22 +3,21 @@ name = "dev_tests"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "dump_tokens"
|
|
||||||
path = "src/dump_tokens.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "uc_lexer_verify"
|
name = "uc_lexer_verify"
|
||||||
path = "src/uc_lexer_verify.rs"
|
path = "src/uc_lexer_verify.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "temp"
|
name = "verify_expr"
|
||||||
path = "src/temp.rs"
|
path = "src/verify_expr.rs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rottlib = { version = "0", path = "../rottlib", features = ["debug"] }
|
rottlib = { version = "0", path = "../rottlib", features = ["debug"] }
|
||||||
|
is-terminal = "0.4"
|
||||||
|
libc = "0.2"
|
||||||
|
sysinfo = "0.30"
|
||||||
walkdir="2.5"
|
walkdir="2.5"
|
||||||
encoding_rs="0.8"
|
encoding_rs="0.8"
|
||||||
chardet="0.2"
|
chardet="0.2"
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use encoding_rs::{Encoding, UTF_8};
|
|
||||||
use rottlib::lexer::{DebugTools, TokenizedFile};
|
|
||||||
|
|
||||||
/// Recursively search `root` for the first file whose *basename* matches
|
|
||||||
/// `needle` (case-sensitive).
|
|
||||||
///
|
|
||||||
/// Returns the absolute path.
|
|
||||||
fn find_file(root: &Path, needle: &str) -> Option<PathBuf> {
|
|
||||||
for entry in walkdir::WalkDir::new(root)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
{
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_file() && (path.file_name().and_then(|name| name.to_str()) == Some(needle)) {
|
|
||||||
return fs::canonicalize(path).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CLI: `dump_tokens <root_dir> <file_name>` - searches for `<file_name>`
|
|
||||||
/// recursively inside `<root_dir>`.
|
|
||||||
///
|
|
||||||
/// This utility takes *root directory* and *file name* instead of the full path
|
|
||||||
/// to help us avoid searching for them typing names out:
|
|
||||||
///
|
|
||||||
/// - We know where all the sources are;
|
|
||||||
/// - We usually just know the name of the file that is being problematic.
|
|
||||||
fn main() {
|
|
||||||
let mut args = std::env::args().skip(1);
|
|
||||||
let root_dir = args.next().unwrap_or_else(|| {
|
|
||||||
eprintln!("Usage: inspect_uc <root_dir> <file_name>");
|
|
||||||
std::process::exit(1);
|
|
||||||
});
|
|
||||||
let file_name = args.next().unwrap_or_else(|| {
|
|
||||||
eprintln!("Usage: inspect_uc <root_dir> <file_name>");
|
|
||||||
std::process::exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
let root = PathBuf::from(&root_dir);
|
|
||||||
if !root.exists() {
|
|
||||||
eprintln!("Root directory '{root_dir}' does not exist.");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let found_path = find_file(&root, &file_name).map_or_else(
|
|
||||||
|| {
|
|
||||||
eprintln!("File '{file_name}' not found under '{root_dir}'.");
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
|path| path,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Read & decode
|
|
||||||
let raw_bytes = match fs::read(&found_path) {
|
|
||||||
Ok(sources) => sources,
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Could not read {}: {error}", found_path.display());
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (encoding_label, _, _) = chardet::detect(&raw_bytes);
|
|
||||||
let encoding = Encoding::for_label(encoding_label.as_bytes()).unwrap_or(UTF_8);
|
|
||||||
let (decoded_str, _, _) = encoding.decode(&raw_bytes);
|
|
||||||
|
|
||||||
let source_text = decoded_str.to_string();
|
|
||||||
let tokenized_file = TokenizedFile::from_str(&source_text);
|
|
||||||
|
|
||||||
tokenized_file.dump_debug_layout();
|
|
||||||
}
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
//! src/main.rs
|
|
||||||
//! --------------------------------------------
|
|
||||||
//! Build & run:
|
|
||||||
//! cargo run
|
|
||||||
//! --------------------------------------------
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use rottlib::arena::Arena;
|
|
||||||
use rottlib::lexer::TokenizedFile;
|
|
||||||
use rottlib::parser::{ParseError, Parser, pretty::ExprTree};
|
|
||||||
|
|
||||||
/*
|
|
||||||
- Convenient array definitions: [1, 3, 5, 2, 4]
|
|
||||||
- Boolean dynamic arrays
|
|
||||||
- Structures in default properties
|
|
||||||
- Auto conversion of arrays into strings
|
|
||||||
- Making 'var' and 'local' unnecessary
|
|
||||||
- Allowing variable creation in 'for' loops
|
|
||||||
- Allowing variable creation at any place inside a function
|
|
||||||
- Default parameters for functions
|
|
||||||
- Function overloading?
|
|
||||||
- repeat/until
|
|
||||||
- The syntax of the default properties block is pretty strict for an arcane reason. Particularly adding spaces before or after the "=" will lead to errors in pre-UT2003 versions.
|
|
||||||
- Scopes
|
|
||||||
- different names for variables and in config file
|
|
||||||
- anonymous pairs (objects?) and value destruction
|
|
||||||
>>> AST > HIR > MIR > byte code
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// Closest plan:
|
|
||||||
/// - Add top-level declaration parsing
|
|
||||||
/// - Handle pretty.rs shit somehow
|
|
||||||
/// - COMMITS
|
|
||||||
/// ---------------------------------------
|
|
||||||
/// - Add fancy error reporting
|
|
||||||
/// - Make a fancy REPL
|
|
||||||
/// - Add evaluation
|
|
||||||
///
|
|
||||||
/// WARNINGS:
|
|
||||||
/// - Empty code/switch blocks
|
|
||||||
|
|
||||||
fn parse_and_print(src: &str) -> Result<(), ParseError> {
|
|
||||||
let tokenized = TokenizedFile::from_str(src);
|
|
||||||
let arena = Arena::new();
|
|
||||||
let mut parser = Parser::new(&tokenized, &arena);
|
|
||||||
|
|
||||||
let expr = parser.parse_expression(); // ArenaNode<Expression>
|
|
||||||
println!("{}", ExprTree(&*expr)); // if ArenaNode<Deref>
|
|
||||||
// or: println!("{}", ExprTree(expr.as_ref())); // if no Deref
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn repl_once() -> Result<(), ParseError> {
|
|
||||||
print!("Enter an statement > ");
|
|
||||||
io::stdout().flush().unwrap();
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
if io::stdin().read_line(&mut input).is_err() {
|
|
||||||
eprintln!("failed to read input");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.trim().is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_and_print(&input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_stdin_all() -> io::Result<String> {
|
|
||||||
let mut buf = String::new();
|
|
||||||
io::stdin().read_to_string(&mut buf)?;
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_file_to_string(path: &Path) -> io::Result<String> {
|
|
||||||
fs::read_to_string(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), ParseError> {
|
|
||||||
// Accept a single positional arg as the input path.
|
|
||||||
// "-" means read all of stdin.
|
|
||||||
let mut args = env::args().skip(1);
|
|
||||||
|
|
||||||
if let Some(arg1) = args.next() {
|
|
||||||
if arg1 == "-h" || arg1 == "--help" {
|
|
||||||
println!("Usage:");
|
|
||||||
println!(
|
|
||||||
" {} # REPL",
|
|
||||||
env::args().next().unwrap_or_else(|| "prog".into())
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" {} <file> # parse file",
|
|
||||||
env::args().next().unwrap_or_else(|| "prog".into())
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" {} - # read source from stdin",
|
|
||||||
env::args().next().unwrap_or_else(|| "prog".into())
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if arg1 == "-" {
|
|
||||||
match read_stdin_all() {
|
|
||||||
Ok(src) => return parse_and_print(&src),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("stdin read error: {}", e);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let path = Path::new(&arg1);
|
|
||||||
match read_file_to_string(path) {
|
|
||||||
Ok(src) => return parse_and_print(&src),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("file read error ({}): {}", path.display(), e);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No filename provided -> keep REPL behavior
|
|
||||||
repl_once()
|
|
||||||
}
|
|
||||||
@ -1,122 +1,356 @@
|
|||||||
use std::{collections::HashSet, fs, path::PathBuf};
|
#![allow(
|
||||||
|
clippy::all,
|
||||||
|
clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo,
|
||||||
|
clippy::restriction
|
||||||
|
)]
|
||||||
|
|
||||||
use rottlib::lexer::{DebugTools, TokenizedFile};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
fs,
|
||||||
|
io::{self, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
/// Read `ignore.txt` (one path per line, `#` for comments) from root directory
|
use encoding_rs::Encoding;
|
||||||
/// and turn it into a canonicalized [`HashSet<PathBuf>`].
|
use rottlib::diagnostics::Diagnostic as Diag;
|
||||||
|
use rottlib::lexer::TokenizedFile;
|
||||||
|
use rottlib::parser::Parser;
|
||||||
|
|
||||||
|
// ---------- CONFIG ----------
|
||||||
|
const FILE_LIMIT: usize = 10000; // cap on files scanned
|
||||||
|
const DIAG_SHOW_FIRST: usize = 12; // show first N diagnostics
|
||||||
|
const DIAG_SHOW_LAST: usize = 12; // show last N diagnostics
|
||||||
|
/// If true, print the old debug struct dump after each pretty diagnostic.
|
||||||
|
const ALSO_PRINT_DEBUG_AFTER_PRETTY: bool = true;
|
||||||
|
|
||||||
|
// Cargo.toml additions:
|
||||||
|
// is-terminal = "0.4"
|
||||||
|
// sysinfo = { version = "0.30", features = ["multithread"] }
|
||||||
|
// walkdir = "2"
|
||||||
|
// chardet = "0.2"
|
||||||
|
// encoding_rs = "0.8"
|
||||||
|
|
||||||
|
fn render_diagnostic(diag: &Diag, file: &TokenizedFile<'_>, file_name: &str) {
|
||||||
|
diag.render(file, file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_diagnostics_window(diags: &[Diag], tf: &TokenizedFile<'_>, file_name: &str) {
|
||||||
|
let total = diags.len();
|
||||||
|
let first_n = DIAG_SHOW_FIRST.min(total);
|
||||||
|
let last_n = DIAG_SHOW_LAST.min(total.saturating_sub(first_n));
|
||||||
|
|
||||||
|
if total > first_n + last_n {
|
||||||
|
for (k, d) in diags.iter().take(first_n).enumerate() {
|
||||||
|
render_diagnostic(d, tf, file_name);
|
||||||
|
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||||
|
eprintln!("#{}: {:#?}", k + 1, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("... {} diagnostics omitted ...", total - (first_n + last_n));
|
||||||
|
|
||||||
|
let start = total - last_n;
|
||||||
|
for (offset, d) in diags.iter().skip(start).enumerate() {
|
||||||
|
let idx_global = start + offset + 1;
|
||||||
|
render_diagnostic(d, tf, file_name);
|
||||||
|
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||||
|
eprintln!("#{idx_global}: {d:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (k, d) in diags.iter().enumerate() {
|
||||||
|
render_diagnostic(d, tf, file_name);
|
||||||
|
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||||
|
eprintln!("#{}: {:#?}", k + 1, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux-only accurate RSS in MB. Fallback uses sysinfo.
|
||||||
|
fn rss_mb() -> u64 {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use std::io::Read;
|
||||||
|
let mut s = String::new();
|
||||||
|
if let Ok(mut f) = std::fs::File::open("/proc/self/statm")
|
||||||
|
&& f.read_to_string(&mut s).is_ok()
|
||||||
|
&& let Some(rss_pages) = s
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|x| x.parse::<u64>().ok())
|
||||||
|
{
|
||||||
|
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as u64 };
|
||||||
|
return (rss_pages * page) / (1024 * 1024);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use sysinfo::{System, get_current_pid};
|
||||||
|
let mut sys = System::new();
|
||||||
|
sys.refresh_processes();
|
||||||
|
let Ok(pid) = get_current_pid() else { return 0 };
|
||||||
|
sys.process(pid).map_or(0, |p| p.memory() / 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark(label: &str, t0: Instant) {
|
||||||
|
println!(
|
||||||
|
"[{:>14}] t={:>8.2?} rss={} MB",
|
||||||
|
label,
|
||||||
|
t0.elapsed(),
|
||||||
|
rss_mb()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `ignore.txt` next to `root` and build a canonicalized set.
|
||||||
fn load_ignore_set(root: &std::path::Path) -> HashSet<PathBuf> {
|
fn load_ignore_set(root: &std::path::Path) -> HashSet<PathBuf> {
|
||||||
let ignore_file = root.join("ignore.txt");
|
let ignore_file = root.join("ignore.txt");
|
||||||
if !ignore_file.exists() {
|
if !ignore_file.exists() {
|
||||||
return HashSet::new();
|
return HashSet::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = match fs::read_to_string(&ignore_file) {
|
let content = match fs::read_to_string(&ignore_file) {
|
||||||
Ok(content) => content,
|
Ok(s) => s,
|
||||||
Err(error) => {
|
Err(e) => {
|
||||||
eprintln!("Could not read {}: {error}", ignore_file.display());
|
eprintln!("Could not read {}: {e}", ignore_file.display());
|
||||||
return HashSet::new();
|
return HashSet::new();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
content
|
content
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||||
.filter_map(|line| {
|
.filter_map(|line| {
|
||||||
let next_path = PathBuf::from(line);
|
let p = PathBuf::from(line);
|
||||||
let absolute_path = if next_path.is_absolute() {
|
let abs = if p.is_absolute() { p } else { root.join(p) };
|
||||||
next_path
|
fs::canonicalize(abs).ok()
|
||||||
} else {
|
|
||||||
root.join(next_path)
|
|
||||||
};
|
|
||||||
fs::canonicalize(absolute_path).ok()
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLI: `verify_uc <root_dir>` - find all `.uc` files in the provided directory
|
/// Wait for Enter if running in a TTY, shown before printing errors.
|
||||||
/// (except those listed in `ignore.txt` in the root) and test them all.
|
fn wait_before_errors(msg: &str) {
|
||||||
///
|
let _ = io::stdout().flush();
|
||||||
/// Reported execution time is the tokenization time, without considering time
|
if is_terminal::is_terminal(io::stdin()) {
|
||||||
/// it takes to read files from disk.
|
eprint!("{msg}");
|
||||||
///
|
let _ = io::stderr().flush();
|
||||||
/// `ignore.txt` is for listing specific files, not directories.
|
let mut s = String::new();
|
||||||
fn main() {
|
let _ = io::stdin().read_line(&mut s);
|
||||||
let root_dir = std::env::args().nth(1).unwrap(); // it is fine to crash debug utility
|
}
|
||||||
let root = PathBuf::from(&root_dir);
|
}
|
||||||
|
|
||||||
|
/// CLI: `verify_uc <root_dir> [file_name]`
|
||||||
|
///
|
||||||
|
fn main() {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let root_dir = args.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("Usage: verify_uc <root_dir> [file_name]");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let target_raw = args.next(); // optional file name hint
|
||||||
|
let target_ci = target_raw.as_ref().map(|s| s.to_ascii_lowercase());
|
||||||
|
let single_mode = target_ci.is_some();
|
||||||
|
|
||||||
|
let root = PathBuf::from(&root_dir);
|
||||||
if !root.exists() {
|
if !root.exists() {
|
||||||
eprintln!("Root directory '{root_dir}' does not exist.");
|
eprintln!("Root directory '{root_dir}' does not exist.");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load files
|
let t0 = Instant::now();
|
||||||
let ignored_paths = load_ignore_set(&root);
|
mark("baseline", t0);
|
||||||
|
|
||||||
|
// Stage 0: discover + read, bounded by FILE_LIMIT or first match in single_mode
|
||||||
|
let ignored = load_ignore_set(&root);
|
||||||
let mut uc_files: Vec<(PathBuf, String)> = Vec::new();
|
let mut uc_files: Vec<(PathBuf, String)> = Vec::new();
|
||||||
|
let mut seen = 0usize;
|
||||||
|
let mut picked_any = false;
|
||||||
|
|
||||||
for entry in walkdir::WalkDir::new(&root)
|
for entry in walkdir::WalkDir::new(&root)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(Result::ok) // for debug tool this is ok
|
.filter_map(Result::ok)
|
||||||
.filter(|entry| {
|
.filter(|e| {
|
||||||
let path = entry.path();
|
let path = e.path();
|
||||||
// Skip anything explicitly ignored
|
if let Ok(abs) = fs::canonicalize(path)
|
||||||
if let Ok(absolute_path) = fs::canonicalize(path) {
|
&& ignored.contains(&abs)
|
||||||
if ignored_paths.contains(&absolute_path) {
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Must be *.uc
|
|
||||||
path.is_file()
|
path.is_file()
|
||||||
&& path
|
&& path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|extension| extension.to_str())
|
.and_then(|e| e.to_str())
|
||||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("uc"))
|
.is_some_and(|e| e.eq_ignore_ascii_case("uc"))
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
if !single_mode && seen >= FILE_LIMIT {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If in single-file mode, keep only the first whose file name matches.
|
||||||
|
if let Some(needle) = target_ci.as_deref() {
|
||||||
|
let fname = entry
|
||||||
|
.path()
|
||||||
|
.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let fname_lc = fname.to_ascii_lowercase();
|
||||||
|
if !(fname_lc == needle || fname_lc.contains(needle)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seen += 1;
|
||||||
|
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
match fs::read(path) {
|
match fs::read(path) {
|
||||||
Ok(raw_bytes) => {
|
Ok(raw) => {
|
||||||
// Auto-detect encoding for old Unreal script sources
|
let (label, _, _) = chardet::detect(&raw);
|
||||||
let (encoding_label, _, _) = chardet::detect(&raw_bytes);
|
let enc = Encoding::for_label(label.as_bytes()).unwrap_or(encoding_rs::UTF_8);
|
||||||
let encoding = encoding_rs::Encoding::for_label(encoding_label.as_bytes())
|
let (txt, _, _) = enc.decode(&raw);
|
||||||
.unwrap_or(encoding_rs::UTF_8);
|
uc_files.push((path.to_path_buf(), txt.into_owned()));
|
||||||
let (decoded_text, _, _) = encoding.decode(&raw_bytes);
|
picked_any = true;
|
||||||
uc_files.push((path.to_path_buf(), decoded_text.into_owned()));
|
if single_mode {
|
||||||
|
// Only the first match.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
}
|
||||||
eprintln!("Failed to read `{}`: {error}", path.display());
|
Err(e) => {
|
||||||
|
wait_before_errors("Read error detected. Press Enter to print details...");
|
||||||
|
eprintln!("Failed to read `{}`: {e}", path.display());
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Loaded {} .uc files into memory.", uc_files.len());
|
|
||||||
|
|
||||||
// Tokenize and measure performance
|
if single_mode && !picked_any {
|
||||||
let start_time = std::time::Instant::now();
|
let needle = target_raw.as_deref().unwrap();
|
||||||
let tokenized_files: Vec<(PathBuf, TokenizedFile)> = uc_files
|
eprintln!(
|
||||||
.iter()
|
"No .uc file matching '{needle}' found under '{}'.",
|
||||||
.map(|(path, source_code)| {
|
root.display()
|
||||||
let tokenized_file = TokenizedFile::from_str(source_code);
|
);
|
||||||
if tokenized_file.has_errors() {
|
std::process::exit(1);
|
||||||
println!("TK: {}", path.display());
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Loaded {} .uc files into memory (cap={}, reached={}).",
|
||||||
|
uc_files.len(),
|
||||||
|
FILE_LIMIT,
|
||||||
|
if !single_mode && uc_files.len() >= FILE_LIMIT {
|
||||||
|
"yes"
|
||||||
|
} else {
|
||||||
|
"no"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
mark("after_read", t0);
|
||||||
|
|
||||||
|
// Stage 1: tokenize all
|
||||||
|
let t_tok = Instant::now();
|
||||||
|
let mut tokenized: Vec<(PathBuf, TokenizedFile)> = Vec::with_capacity(uc_files.len());
|
||||||
|
let mut tk_error_idx: Option<usize> = None;
|
||||||
|
|
||||||
|
for (i, (path, source)) in uc_files.iter().enumerate() {
|
||||||
|
let tf = TokenizedFile::tokenize(source);
|
||||||
|
if tk_error_idx.is_none() && tf.has_errors() {
|
||||||
|
tk_error_idx = Some(i);
|
||||||
|
}
|
||||||
|
tokenized.push((path.clone(), tf));
|
||||||
}
|
}
|
||||||
(path.clone(), tokenized_file)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let elapsed_time = start_time.elapsed();
|
|
||||||
println!(
|
println!(
|
||||||
"Tokenized {} files in {:.2?}",
|
"Tokenized {} files in {:.2?}",
|
||||||
tokenized_files.len(),
|
tokenized.len(),
|
||||||
elapsed_time
|
t_tok.elapsed()
|
||||||
);
|
);
|
||||||
|
mark("after_tokenize", t0);
|
||||||
|
|
||||||
|
// If tokenization error: wait, dump tokens for the first failing file, then exit.
|
||||||
|
// If tokenization error: wait, print lexer diagnostics for the first failing file, then exit.
|
||||||
|
if let Some(idx) = tk_error_idx {
|
||||||
|
let (bad_path, bad_tf) = &tokenized[idx];
|
||||||
|
wait_before_errors("Tokenization issues detected. Press Enter to print diagnostics...");
|
||||||
|
|
||||||
|
let fname = bad_path.display().to_string();
|
||||||
|
|
||||||
|
eprintln!("--- Tokenization issues in first failing file ---");
|
||||||
|
eprintln!("File: {fname}");
|
||||||
|
|
||||||
|
let diags = bad_tf.diagnostics();
|
||||||
|
|
||||||
|
if diags.is_empty() {
|
||||||
|
eprintln!("(no diagnostics captured)");
|
||||||
|
} else {
|
||||||
|
render_diagnostics_window(diags, bad_tf, &fname);
|
||||||
|
}
|
||||||
|
|
||||||
// Round-trip check
|
|
||||||
for ((path, original), (_, tokenized_file)) in uc_files.iter().zip(tokenized_files.iter()) {
|
|
||||||
let reconstructed = tokenized_file.reconstruct_source();
|
|
||||||
if original != &reconstructed {
|
|
||||||
eprintln!("Reconstruction mismatch in `{}`!", path.display());
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stage 2: parse all with ONE arena kept alive
|
||||||
|
let arena = rottlib::arena::Arena::new();
|
||||||
|
let t_parse = Instant::now();
|
||||||
|
|
||||||
|
// First failing parse: (tokenized_index, diagnostics, fatal)
|
||||||
|
let mut first_fail: Option<(usize, Vec<Diag>, Option<String>)> = None;
|
||||||
|
|
||||||
|
for (i, (path, tk)) in tokenized.iter().enumerate() {
|
||||||
|
// --- progress line BEFORE parsing this file ---
|
||||||
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
eprint!(
|
||||||
|
"Parsing [{}/{}] {} | rss={} MB\r\n",
|
||||||
|
i + 1,
|
||||||
|
tokenized.len(),
|
||||||
|
path.display(),
|
||||||
|
rss_mb()
|
||||||
|
);
|
||||||
|
let _ = io::stderr().flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("All .uc files matched successfully.");
|
let mut parser = Parser::new(tk, &arena);
|
||||||
|
|
||||||
|
match parser.parse_source_file() {
|
||||||
|
Ok(_) => {
|
||||||
|
if !parser.diagnostics.is_empty() && first_fail.is_none() {
|
||||||
|
first_fail = Some((i, parser.diagnostics.clone(), None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if first_fail.is_none() {
|
||||||
|
first_fail = Some((i, parser.diagnostics.clone(), Some(format!("{e:?}"))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Parsed {} files in {:.2?}",
|
||||||
|
tokenized.len(),
|
||||||
|
t_parse.elapsed()
|
||||||
|
);
|
||||||
|
mark("after_parse", t0);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
println!("--- Summary ---");
|
||||||
|
println!("Files processed: {}", tokenized.len());
|
||||||
|
println!("File cap: {FILE_LIMIT}");
|
||||||
|
|
||||||
|
if let Some((idx, diags, fatal)) = first_fail {
|
||||||
|
wait_before_errors("Parse issues detected. Press Enter to print diagnostics...");
|
||||||
|
let (path, tf) = &tokenized[idx];
|
||||||
|
eprintln!("--- Parse issues in first failing file ---");
|
||||||
|
eprintln!("File: {}", path.display());
|
||||||
|
if let Some(f) = &fatal {
|
||||||
|
eprintln!("Fatal parse error: {f}");
|
||||||
|
}
|
||||||
|
if diags.is_empty() && fatal.is_none() {
|
||||||
|
eprintln!("(no diagnostics captured)");
|
||||||
|
} else {
|
||||||
|
let fname = path.display().to_string();
|
||||||
|
render_diagnostics_window(&diags, tf, &fname);
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("All files parsed without diagnostics.");
|
||||||
}
|
}
|
||||||
|
|||||||
303
dev_tests/src/verify_expr.rs
Normal file
303
dev_tests/src/verify_expr.rs
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
#![allow(
|
||||||
|
clippy::all,
|
||||||
|
clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo,
|
||||||
|
clippy::restriction
|
||||||
|
)]
|
||||||
|
|
||||||
|
use rottlib::arena::Arena;
|
||||||
|
use rottlib::diagnostics::Diagnostic;
|
||||||
|
use rottlib::lexer::TokenizedFile;
|
||||||
|
use rottlib::parser::Parser;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum ParseMode {
|
||||||
|
Expression,
|
||||||
|
SourceFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARSE_MODE: ParseMode = ParseMode::SourceFile;
|
||||||
|
|
||||||
|
const TEST_CASES: &[(&str, &str)] = &[
|
||||||
|
// P0057 - VarEditorSpecifierExpected
|
||||||
|
(
|
||||||
|
"files/P0057_01.uc",
|
||||||
|
concat!("class Test extends Actor;\n", "var(Category,) int A;\n"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0057_02.uc",
|
||||||
|
concat!("class Test extends Actor;\n", "var(,) int A;\n"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0057_03.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n ,\n ,\n Category\n) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0057_04.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n Category,\n\n\n) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0058 - VarEditorSpecifierListMissingSeparator
|
||||||
|
(
|
||||||
|
"files/P0058_01.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(Category DisplayName) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0058_02.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\"Display\" \"Tooltip\") int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0058_03.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n Category\n DisplayName\n) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0058_04.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n Category\n\n \"Display Name\",\n Advanced\n) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0059 - VarEditorSpecifierListExpectedCommaOrClosingParenthesis
|
||||||
|
(
|
||||||
|
"files/P0059_01.uc",
|
||||||
|
concat!("class Test extends Actor;\n", "var(Category :) int A;\n"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0059_02.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\"Display\" *) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0059_03.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n Category\n [\n) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0059_04.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n \"Display Name\"\n\n ]\n) int A;\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0060 - VarEditorSpecifierListMissingClosingParenthesis
|
||||||
|
(
|
||||||
|
"files/P0060_01.uc",
|
||||||
|
concat!("class Test extends Actor;\n", "var(Category\n"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0060_02.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n Category,\n \"Display Name\"\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0060_03.uc",
|
||||||
|
concat!("class Test extends Actor;\n", "var(\n\n\n"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0060_04.uc",
|
||||||
|
concat!(
|
||||||
|
"class Test extends Actor;\n",
|
||||||
|
"var(\n Category,\n\n \"Display Name\"\n\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Lexer-focused fixtures.
|
||||||
|
///
|
||||||
|
/// Keep these small: the goal is to inspect lexer diagnostics and delimiter
|
||||||
|
/// recovery behavior, not full parser behavior.
|
||||||
|
/*const TEST_CASES: &[(&str, &str)] = &[
|
||||||
|
// P0061 - TypeSpecifierExpected
|
||||||
|
("files/P0061_01.uc", "{\n local ;\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0061_02.uc",
|
||||||
|
"{\n local\n ;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0061_03.uc",
|
||||||
|
"{\n local\n [\n ] Values;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0061_04.uc",
|
||||||
|
"{\n local\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0062 - NamedTypeInvalidName
|
||||||
|
("files/P0062_01.uc", "{\n local Foo. ;\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0062_02.uc",
|
||||||
|
"{\n local\n Foo.\n ;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0062_03.uc",
|
||||||
|
"{\n local Foo..Bar Value;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0062_04.uc",
|
||||||
|
"{\n local\n SomePackage\n .\n",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0063 - ArrayTypeMissingOpeningAngleBracket
|
||||||
|
("files/P0063_01.uc", "{\n local array Values;\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0063_02.uc",
|
||||||
|
"{\n local\n array\n Values;\n}\n",
|
||||||
|
),
|
||||||
|
("files/P0063_03.uc", "{\n local array[] Values;\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0063_04.uc",
|
||||||
|
"{\n local\n array",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0064 - ArrayTypeMissingClosingAngleBracket
|
||||||
|
("files/P0064_01.uc", "{\n local array<int Values;\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0064_02.uc",
|
||||||
|
"{\n local\n array<\n int\n Values;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0064_03.uc",
|
||||||
|
"{\n local array<class<Actor> ActorClasses;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0064_04.uc",
|
||||||
|
"{\n local\n array<\n string",
|
||||||
|
),
|
||||||
|
|
||||||
|
// P0065 - ArrayTypeMissingElementType
|
||||||
|
("files/P0065_01.uc", "{\n local array<> Values;\n}\n"),
|
||||||
|
(
|
||||||
|
"files/P0065_02.uc",
|
||||||
|
"{\n local\n array<>\n Values;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0065_03.uc",
|
||||||
|
"{\n local array<\n > Values;\n}\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"files/P0065_04.uc",
|
||||||
|
"{\n local array<array<> > NestedValues;\n}\n",
|
||||||
|
),
|
||||||
|
];*/
|
||||||
|
|
||||||
|
/// If true, also run the parser after tokenization.
|
||||||
|
///
|
||||||
|
/// For lexer-focused fixtures this is usually noisy, so keep it off unless you
|
||||||
|
/// want to inspect how parser recovery behaves after lexer diagnostics.
|
||||||
|
const RUN_PARSER: bool = true;
|
||||||
|
|
||||||
|
/// If true, print the parsed expression using Debug formatting.
|
||||||
|
const PRINT_PARSED_EXPR: bool = false;
|
||||||
|
|
||||||
|
const PRINT_LEXER_DIAGNOSTICS: bool = false;
|
||||||
|
const ALWAYS_PRINT_DIAGNOSTICS: bool = true;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let arena = Arena::new();
|
||||||
|
|
||||||
|
let mut had_any_problem = false;
|
||||||
|
|
||||||
|
for (idx, (label, source)) in TEST_CASES.iter().enumerate() {
|
||||||
|
println!("============================================================");
|
||||||
|
println!("Case {}: {}", idx + 1, label);
|
||||||
|
println!("------------------------------------------------------------");
|
||||||
|
|
||||||
|
let tf = TokenizedFile::tokenize(source);
|
||||||
|
|
||||||
|
let lexer_diagnostics = tf.diagnostics();
|
||||||
|
|
||||||
|
if lexer_diagnostics.is_empty() {
|
||||||
|
println!("Lexer diagnostics: none");
|
||||||
|
} else {
|
||||||
|
had_any_problem = true;
|
||||||
|
|
||||||
|
if PRINT_LEXER_DIAGNOSTICS {
|
||||||
|
println!("Lexer diagnostics:");
|
||||||
|
for diag in lexer_diagnostics {
|
||||||
|
render_diagnostic(diag, &tf, Some(label), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if RUN_PARSER {
|
||||||
|
let mut parser = Parser::new(&tf, &arena);
|
||||||
|
|
||||||
|
match PARSE_MODE {
|
||||||
|
ParseMode::Expression => {
|
||||||
|
let expr = parser.parse_expression();
|
||||||
|
|
||||||
|
if PRINT_PARSED_EXPR {
|
||||||
|
println!("Parsed expression:");
|
||||||
|
println!("{expr:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParseMode::SourceFile => {
|
||||||
|
if let Err(error) = parser.parse_source_file() {
|
||||||
|
parser.report_error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if PRINT_PARSED_EXPR {
|
||||||
|
println!("Parsed source file: <not printed>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parser.diagnostics.is_empty() {
|
||||||
|
println!("Parser diagnostics: none");
|
||||||
|
} else {
|
||||||
|
had_any_problem = true;
|
||||||
|
|
||||||
|
if ALWAYS_PRINT_DIAGNOSTICS {
|
||||||
|
println!("Parser diagnostics:");
|
||||||
|
for diag in &parser.diagnostics {
|
||||||
|
render_diagnostic(diag, &tf, Some(label), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("============================================================");
|
||||||
|
|
||||||
|
if had_any_problem {
|
||||||
|
println!("Done. At least one case had lexer or parser diagnostics.");
|
||||||
|
std::process::exit(1);
|
||||||
|
} else {
|
||||||
|
println!("Done. All cases completed without diagnostics.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_diagnostic(
|
||||||
|
diag: &Diagnostic,
|
||||||
|
file: &TokenizedFile<'_>,
|
||||||
|
file_name: Option<&str>,
|
||||||
|
_colors: bool,
|
||||||
|
) {
|
||||||
|
diag.render(file, file_name.unwrap_or("<default>"));
|
||||||
|
}
|
||||||
413
kf_sources/Acedia/Classes/BaseGameMode.uc
Normal file
413
kf_sources/Acedia/Classes/BaseGameMode.uc
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
/**
|
||||||
|
* Base class for a game mode config, contains all the information Acedia's
|
||||||
|
* game modes must have, including settings
|
||||||
|
* (`includeFeature`, `includeFeatureAs` and `excludeFeature`)
|
||||||
|
* for picking used `Feature`s.
|
||||||
|
*
|
||||||
|
* Contains three types of methods:
|
||||||
|
* 1. Getters for its values;
|
||||||
|
* 2. `UpdateFeatureArray()` method for updating list of `Feature`s to
|
||||||
|
* be used based on game info's settings;
|
||||||
|
* 3. `Report...()` methods that perform various validation checks
|
||||||
|
* (and log them) on config data.
|
||||||
|
* Copyright 2021-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class BaseGameMode extends AcediaConfig
|
||||||
|
dependson(Packages)
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
// Name of the game mode players will see in voting (formatted string)
|
||||||
|
var protected config string title;
|
||||||
|
// Preferable game length (plain string)
|
||||||
|
var protected config string length;
|
||||||
|
// Preferable difficulty level (plain string)
|
||||||
|
var protected config string difficulty;
|
||||||
|
// `Mutator`s to add with this game mode
|
||||||
|
var protected config array<string> includeMutator;
|
||||||
|
// `Feature`s to include (with "default" config)
|
||||||
|
var protected config array<string> includeFeature;
|
||||||
|
// `Feature`s to exclude from game mode, regardless of other settings
|
||||||
|
// (this one has highest priority)
|
||||||
|
var protected config array<string> excludeFeature;
|
||||||
|
|
||||||
|
struct FeatureConfigPair
|
||||||
|
{
|
||||||
|
var public string feature;
|
||||||
|
var public string config;
|
||||||
|
};
|
||||||
|
// `Feature`s to include (with specified config).
|
||||||
|
// Higher priority than `includeFeature`, but lower than `excludeFeature`.
|
||||||
|
var protected config array<FeatureConfigPair> includeFeatureAs;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition warnBadMutatorName, warnBadFeatureName;
|
||||||
|
|
||||||
|
protected function HashTable ToData()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local HashTable result;
|
||||||
|
local HashTable nextPair;
|
||||||
|
local ArrayList nextArray;
|
||||||
|
|
||||||
|
result = _.collections.EmptyHashTable();
|
||||||
|
result.SetFormattedString(P("title"), title);
|
||||||
|
result.SetString(P("length"), length);
|
||||||
|
result.SetString(P("difficulty"), difficulty);
|
||||||
|
nextArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < includeFeature.length; i += 1) {
|
||||||
|
nextArray.AddString(includeFeature[i]);
|
||||||
|
}
|
||||||
|
result.SetItem(P("includeFeature"), nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
nextArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < excludeFeature.length; i += 1) {
|
||||||
|
nextArray.AddItem(_.text.FromString(excludeFeature[i]));
|
||||||
|
}
|
||||||
|
result.SetItem(P("excludeFeature"), nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
nextArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < includeMutator.length; i += 1) {
|
||||||
|
nextArray.AddItem(_.text.FromString(includeFeature[i]));
|
||||||
|
}
|
||||||
|
result.SetItem(P("includeMutator"), nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
nextArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < includeFeatureAs.length; i += 1)
|
||||||
|
{
|
||||||
|
nextPair = _.collections.EmptyHashTable();
|
||||||
|
nextPair.SetString(P("feature"), includeFeatureAs[i].feature);
|
||||||
|
nextPair.SetString(P("config"), includeFeatureAs[i].config);
|
||||||
|
nextArray.AddItem(nextPair);
|
||||||
|
_.memory.Free(nextPair);
|
||||||
|
}
|
||||||
|
result.SetItem(P("includeFeatureAs"), nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function FromData(HashTable source)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local ArrayList nextArray;
|
||||||
|
local HashTable nextPair;
|
||||||
|
|
||||||
|
if (source == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
title = source.GetFormattedString(P("title"));
|
||||||
|
length = source.GetString(P("length"));
|
||||||
|
difficulty = source.GetString(P("difficulty"));
|
||||||
|
nextArray = source.GetArrayList(P("includeFeature"));
|
||||||
|
includeFeature = DynamicIntoStringArray(nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
nextArray = source.GetArrayList(P("excludeFeature"));
|
||||||
|
excludeFeature = DynamicIntoStringArray(nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
nextArray = source.GetArrayList(P("includeMutator"));
|
||||||
|
includeMutator = DynamicIntoStringArray(nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
nextArray = source.GetArrayList(P("includeFeatureAs"));
|
||||||
|
if (nextArray == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
includeFeatureAs.length = 0;
|
||||||
|
for (i = 0; i < nextArray.GetLength(); i += 1)
|
||||||
|
{
|
||||||
|
nextPair = nextArray.GetHashTable(i);
|
||||||
|
includeFeatureAs[i] = HashTableIntoPair(nextPair);
|
||||||
|
_.memory.Free(nextPair);
|
||||||
|
}
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function FeatureConfigPair HashTableIntoPair(HashTable source)
|
||||||
|
{
|
||||||
|
local Text nextText;
|
||||||
|
local FeatureConfigPair result;
|
||||||
|
|
||||||
|
if (source == none) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
nextText = source.GetText(P("feature"));
|
||||||
|
if (nextText != none) {
|
||||||
|
result.feature = nextText.ToString();
|
||||||
|
}
|
||||||
|
nextText = source.GetText(P("config"));
|
||||||
|
if (nextText != none) {
|
||||||
|
result.config = nextText.ToString();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function array<string> DynamicIntoStringArray(ArrayList source)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text nextText;
|
||||||
|
local array<string> result;
|
||||||
|
|
||||||
|
if (source == none) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (i = 0; i < source.GetLength(); i += 1)
|
||||||
|
{
|
||||||
|
nextText = source.GetText(i);
|
||||||
|
if (nextText != none) {
|
||||||
|
includeFeature[i] = nextText.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function array<Text> StringToTextArray(array<string> input)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<Text> result;
|
||||||
|
|
||||||
|
for (i = 0; i < input.length; i += 1) {
|
||||||
|
result[i] = _.text.FromString(input[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Name of the `GameInfo` class to be used with the caller game mode.
|
||||||
|
*/
|
||||||
|
public function Text GetGameTypeClass()
|
||||||
|
{
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Human-readable name of the caller game mode.
|
||||||
|
* Players will see it as the name of the mode in the voting options.
|
||||||
|
*/
|
||||||
|
public function Text GetTitle()
|
||||||
|
{
|
||||||
|
return _.text.FromFormattedString(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Specified game length for the game mode.
|
||||||
|
* Interpretation of this value can depend on each particular game mode.
|
||||||
|
*/
|
||||||
|
public function Text GetLength()
|
||||||
|
{
|
||||||
|
return _.text.FromString(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Specified difficulty for the game mode.
|
||||||
|
* Interpretation of this value can depend on each particular game mode.
|
||||||
|
*/
|
||||||
|
public function Text GetDifficulty()
|
||||||
|
{
|
||||||
|
return _.text.FromString(difficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks `Feature`-related settings (`includeFeature`, `includeFeatureAs` and
|
||||||
|
* `excludeFeature`) for correctness and reports any issues.
|
||||||
|
* Currently correctness check simply ensures that all listed `Feature`s
|
||||||
|
* actually exist.
|
||||||
|
*/
|
||||||
|
public function ReportIncorrectSettings(
|
||||||
|
array<Packages.FeatureConfigPair> featuresToEnable)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<string> featureNames, featuresToReplace;
|
||||||
|
|
||||||
|
for (i = 0; i < featuresToEnable.length; i += 1) {
|
||||||
|
featureNames[i] = string(featuresToEnable[i].featureClass);
|
||||||
|
}
|
||||||
|
ValidateFeatureArray(includeFeature, featureNames, "includeFeatures");
|
||||||
|
ValidateFeatureArray(excludeFeature, featureNames, "excludeFeatures");
|
||||||
|
for (i = 0; i < includeFeatureAs.length; i += 1) {
|
||||||
|
featuresToReplace[i] = includeFeatureAs[i].feature;
|
||||||
|
}
|
||||||
|
ValidateFeatureArray(featuresToReplace, featureNames, "includeFeatureAs");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks `Mutator`-related settings (`includeMutator`) for correctness and
|
||||||
|
* reports any issues.
|
||||||
|
* Currently correctness check performs a simple validity check for mutator,
|
||||||
|
* to make sure it would not define a new option in server's URL.
|
||||||
|
*
|
||||||
|
* See `ValidateServerURLName()` for more information.
|
||||||
|
*/
|
||||||
|
public function ReportBadMutatorNames()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < includeMutator.length; i += 1)
|
||||||
|
{
|
||||||
|
if (!ValidateServerURLName(includeMutator[i]))
|
||||||
|
{
|
||||||
|
_.logger.Auto(warnBadMutatorName)
|
||||||
|
.Arg(_.text.FromString(includeMutator[i]))
|
||||||
|
.Arg(_.text.FromString(string(name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure that a word to be used in server URL as a part of an option
|
||||||
|
* does not contain "," / "?" / "=" or whitespace.
|
||||||
|
* This is useful to make sure that user-specified mutator entries only add
|
||||||
|
* one mutator or option's key / values will not specify only one pair,
|
||||||
|
* avoiding "?opt1=value1?opt2=value2" entries.
|
||||||
|
*/
|
||||||
|
protected function bool ValidateServerURLName(string entry)
|
||||||
|
{
|
||||||
|
if (InStr(entry, "=") >= 0) return false;
|
||||||
|
if (InStr(entry, "?") >= 0) return false;
|
||||||
|
if (InStr(entry, ",") >= 0) return false;
|
||||||
|
if (InStr(entry, " ") >= 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is every element `subset` present inside `whole`?
|
||||||
|
private function ValidateFeatureArray(
|
||||||
|
array<string> subset,
|
||||||
|
array<string> whole,
|
||||||
|
string arrayName)
|
||||||
|
{
|
||||||
|
local int i, j;
|
||||||
|
local bool foundItem;
|
||||||
|
|
||||||
|
for (i = 0; i < subset.length; i += 1)
|
||||||
|
{
|
||||||
|
foundItem = false;
|
||||||
|
for (j = 0; j < whole.length; j += 1)
|
||||||
|
{
|
||||||
|
if (subset[i] ~= whole[j])
|
||||||
|
{
|
||||||
|
foundItem = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundItem)
|
||||||
|
{
|
||||||
|
_.logger.Auto(warnBadFeatureName)
|
||||||
|
.Arg(_.text.FromString(includeMutator[i]))
|
||||||
|
.Arg(_.text.FromString(string(name)))
|
||||||
|
.Arg(_.text.FromString(arrayName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates passed `Feature` settings according to this game mode's settings.
|
||||||
|
*
|
||||||
|
* @param featuresToEnable Settings to update.
|
||||||
|
* `FeatureConfigPair` is a pair of `Feature` (`featureClass`) and its
|
||||||
|
* config's name (`configName`).
|
||||||
|
* If `configName` is set to `none`, then corresponding `Feature`
|
||||||
|
* should not be enabled.
|
||||||
|
* Otherwise it should be enabled with a specified config.
|
||||||
|
*/
|
||||||
|
public function UpdateFeatureArray(
|
||||||
|
out array<Packages.FeatureConfigPair> featuresToEnable)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text newConfigName;
|
||||||
|
local string nextFeatureClassName;
|
||||||
|
|
||||||
|
for (i = 0; i < featuresToEnable.length; i += 1)
|
||||||
|
{
|
||||||
|
nextFeatureClassName = string(featuresToEnable[i].featureClass);
|
||||||
|
// `excludeFeature`
|
||||||
|
if (FeatureExcluded(nextFeatureClassName))
|
||||||
|
{
|
||||||
|
_.memory.Free(featuresToEnable[i].configName);
|
||||||
|
featuresToEnable[i].configName = none;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// `includeFeatureAs`
|
||||||
|
newConfigName = TryReplacingFeatureConfig(nextFeatureClassName);
|
||||||
|
if (newConfigName != none)
|
||||||
|
{
|
||||||
|
_.memory.Free(featuresToEnable[i].configName);
|
||||||
|
featuresToEnable[i].configName = newConfigName;
|
||||||
|
}
|
||||||
|
// `includeFeature`
|
||||||
|
if ( featuresToEnable[i].configName == none
|
||||||
|
&& FeatureInIncludedArray(nextFeatureClassName))
|
||||||
|
{
|
||||||
|
featuresToEnable[i].configName = P("default").Copy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bool FeatureExcluded(string featureClassName)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < excludeFeature.length; i += 1)
|
||||||
|
{
|
||||||
|
if (excludeFeature[i] ~= featureClassName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function Text TryReplacingFeatureConfig(string featureClassName)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < includeFeatureAs.length; i += 1)
|
||||||
|
{
|
||||||
|
if (includeFeatureAs[i].feature ~= featureClassName) {
|
||||||
|
return _.text.FromString(includeFeatureAs[i].config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bool FeatureInIncludedArray(string featureClassName)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < includeFeature.length; i += 1)
|
||||||
|
{
|
||||||
|
if (includeFeature[i] ~= featureClassName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function array<Text> GetIncludedMutators()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<string> validatedMutators;
|
||||||
|
|
||||||
|
for (i = 0; i < includeMutator.length; i += 1)
|
||||||
|
{
|
||||||
|
if (ValidateServerURLName(includeMutator[i])) {
|
||||||
|
validatedMutators[validatedMutators.length] = includeMutator[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StringToTextArray(validatedMutators);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configName = "AcediaGameModes"
|
||||||
|
warnBadMutatorName = (l=LOG_Warning,m="Mutator \"%1\" specified for game mode \"%2\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.")
|
||||||
|
warnBadFeatureName = (l=LOG_Warning,m="Feature \"%1\" specified for game mode \"%2\" in array `%3` does not exist in enabled packages and will be ignored. This is a configuration error, you should fix it.")
|
||||||
|
}
|
||||||
280
kf_sources/Acedia/Classes/GameMode.uc
Normal file
280
kf_sources/Acedia/Classes/GameMode.uc
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* The only implementation for `BaseGameMode` suitable for standard
|
||||||
|
* killing floor game types.
|
||||||
|
* Copyright 2021-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class GameMode extends BaseGameMode
|
||||||
|
perobjectconfig
|
||||||
|
config(AcediaGameModes);
|
||||||
|
|
||||||
|
struct GameOption
|
||||||
|
{
|
||||||
|
var public string key;
|
||||||
|
var public string value;
|
||||||
|
};
|
||||||
|
// Allow to specify additional server options for this game mode
|
||||||
|
var protected config array<GameOption> option;
|
||||||
|
// Specify `GameInfo`'s class to use, default is "KFMod.KFGameType"
|
||||||
|
// (plain string)
|
||||||
|
var protected config string gameTypeClass;
|
||||||
|
// Short version of the name of the game mode players will see in
|
||||||
|
// voting handler messages sometimes (plain string)
|
||||||
|
var protected config string acronym;
|
||||||
|
// Map prefix - only maps that start with specified prefix will be voteable for
|
||||||
|
// this game mode (plain string)
|
||||||
|
var protected config string mapPrefix;
|
||||||
|
|
||||||
|
// Aliases are an unnecessary overkill for difficulty names, so just define
|
||||||
|
// them in special `string` arrays.
|
||||||
|
// We accept not just these exact words, but any of their prefixes.
|
||||||
|
var private const array<string> beginnerSynonyms;
|
||||||
|
var private const array<string> normalSynonyms;
|
||||||
|
var private const array<string> hardSynonyms;
|
||||||
|
var private const array<string> suicidalSynonyms;
|
||||||
|
var private const array<string> hoeSynonyms;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition warnBadOption, warnDifficultyOption;
|
||||||
|
|
||||||
|
protected function DefaultIt()
|
||||||
|
{
|
||||||
|
title = "Acedia game mode";
|
||||||
|
length = "long";
|
||||||
|
difficulty = "Hell On Earth";
|
||||||
|
gameTypeClass = "KFMod.KFGameType";
|
||||||
|
acronym = "";
|
||||||
|
mapPrefix = "KF";
|
||||||
|
includeFeature.length = 0;
|
||||||
|
excludeFeature.length = 0;
|
||||||
|
includeMutator.length = 0;
|
||||||
|
option.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function HashTable ToData()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local ArrayList nextArray;
|
||||||
|
local HashTable result, nextPair;
|
||||||
|
|
||||||
|
result = super.ToData();
|
||||||
|
if (result == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
result.SetString(P("gameTypeClass"), gameTypeClass);
|
||||||
|
result.SetString(P("acronym"), acronym);
|
||||||
|
result.SetString(P("mapPrefix"), mapPrefix);
|
||||||
|
nextArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < option.length; i += 1)
|
||||||
|
{
|
||||||
|
nextPair = _.collections.EmptyHashTable();
|
||||||
|
nextPair.SetString(P("key"), option[i].key);
|
||||||
|
nextPair.SetString(P("value"), option[i].value);
|
||||||
|
nextArray.AddItem(nextPair);
|
||||||
|
_.memory.Free(nextPair);
|
||||||
|
}
|
||||||
|
result.SetItem(P("option"), nextArray);
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function FromData(HashTable source)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local GameOption nextGameOption;
|
||||||
|
local ArrayList nextArray;
|
||||||
|
local HashTable nextPair;
|
||||||
|
|
||||||
|
super.FromData(source);
|
||||||
|
if (source == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gameTypeClass = source.GetString(P("gameTypeClass"));
|
||||||
|
acronym = source.GetString(P("acronym"));
|
||||||
|
mapPrefix = source.GetString(P("mapPrefix"));
|
||||||
|
nextArray = source.GetArrayList(P("option"));
|
||||||
|
if (nextArray == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
option.length = 0;
|
||||||
|
for (i = 0; i < nextArray.GetLength(); i += 1)
|
||||||
|
{
|
||||||
|
nextPair = HashTable(nextArray.GetItem(i));
|
||||||
|
if (nextPair == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextGameOption.key = nextPair.GetString(P("key"));
|
||||||
|
nextGameOption.value = nextPair.GetString(P("value"));
|
||||||
|
option[option.length] = nextGameOption;
|
||||||
|
_.memory.Free(nextPair);
|
||||||
|
}
|
||||||
|
_.memory.Free(nextArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Text GetGameTypeClass()
|
||||||
|
{
|
||||||
|
if (gameTypeClass == "") {
|
||||||
|
return P("KFMod.KFGameType").Copy();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _.text.FromString(gameTypeClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Text GetAcronym()
|
||||||
|
{
|
||||||
|
if (acronym == "") {
|
||||||
|
return _.text.FromString(string(name));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _.text.FromFormattedString(acronym);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Text GetMapPrefix()
|
||||||
|
{
|
||||||
|
if (acronym == "") {
|
||||||
|
return _.text.FromString("KF-");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _.text.FromString(mapPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks option-related settings (`option`) for correctness and reports
|
||||||
|
* any issues.
|
||||||
|
* Currently correctness check performs a simple validity check for mutator,
|
||||||
|
* to make sure it would not define a new option in server's URL.
|
||||||
|
*
|
||||||
|
* See `ValidateServerURLName()` in `BaseGameMode` for more information.
|
||||||
|
*/
|
||||||
|
public function ReportBadOptions()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < option.length; i += 1)
|
||||||
|
{
|
||||||
|
if ( !ValidateServerURLName(option[i].key)
|
||||||
|
|| !ValidateServerURLName(option[i].value))
|
||||||
|
{
|
||||||
|
_.logger.Auto(warnBadOption)
|
||||||
|
.Arg(_.text.FromString(option[i].key))
|
||||||
|
.Arg(_.text.FromString(option[i].value))
|
||||||
|
.Arg(_.text.FromString(string(name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Server options as key-value pairs in an `HashTable`.
|
||||||
|
*/
|
||||||
|
public function HashTable GetOptions()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local HashTable result;
|
||||||
|
local Text nextKey, nextValue;
|
||||||
|
|
||||||
|
result = _.collections.EmptyHashTable();
|
||||||
|
for (i = 0; i < option.length; i += 1)
|
||||||
|
{
|
||||||
|
if (!ValidateServerURLName(option[i].key)) continue;
|
||||||
|
if (!ValidateServerURLName(option[i].value)) continue;
|
||||||
|
if (option[i].key ~= "difficulty")
|
||||||
|
{
|
||||||
|
_.logger.Auto(warnDifficultyOption);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextKey = _.text.FromString(option[i].key);
|
||||||
|
nextValue = _.text.FromString(option[i].value);
|
||||||
|
result.SetItem(nextKey, nextValue);
|
||||||
|
nextKey.FreeSelf();
|
||||||
|
nextValue.FreeSelf();
|
||||||
|
}
|
||||||
|
// Add difficulty option
|
||||||
|
nextValue = _.text.FromInt(GetNumericDifficulty());
|
||||||
|
result.SetItem(P("difficulty"), nextValue);
|
||||||
|
nextValue.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert `GameMode`'s difficulty's textual representation into
|
||||||
|
// KF's numeric one.
|
||||||
|
private final function int GetNumericDifficulty()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local string lowerCaseDifficulty;
|
||||||
|
|
||||||
|
lowerCaseDifficulty = Locs(_.text.IntoString(GetDifficulty()));
|
||||||
|
for (i = 0; i < default.beginnerSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(lowerCaseDifficulty, default.beginnerSynonyms[i])) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < default.normalSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(lowerCaseDifficulty, default.normalSynonyms[i])) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < default.hardSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(lowerCaseDifficulty, default.hardSynonyms[i])) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < default.suicidalSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(lowerCaseDifficulty, default.suicidalSynonyms[i])) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < default.hoeSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(lowerCaseDifficulty, default.hoeSynonyms[i])) {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return int(lowerCaseDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final static function bool IsPrefixOf(string prefix, string value)
|
||||||
|
{
|
||||||
|
return (InStr(value, prefix) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configName = "AcediaGameModes"
|
||||||
|
beginnerSynonyms(0) = "easy"
|
||||||
|
beginnerSynonyms(1) = "beginer"
|
||||||
|
beginnerSynonyms(2) = "beginner"
|
||||||
|
beginnerSynonyms(3) = "begginer"
|
||||||
|
beginnerSynonyms(4) = "begginner"
|
||||||
|
normalSynonyms(0) = "regular"
|
||||||
|
normalSynonyms(1) = "default"
|
||||||
|
normalSynonyms(2) = "normal"
|
||||||
|
hardSynonyms(0) = "harder" // "hard" is prefix of this, so it will count
|
||||||
|
hardSynonyms(1) = "difficult"
|
||||||
|
suicidalSynonyms(0) = "suicidal"
|
||||||
|
hoeSynonyms(0) = "hellonearth"
|
||||||
|
hoeSynonyms(1) = "hellon earth"
|
||||||
|
hoeSynonyms(2) = "hell onearth"
|
||||||
|
hoeSynonyms(3) = "hoe"
|
||||||
|
warnBadOption = (l=LOG_Warning,m="Option with key \"%1\" and value \"%2\" specified for game mode \"%3\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.")
|
||||||
|
warnDifficultyOption = (l=LOG_Warning,m="Option with key \"Difficulty\" is specified. This key reserved and will be ignored. Difficulty value should be set through the game mode's \"Difficulty\" setting in \"AcediaGameModes.ini\" config. This is a configuration error, you should fix it.")
|
||||||
|
}
|
||||||
274
kf_sources/Acedia/Classes/Packages.uc
Normal file
274
kf_sources/Acedia/Classes/Packages.uc
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* Main and only Acedia mutator used for loading Acedia packages
|
||||||
|
* and providing access to mutator events' calls.
|
||||||
|
* Name is chosen to make config files more readable.
|
||||||
|
* Copyright 2020-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Packages extends Mutator
|
||||||
|
config(Acedia);
|
||||||
|
|
||||||
|
// Acedia's reference to a `Global` object.
|
||||||
|
var private Global _;
|
||||||
|
var private ServerGlobal _server;
|
||||||
|
var private ClientGlobal _client;
|
||||||
|
|
||||||
|
// Load Acedia on the client as well?
|
||||||
|
var private config bool clientside;
|
||||||
|
// Array of predefined services that must be started along with Acedia mutator.
|
||||||
|
var private config array<string> package;
|
||||||
|
// Set to `true` to activate Acedia's game modes system
|
||||||
|
var private config bool useGameModes;
|
||||||
|
// Responsible for setting up Acedia's game modes in current voting system
|
||||||
|
var VotingHandlerAdapter votingAdapter;
|
||||||
|
|
||||||
|
var Mutator_OnMutate_Signal onMutateSignal;
|
||||||
|
var Mutator_OnModifyLogin_Signal onModifyLoginSignal;
|
||||||
|
var Mutator_OnCheckReplacement_Signal onCheckReplacementSignal;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition infoFeatureEnabled;
|
||||||
|
var private LoggerAPI.Definition errNoServerLevelCore, errorCannotRunTests;
|
||||||
|
|
||||||
|
struct FeatureConfigPair
|
||||||
|
{
|
||||||
|
var public class<Feature> featureClass;
|
||||||
|
var public Text configName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// "Constructor"
|
||||||
|
simulated function PreBeginPlay()
|
||||||
|
{
|
||||||
|
if (level.netMode == NM_DedicatedServer) {
|
||||||
|
InitializeServer();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
InitializeClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private simulated function InitializeClient()
|
||||||
|
{
|
||||||
|
_ = class'Global'.static.GetInstance();
|
||||||
|
class'ClientLevelCore'.static.CreateLevelCore(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function InitializeServer()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local LevelCore serverCore;
|
||||||
|
local GameMode currentGameMode;
|
||||||
|
local array<FeatureConfigPair> availableFeatures;
|
||||||
|
|
||||||
|
if (clientside) {
|
||||||
|
AddToPackageMap("Acedia");
|
||||||
|
}
|
||||||
|
CheckForGarbage();
|
||||||
|
// Launch and setup core Acedia
|
||||||
|
_ = class'Global'.static.GetInstance();
|
||||||
|
_server = class'ServerGlobal'.static.GetInstance();
|
||||||
|
_client = class'ClientGlobal'.static.GetInstance();
|
||||||
|
serverCore = class'ServerLevelCore'.static.CreateLevelCore(self);
|
||||||
|
for (i = 0; i < package.length; i += 1) {
|
||||||
|
_.environment.RegisterPackage_S(package[i]);
|
||||||
|
}
|
||||||
|
if (serverCore != none) {
|
||||||
|
_server.ConnectServerLevelCore();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_.logger.Auto(errNoServerLevelCore);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (class'TestingService'.default.runTestsOnStartUp) {
|
||||||
|
RunStartUpTests();
|
||||||
|
}
|
||||||
|
SetupMutatorSignals();
|
||||||
|
// Determine required features and launch them
|
||||||
|
availableFeatures = GetAutoConfigurationInfo();
|
||||||
|
if (useGameModes)
|
||||||
|
{
|
||||||
|
votingAdapter = VotingHandlerAdapter(
|
||||||
|
_.memory.Allocate(class'VotingHandlerAdapter'));
|
||||||
|
currentGameMode = votingAdapter.SetupGameModeAfterTravel();
|
||||||
|
if (currentGameMode != none) {
|
||||||
|
currentGameMode.UpdateFeatureArray(availableFeatures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EnableFeatures(availableFeatures);
|
||||||
|
if (votingAdapter != none) {
|
||||||
|
votingAdapter.InjectIntoVotingHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Finalizer"
|
||||||
|
function ServerTraveling(string URL, bool bItems)
|
||||||
|
{
|
||||||
|
if (votingAdapter != none)
|
||||||
|
{
|
||||||
|
votingAdapter.PrepareForServerTravel();
|
||||||
|
votingAdapter.RestoreVotingHandlerConfigBackup();
|
||||||
|
_.memory.Free(votingAdapter);
|
||||||
|
votingAdapter = none;
|
||||||
|
}
|
||||||
|
_.environment.ShutDown();
|
||||||
|
if (nextMutator != none) {
|
||||||
|
nextMutator.ServerTraveling(URL, bItems);
|
||||||
|
}
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether Acedia has left garbage after the previous map.
|
||||||
|
// This can lead to serious problems, so such diagnostic check is warranted.
|
||||||
|
private function CheckForGarbage()
|
||||||
|
{
|
||||||
|
local int leftoverObjectAmount;
|
||||||
|
local int leftoverActorAmount;
|
||||||
|
local int leftoverDBRAmount;
|
||||||
|
local AcediaObject nextObject;
|
||||||
|
local AcediaActor nextActor;
|
||||||
|
local DBRecord nextRecord;
|
||||||
|
|
||||||
|
foreach AllObjects(class'AcediaObject', nextObject) {
|
||||||
|
leftoverObjectAmount += 1;
|
||||||
|
}
|
||||||
|
foreach AllActors(class'AcediaActor', nextActor) {
|
||||||
|
leftoverActorAmount += 1;
|
||||||
|
}
|
||||||
|
foreach AllObjects(class'DBRecord', nextRecord) {
|
||||||
|
leftoverDBRAmount += 1;
|
||||||
|
}
|
||||||
|
if ( leftoverObjectAmount == 0 && leftoverActorAmount == 0
|
||||||
|
&& leftoverDBRAmount == 0)
|
||||||
|
{
|
||||||
|
Log("Acedia garbage check: nothing was found.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("Acedia garbage check: garbage was found." @
|
||||||
|
"This can cause problems, report it.");
|
||||||
|
Log("Leftover object:" @ leftoverObjectAmount);
|
||||||
|
Log("Leftover actors:" @ leftoverActorAmount);
|
||||||
|
Log("Leftover database records:" @ leftoverDBRAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function array<FeatureConfigPair> GetAutoConfigurationInfo()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array< class<Feature> > availableFeatures;
|
||||||
|
local FeatureConfigPair nextPair;
|
||||||
|
local array<FeatureConfigPair> result;
|
||||||
|
|
||||||
|
availableFeatures = _.environment.GetAvailableFeatures();
|
||||||
|
for (i = 0; i < availableFeatures.length; i += 1)
|
||||||
|
{
|
||||||
|
nextPair.featureClass = availableFeatures[i];
|
||||||
|
nextPair.configName = availableFeatures[i].static
|
||||||
|
.GetAutoEnabledConfig();
|
||||||
|
result[result.length] = nextPair;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function EnableFeatures(array<FeatureConfigPair> features)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < features.length; i += 1)
|
||||||
|
{
|
||||||
|
if (features[i].featureClass == none) continue;
|
||||||
|
if (features[i].configName == none) continue;
|
||||||
|
features[i].featureClass.static.EnableMe(features[i].configName);
|
||||||
|
_.logger.Auto(infoFeatureEnabled)
|
||||||
|
.Arg(_.text.FromString(string(features[i].featureClass)))
|
||||||
|
.Arg(features[i].configName); // consumes `configName`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches and sets up signals that `Mutator` needs to provide
|
||||||
|
private function SetupMutatorSignals()
|
||||||
|
{
|
||||||
|
local ServerUnrealService service;
|
||||||
|
|
||||||
|
service = ServerUnrealService(class'ServerUnrealService'.static.Require());
|
||||||
|
onMutateSignal = Mutator_OnMutate_Signal(
|
||||||
|
service.GetSignal(class'Mutator_OnMutate_Signal'));
|
||||||
|
onModifyLoginSignal = Mutator_OnModifyLogin_Signal(
|
||||||
|
service.GetSignal(class'Mutator_OnModifyLogin_Signal'));
|
||||||
|
onCheckReplacementSignal = Mutator_OnCheckReplacement_Signal(
|
||||||
|
service.GetSignal(class'Mutator_OnCheckReplacement_Signal'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function RunStartUpTests()
|
||||||
|
{
|
||||||
|
local TestingService testService;
|
||||||
|
|
||||||
|
testService = TestingService(class'TestingService'.static.Require());
|
||||||
|
testService.PrepareTests();
|
||||||
|
if (testService.filterTestsByName) {
|
||||||
|
testService.FilterByName(testService.requiredName);
|
||||||
|
}
|
||||||
|
if (testService.filterTestsByGroup) {
|
||||||
|
testService.FilterByGroup(testService.requiredGroup);
|
||||||
|
}
|
||||||
|
if (!testService.Run()) {
|
||||||
|
_.logger.Auto(errorCannotRunTests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Below `Mutator` events are redirected into appropriate signals.
|
||||||
|
*/
|
||||||
|
function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
||||||
|
{
|
||||||
|
if (onCheckReplacementSignal != none) {
|
||||||
|
return onCheckReplacementSignal.Emit(other, isSuperRelevant);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Mutate(string command, PlayerController sendingController)
|
||||||
|
{
|
||||||
|
if (onMutateSignal != none) {
|
||||||
|
onMutateSignal.Emit(command, sendingController);
|
||||||
|
}
|
||||||
|
super.Mutate(command, sendingController);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModifyLogin(out string portal, out string options)
|
||||||
|
{
|
||||||
|
if (onModifyLoginSignal != none) {
|
||||||
|
onModifyLoginSignal.Emit(portal, options);
|
||||||
|
}
|
||||||
|
super.ModifyLogin(portal, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
clientside = false
|
||||||
|
useGameModes = false
|
||||||
|
// This is a server-only mutator
|
||||||
|
remoteRole = ROLE_SimulatedProxy
|
||||||
|
bAlwaysRelevant = true
|
||||||
|
// Mutator description
|
||||||
|
GroupName = "Package loader"
|
||||||
|
FriendlyName = "Acedia loader"
|
||||||
|
Description = "Launcher for Acedia packages"
|
||||||
|
infoFeatureEnabled = (l=LOG_Info,m="Feature `%1` enabled with config \"%2\".")
|
||||||
|
errNoServerLevelCore = (l=LOG_Error,m="Cannot create `ServerLevelCore`!")
|
||||||
|
errorCannotRunTests = (l=LOG_Error,m="Could not perform Acedia's tests.")
|
||||||
|
}
|
||||||
36
kf_sources/Acedia/Classes/StartUp.uc
Normal file
36
kf_sources/Acedia/Classes/StartUp.uc
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* This actor's role is to add Acedia mutator on listen and dedicated servers.
|
||||||
|
* Copyright 2019-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class StartUp extends Actor;
|
||||||
|
|
||||||
|
function PreBeginPlay()
|
||||||
|
{
|
||||||
|
super.PreBeginPlay();
|
||||||
|
if (level != none && level.game != none) {
|
||||||
|
level.game.AddMutator(string(class'Packages'));
|
||||||
|
}
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
// This is a server-only actor
|
||||||
|
remoteRole = ROLE_None
|
||||||
|
}
|
||||||
364
kf_sources/Acedia/Classes/VotingHandlerAdapter.uc
Normal file
364
kf_sources/Acedia/Classes/VotingHandlerAdapter.uc
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
/**
|
||||||
|
* Acedia currently lacks its own means to provide a map/mode voting
|
||||||
|
* (and new voting mod with proper GUI would not be whitelisted anyway).
|
||||||
|
* This is why this class was made - to inject existing voting handlers with
|
||||||
|
* data from Acedia's game modes.
|
||||||
|
* Requires `GameInfo`'s voting handler to be derived from
|
||||||
|
* `XVotingHandler`, which is satisfied by pretty much every used handler.
|
||||||
|
* Copyright 2021-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class VotingHandlerAdapter extends AcediaObject
|
||||||
|
dependson(VotingHandler)
|
||||||
|
config(AcediaLauncherData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All usage of this object should start with `InjectIntoVotingHandler()`
|
||||||
|
* method that will read all the `GameMode` configs and fill voting handler's
|
||||||
|
* config with their data, while making a backup of all values.
|
||||||
|
* Backup can be restored with `RestoreVotingHandlerConfigBackup()` method.
|
||||||
|
* How that affects the clients depends on whether restoration was done before,
|
||||||
|
* during or after the replication. It is intended to be done after
|
||||||
|
* server travel has started.
|
||||||
|
* the process of injection is to create an ordered list of game modes
|
||||||
|
* (`availableGameModes`) and generate appropriate voting handler's configs
|
||||||
|
* with `BuildVotingHandlerConfig()`, saving them in the same order inside
|
||||||
|
* the voting handler. Picked game mode is then determined by index of
|
||||||
|
* the picked voting handler's option.
|
||||||
|
*
|
||||||
|
* Additionally this class has a static internal state that allows it to
|
||||||
|
* transfer data along the server travel - it is used mainly to remember picked
|
||||||
|
* game mode and enforce game's difficulty by altering and restoring
|
||||||
|
* `GameInfo`'s variable.
|
||||||
|
* To make such transfer happen one must call `PrepareForServerTravel()` before
|
||||||
|
* server travel to set the internal static state and
|
||||||
|
* then `SetupGameModeAfterTravel()` after travel (when the new map is loading)
|
||||||
|
* to read (and forget) from internal state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// All available game modes for Acedia, loaded during initialization.
|
||||||
|
// This array is directly produces replacement for `XVotingHandler`'s
|
||||||
|
// `gameConfig` array and records of `availableGameModes` relate to those of
|
||||||
|
// `gameConfig` with the same index.
|
||||||
|
// So if we know that a voting option with a certain index was chosen -
|
||||||
|
// it means that user picked game mode from `availableGameModes` with
|
||||||
|
// the same index.
|
||||||
|
var private array<Text> availableGameModes;
|
||||||
|
|
||||||
|
// Finding voting handler is not cheap, so only do it once and then store it.
|
||||||
|
var private NativeActorRef votingHandlerReference;
|
||||||
|
// Save `VotingHandler`'s config to restore it before server travel -
|
||||||
|
// otherwise Acedia will alter its config
|
||||||
|
var private array<VotingHandler.MapVoteGameConfig> backupVotingHandlerConfig;
|
||||||
|
|
||||||
|
// Setting value of this flag to `true` indicates that map switching just
|
||||||
|
// occurred and we need to recover some information from the previous map.
|
||||||
|
var private config bool isServerTraveling;
|
||||||
|
// We should not rely on "VotingHandler" to inform us from which game mode its
|
||||||
|
// selected config option originated after server travel, so we need to
|
||||||
|
// remember it in this config variable before switching maps.
|
||||||
|
var private config string targetGameMode;
|
||||||
|
// Acedia's game modes intend on supporting difficulty switching, but
|
||||||
|
// `KFGameType` does not support appropriate flags, so we enforce default
|
||||||
|
// difficulty by overwriting default value of its `gameDifficulty` variable.
|
||||||
|
// But to not affect game's configs we must restore old value after new map is
|
||||||
|
// loaded. Store it in config variable for that.
|
||||||
|
var private config int storedGameLength;
|
||||||
|
|
||||||
|
// Aliases are an unnecessary overkill for difficulty names, so just define
|
||||||
|
// them in special `string` arrays.
|
||||||
|
// We accept not just these exact words, but any of their prefixes.
|
||||||
|
var private const array<string> shortSynonyms;
|
||||||
|
var private const array<string> normalSynonyms;
|
||||||
|
var private const array<string> longSynonyms;
|
||||||
|
var private const array<string> customSynonyms;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition fatNoXVotingHandler, fatBadGameConfigIndexVH;
|
||||||
|
var private LoggerAPI.Definition fatBadGameConfigIndexAdapter;
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
_.memory.Free(votingHandlerReference);
|
||||||
|
_.memory.FreeMany(availableGameModes);
|
||||||
|
votingHandlerReference = none;
|
||||||
|
availableGameModes.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces `XVotingHandler`'s configs with Acedia's game modes.
|
||||||
|
* Backup of replaced configs is made internally, so that they can be restored
|
||||||
|
* on map change.
|
||||||
|
*/
|
||||||
|
public final function InjectIntoVotingHandler()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local GameMode nextGameMode;
|
||||||
|
local XVotingHandler votingHandler;
|
||||||
|
local array<VotingHandler.MapVoteGameConfig> newVotingHandlerConfig;
|
||||||
|
|
||||||
|
if (votingHandlerReference != none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
votingHandler = XVotingHandler(_server.unreal.FindActorInstance(
|
||||||
|
_server.unreal.GetGameType().VotingHandlerClass));
|
||||||
|
if (votingHandler == none)
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatNoXVotingHandler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
votingHandlerReference = _server.unreal.ActorRef(votingHandler);
|
||||||
|
class'GameMode'.static.Initialize();
|
||||||
|
availableGameModes = class'GameMode'.static.AvailableConfigs();
|
||||||
|
for (i = 0; i < availableGameModes.length; i += 1)
|
||||||
|
{
|
||||||
|
nextGameMode = GameMode(class'GameMode'.static
|
||||||
|
.GetConfigInstance(availableGameModes[i]));
|
||||||
|
newVotingHandlerConfig[i] = BuildVotingHandlerConfig(nextGameMode);
|
||||||
|
// Report omitted mutators / server options
|
||||||
|
nextGameMode.ReportBadMutatorNames();
|
||||||
|
nextGameMode.ReportBadOptions();
|
||||||
|
}
|
||||||
|
backupVotingHandlerConfig = votingHandler.gameConfig;
|
||||||
|
votingHandler.gameConfig = newVotingHandlerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig(
|
||||||
|
GameMode gameMode)
|
||||||
|
{
|
||||||
|
local MutableText nextColoredName;
|
||||||
|
local VotingHandler.MapVoteGameConfig result;
|
||||||
|
|
||||||
|
result.gameClass = _.text.IntoString(gameMode.GetGameTypeClass());
|
||||||
|
result.prefix = _.text.IntoString(gameMode.GetMapPrefix());
|
||||||
|
nextColoredName = gameMode
|
||||||
|
.GetTitle()
|
||||||
|
.IntoMutableText()
|
||||||
|
.ChangeDefaultColor(_.color.white);
|
||||||
|
result.gameName = _.text.IntoColoredString(nextColoredName)
|
||||||
|
$ _.color.GetColorTag(_.color.White);
|
||||||
|
nextColoredName = gameMode
|
||||||
|
.GetAcronym()
|
||||||
|
.IntoMutableText()
|
||||||
|
.ChangeDefaultColor(_.color.white);
|
||||||
|
result.acronym = _.text.IntoColoredString(nextColoredName)
|
||||||
|
$ _.color.GetColorTag(_.color.White);
|
||||||
|
result.mutators = BuildMutatorString(gameMode);
|
||||||
|
result.options = BuildOptionsString(gameMode);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function string BuildMutatorString(GameMode gameMode)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local string result;
|
||||||
|
local array<Text> usedMutators;
|
||||||
|
|
||||||
|
usedMutators = gameMode.GetIncludedMutators();
|
||||||
|
for (i = 0; i < usedMutators.length; i += 1)
|
||||||
|
{
|
||||||
|
if (i > 0) {
|
||||||
|
result $= ",";
|
||||||
|
}
|
||||||
|
result $= _.text.IntoString(usedMutators[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function string BuildOptionsString(GameMode gameMode)
|
||||||
|
{
|
||||||
|
local bool optionWasAdded;
|
||||||
|
local string result;
|
||||||
|
local string nextKey, nextValue;
|
||||||
|
local CollectionIterator iter;
|
||||||
|
local HashTable options;
|
||||||
|
|
||||||
|
options = gameMode.GetOptions();
|
||||||
|
for (iter = options.Iterate(); !iter.HasFinished(); iter.Next())
|
||||||
|
{
|
||||||
|
nextKey = _.text.IntoString(Text(iter.GetKey()));
|
||||||
|
nextValue = _.text.IntoString(Text(iter.Get()));
|
||||||
|
if (optionWasAdded) {
|
||||||
|
result $= "?";
|
||||||
|
}
|
||||||
|
result $= (nextKey $ "=" $ nextValue);
|
||||||
|
optionWasAdded = true;
|
||||||
|
}
|
||||||
|
options.FreeSelf();
|
||||||
|
iter.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes necessary preparations for the server travel.
|
||||||
|
*/
|
||||||
|
public final function PrepareForServerTravel()
|
||||||
|
{
|
||||||
|
local int pickedVHConfig;
|
||||||
|
local GameMode nextGameMode;
|
||||||
|
local string nextGameClassName;
|
||||||
|
local class<GameInfo> nextGameClass;
|
||||||
|
local class<KFGameType> nextKFGameType;
|
||||||
|
local XVotingHandler votingHandler;
|
||||||
|
|
||||||
|
if (votingHandlerReference == none) return;
|
||||||
|
votingHandler = XVotingHandler(votingHandlerReference.Get());
|
||||||
|
if (votingHandler == none) return;
|
||||||
|
// Server travel caused by something else than `XVotingHandler`
|
||||||
|
if (!votingHandler.bLevelSwitchPending) return;
|
||||||
|
|
||||||
|
pickedVHConfig = votingHandler.currentGameConfig;
|
||||||
|
if (pickedVHConfig < 0 || pickedVHConfig >= votingHandler.gameConfig.length)
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatBadGameConfigIndexVH)
|
||||||
|
.ArgInt(pickedVHConfig)
|
||||||
|
.ArgInt(votingHandler.gameConfig.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pickedVHConfig >= availableGameModes.length)
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatBadGameConfigIndexAdapter)
|
||||||
|
.ArgInt(pickedVHConfig)
|
||||||
|
.ArgInt(availableGameModes.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextGameClassName = votingHandler.gameConfig[pickedVHConfig].gameClass;
|
||||||
|
if (string(_server.unreal.GetGameType().class) ~= nextGameClassName) {
|
||||||
|
nextGameClass = _server.unreal.GetGameType().class;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nextGameClass =
|
||||||
|
class<GameInfo>(_.memory.LoadClass_S(nextGameClassName));
|
||||||
|
}
|
||||||
|
isServerTraveling = true;
|
||||||
|
targetGameMode = availableGameModes[pickedVHConfig].ToString();
|
||||||
|
nextGameMode = GetConfigFromString(targetGameMode);
|
||||||
|
nextKFGameType = class<KFGameType>(nextGameClass);
|
||||||
|
if (nextKFGameType != none)
|
||||||
|
{
|
||||||
|
storedGameLength = nextKFGameType.default.kfGameLength;
|
||||||
|
nextKFGameType.default.kfGameLength =
|
||||||
|
GetNumericGameLength(nextGameMode);
|
||||||
|
}
|
||||||
|
nextGameClass.static.StaticSaveConfig();
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore `GameInfo`'s settings after the server travel and
|
||||||
|
* apply selected `GameMode`.
|
||||||
|
*
|
||||||
|
* @return `GameMode` picked before server travel
|
||||||
|
* (the one that must be running now).
|
||||||
|
*/
|
||||||
|
public final function GameMode SetupGameModeAfterTravel()
|
||||||
|
{
|
||||||
|
local KFGameType kfGameType;
|
||||||
|
|
||||||
|
if (!isServerTraveling) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
kfGameType = _server.unreal.GetKFGameType();
|
||||||
|
if (kfGameType != none) {
|
||||||
|
kfGameType.default.kfGameLength = storedGameLength;
|
||||||
|
}
|
||||||
|
isServerTraveling = false;
|
||||||
|
_server.unreal.GetGameType().StaticSaveConfig();
|
||||||
|
SaveConfig();
|
||||||
|
return GetConfigFromString(targetGameMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores `XVotingHandler`'s config to the values that were overridden by
|
||||||
|
* `VHAdapter`'s `InjectIntoVotingHandler()` method.
|
||||||
|
*/
|
||||||
|
public final function RestoreVotingHandlerConfigBackup()
|
||||||
|
{
|
||||||
|
local XVotingHandler votingHandler;
|
||||||
|
|
||||||
|
if (votingHandlerReference == none) return;
|
||||||
|
votingHandler = XVotingHandler(votingHandlerReference.Get());
|
||||||
|
if (votingHandler == none) return;
|
||||||
|
|
||||||
|
votingHandler.gameConfig = backupVotingHandlerConfig;
|
||||||
|
votingHandler.default.gameConfig = backupVotingHandlerConfig;
|
||||||
|
votingHandler.SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// `GameMode`'s name as a `string` -> `GameMode` instance
|
||||||
|
private function GameMode GetConfigFromString(string configName)
|
||||||
|
{
|
||||||
|
local GameMode result;
|
||||||
|
local Text nextConfigName;
|
||||||
|
nextConfigName = _.text.FromString(configName);
|
||||||
|
result = GameMode(class'GameMode'.static.GetConfigInstance(nextConfigName));
|
||||||
|
_.memory.Free(nextConfigName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert `GameMode`'s difficulty's textual representation into
|
||||||
|
// KF's numeric one.
|
||||||
|
private final function int GetNumericGameLength(BaseGameMode gameMode)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local string length;
|
||||||
|
|
||||||
|
length = Locs(_.text.IntoString(gameMode.GetLength()));
|
||||||
|
for (i = 0; i < default.shortSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(length, default.shortSynonyms[i])) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < default.normalSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(length, default.normalSynonyms[i])) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < default.longSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(length, default.longSynonyms[i])) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < default.customSynonyms.length; i += 1)
|
||||||
|
{
|
||||||
|
if (IsPrefixOf(length, default.customSynonyms[i])) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final static function bool IsPrefixOf(string prefix, string value)
|
||||||
|
{
|
||||||
|
return (InStr(value, prefix) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
shortSynonyms(0) = "short"
|
||||||
|
normalSynonyms(0) = "normal"
|
||||||
|
normalSynonyms(1) = "medium"
|
||||||
|
normalSynonyms(2) = "regular"
|
||||||
|
longSynonyms(0) = "long"
|
||||||
|
customSynonyms(0) = "custom"
|
||||||
|
fatNoXVotingHandler = (l=LOG_Fatal,m="`XVotingHandler` class is missing. Make sure your server setup supports Acedia's game modes (by used voting handler derived from `XVotingHandler`).")
|
||||||
|
fatBadGameConfigIndexVH = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `XVotingHandler.gameConfig` of length %2. Report this issue.")
|
||||||
|
fatBadGameConfigIndexAdapter = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `VHAdapter` of length %2. Report this issue.")
|
||||||
|
}
|
||||||
11
kf_sources/Acedia/Classes/index.html
Normal file
11
kf_sources/Acedia/Classes/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<html>
|
||||||
|
<head><title>Index of /kf_sources/Acedia/Classes/</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Index of /kf_sources/Acedia/Classes/</h1><hr><pre><a href="../">../</a>
|
||||||
|
<a href="BaseGameMode.uc">BaseGameMode.uc</a> 08-Aug-2022 11:04 13630
|
||||||
|
<a href="GameMode.uc">GameMode.uc</a> 08-Aug-2022 11:50 9256
|
||||||
|
<a href="Packages.uc">Packages.uc</a> 08-Aug-2022 11:38 9297
|
||||||
|
<a href="StartUp.uc">StartUp.uc</a> 08-Aug-2022 06:17 1196
|
||||||
|
<a href="VotingHandlerAdapter.uc">VotingHandlerAdapter.uc</a> 11-Aug-2022 18:26 14483
|
||||||
|
</pre><hr></body>
|
||||||
|
</html>
|
||||||
7
kf_sources/Acedia/index.html
Normal file
7
kf_sources/Acedia/index.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html>
|
||||||
|
<head><title>Index of /kf_sources/Acedia/</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Index of /kf_sources/Acedia/</h1><hr><pre><a href="../">../</a>
|
||||||
|
<a href="Classes/">Classes/</a> 29-Jun-2025 19:43 -
|
||||||
|
</pre><hr></body>
|
||||||
|
</html>
|
||||||
163
kf_sources/AcediaCore/Classes/ACommandFakers.uc
Normal file
163
kf_sources/AcediaCore/Classes/ACommandFakers.uc
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ACommandFakers extends Command
|
||||||
|
dependsOn(VotingModel);
|
||||||
|
|
||||||
|
var private array<UserID> fakers;
|
||||||
|
|
||||||
|
protected static function StaticFinalizer() {
|
||||||
|
__().memory.FreeMany(default.fakers);
|
||||||
|
default.fakers.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function BuildData(CommandDataBuilder builder) {
|
||||||
|
builder.Group(P("debug"));
|
||||||
|
builder.Summary(P("Adds fake voters for testing \"vote\" command."));
|
||||||
|
builder.Describe(P("Displays current fake voters."));
|
||||||
|
|
||||||
|
builder.SubCommand(P("amount"));
|
||||||
|
builder.Describe(P("Specify amount of faker that are allowed to vote."));
|
||||||
|
builder.ParamInteger(P("fakers_amount"));
|
||||||
|
|
||||||
|
builder.SubCommand(P("vote"));
|
||||||
|
builder.Describe(P("Make a vote as a faker."));
|
||||||
|
builder.ParamInteger(P("faker_number"));
|
||||||
|
builder.ParamBoolean(P("vote_for"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Executed(
|
||||||
|
CallData arguments,
|
||||||
|
EPlayer instigator,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
if (arguments.subCommandName.IsEmpty()) {
|
||||||
|
DisplayCurrentFakers();
|
||||||
|
} else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) {
|
||||||
|
ChangeAmount(arguments.parameters.GetInt(P("fakers_amount")));
|
||||||
|
} else if (arguments.subCommandName.Compare(P("vote"), SCASE_INSENSITIVE)) {
|
||||||
|
CastVote(
|
||||||
|
arguments.parameters.GetInt(P("faker_number")),
|
||||||
|
arguments.parameters.GetBool(P("vote_for")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static function /*borrow*/ array<UserID> BorrowDebugVoters() {
|
||||||
|
return default.fakers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function CastVote(int fakerID, bool voteFor) {
|
||||||
|
local Voting currentVoting;
|
||||||
|
|
||||||
|
if (fakerID < 0 || fakerID >= fakers.length) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextFailure)
|
||||||
|
.WriteLine(P("Faker number is out of bounds."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentVoting = _.commands.GetCurrentVoting();
|
||||||
|
if (currentVoting == none) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextFailure)
|
||||||
|
.WriteLine(P("There is no voting active right now."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentVoting.CastVoteByID(fakers[fakerID], voteFor);
|
||||||
|
_.memory.Free(currentVoting);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function ChangeAmount(int newAmount) {
|
||||||
|
local int i;
|
||||||
|
local Text nextIDName;
|
||||||
|
local UserID nextID;
|
||||||
|
local Voting currentVoting;
|
||||||
|
|
||||||
|
if (newAmount < 0) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextFailure)
|
||||||
|
.WriteLine(P("Cannot specify negative amount."));
|
||||||
|
}
|
||||||
|
if (newAmount == fakers.length) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextNeutral)
|
||||||
|
.WriteLine(P("Specified same amount of fakers."));
|
||||||
|
} else if (newAmount > fakers.length) {
|
||||||
|
for (i = fakers.length; i < newAmount; i += 1) {
|
||||||
|
nextIDName = _.text.FromString("DEBUG:FAKER:" $ i);
|
||||||
|
nextID = UserID(__().memory.Allocate(class'UserID'));
|
||||||
|
nextID.Initialize(nextIDName);
|
||||||
|
_.memory.Free(nextIDName);
|
||||||
|
fakers[fakers.length] = nextID;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i = fakers.length - 1; i >= newAmount; i -= 1) {
|
||||||
|
_.memory.Free(fakers[i]);
|
||||||
|
}
|
||||||
|
fakers.length = newAmount;
|
||||||
|
}
|
||||||
|
default.fakers = fakers;
|
||||||
|
currentVoting = _.commands.GetCurrentVoting();
|
||||||
|
if (currentVoting != none) {
|
||||||
|
currentVoting.SetDebugVoters(default.fakers);
|
||||||
|
_.memory.Free(currentVoting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function DisplayCurrentFakers() {
|
||||||
|
local int i;
|
||||||
|
local VotingModel.PlayerVoteStatus nextVoteStatus;
|
||||||
|
local MutableText nextNumber;
|
||||||
|
local Voting currentVoting;
|
||||||
|
|
||||||
|
if (fakers.length <= 0) {
|
||||||
|
callerConsole.WriteLine(P("No fakers!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentVoting =_.commands.GetCurrentVoting();
|
||||||
|
for (i = 0; i < fakers.length; i += 1) {
|
||||||
|
nextNumber = _.text.FromIntM(i);
|
||||||
|
callerConsole
|
||||||
|
.Write(P("Faker #"))
|
||||||
|
.Write(nextNumber)
|
||||||
|
.Write(P(": "));
|
||||||
|
if (currentVoting != none) {
|
||||||
|
nextVoteStatus = currentVoting.GetVote(fakers[i]);
|
||||||
|
}
|
||||||
|
switch (nextVoteStatus) {
|
||||||
|
case PVS_NoVote:
|
||||||
|
callerConsole.WriteLine(P("no vote"));
|
||||||
|
break;
|
||||||
|
case PVS_VoteFor:
|
||||||
|
callerConsole.UseColorOnce(_.color.TextPositive).WriteLine(P("vote for"));
|
||||||
|
break;
|
||||||
|
case PVS_VoteAgainst:
|
||||||
|
callerConsole.UseColorOnce(_.color.TextNegative).WriteLine(P("vote against"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
callerConsole.UseColorOnce(_.color.TextFailure).WriteLine(P("vote !ERROR!"));
|
||||||
|
}
|
||||||
|
_.memory.Free(nextNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
preferredName = "fakers"
|
||||||
|
}
|
||||||
731
kf_sources/AcediaCore/Classes/ACommandHelp.uc
Normal file
731
kf_sources/AcediaCore/Classes/ACommandHelp.uc
Normal file
@ -0,0 +1,731 @@
|
|||||||
|
/**
|
||||||
|
* Command for displaying help information about registered Acedia's commands.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ACommandHelp extends Command
|
||||||
|
dependson(LoggerAPI)
|
||||||
|
dependson(CommandAPI);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # `ACommandHelp`
|
||||||
|
*
|
||||||
|
* This built-in command is meant to do two things:
|
||||||
|
*
|
||||||
|
* 1. List all available commands and aliases related to them;
|
||||||
|
* 2. Display help pages for available commands (both by command and
|
||||||
|
* alias names).
|
||||||
|
*
|
||||||
|
* ## Implementation
|
||||||
|
*
|
||||||
|
* ### Aliases loading
|
||||||
|
*
|
||||||
|
* First thing this command tries to do upon creation is to read all
|
||||||
|
* command aliases and build a reverse map that allows to access aliases names
|
||||||
|
* by command + subcommand (even if latter one is empty) that these names
|
||||||
|
* refer to. This allows us to display relevant aliases names alongside
|
||||||
|
* command names. Map is stored inside `commandToAliasesMap` variable.
|
||||||
|
* (If aliases feature isn't yet available, `ACommandHelp` connects to
|
||||||
|
* `OnFeatureEnabled()` signal to do its job the moment required feature is
|
||||||
|
* enabled).
|
||||||
|
* This map is also made to be two-level one - it is...
|
||||||
|
*
|
||||||
|
* * a `HashTable` with command names as keys,
|
||||||
|
* * that points to `HashTable`s with subcommand names as keys,
|
||||||
|
* * that points at `ArrayList` of aliases.
|
||||||
|
*
|
||||||
|
* This allows us to also sort aliases by the subcommand name when displaying
|
||||||
|
* their list. This work is done by `FillCommandToAliasesMap()` method (that
|
||||||
|
* uses `ParseCommandNames()` method, which is also used for displaying
|
||||||
|
* command list).
|
||||||
|
*
|
||||||
|
* ### Command list displaying
|
||||||
|
*
|
||||||
|
* This is a simple process performed by `DisplayCommandLists()` that calls on
|
||||||
|
* a bunch of auxiliary `Print...()` methods.
|
||||||
|
*
|
||||||
|
* ### Displaying help pages
|
||||||
|
*
|
||||||
|
* Similar to the above, this is mostly a simple process that displays
|
||||||
|
* `Command`s' `CommandData` as a help page. Work is performed by
|
||||||
|
* `DisplayCommandHelpPages()` method that calls on a bunch of auxiliary
|
||||||
|
* `Print...()` methods, most notably `PrintHelpPageFor()` that can display
|
||||||
|
* help pages both for full commands, as well as only for a singular
|
||||||
|
* subcommand, which allows us to print proper help pages for command aliases
|
||||||
|
* that refer to a particular subcommand.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// For each key (given by lower case command name) stores another `HashMap`
|
||||||
|
// that uses sub-command names as keys and returns `ArrayList` of aliases.
|
||||||
|
var private HashTable commandToAliasesMap;
|
||||||
|
|
||||||
|
var private User callerUser;
|
||||||
|
|
||||||
|
var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
|
||||||
|
var public const int TOPEN_BRACKET, TCLOSE_BRACKET, TCOLON_SPACE;
|
||||||
|
var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT;
|
||||||
|
var public const int TBOOLEAN_TRUE_FALSE, TBOOLEAN_ENABLE_DISABLE;
|
||||||
|
var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO;
|
||||||
|
var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET;
|
||||||
|
var public const int TSEPARATOR, TLIST_REGIRESTED_CMDS, TEMPTY_GROUP;
|
||||||
|
var public const int TALIASES_FOR, TEMPTY, TDOT, TNO_COMMAND_BEGIN;
|
||||||
|
var public const int TNO_COMMAND_END, TEMPTY_GROUP_BEGIN, TEMPTY_GROUP_END;
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
local Feature preenabledInstance;
|
||||||
|
|
||||||
|
super.Constructor();
|
||||||
|
// We need to update aliases map every time aliases feature is reenabled
|
||||||
|
_.environment.OnFeatureEnabled(self).connect = FillCommandToAliasesMap;
|
||||||
|
// If `Aliases_Feature` is already enabled - read its command aliases now
|
||||||
|
preenabledInstance = class'Aliases_Feature'.static.GetEnabledInstance();
|
||||||
|
FillCommandToAliasesMap(Aliases_Feature(preenabledInstance));
|
||||||
|
_.memory.Free(preenabledInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
_.memory.Free(commandToAliasesMap);
|
||||||
|
commandToAliasesMap = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function BuildData(CommandDataBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Group(P("core"));
|
||||||
|
builder.Summary(P("Displays detailed information about available commands."));
|
||||||
|
builder.OptionalParams();
|
||||||
|
builder.ParamTextList(P("commands"));
|
||||||
|
|
||||||
|
builder.Option(P("aliases"));
|
||||||
|
builder.Describe(P("When displaying available commands, specifying this flag will additionally"
|
||||||
|
@ "make command to display all of their available aliases."));
|
||||||
|
|
||||||
|
builder.Option(P("list"));
|
||||||
|
builder.Describe(P("Display available commands. Optionally command groups can be specified and"
|
||||||
|
@ "then only commands from such groups will be listed. Otherwise all commands will"
|
||||||
|
@ "be displayed."));
|
||||||
|
builder.OptionalParams();
|
||||||
|
builder.ParamTextList(P("groups"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Executed(
|
||||||
|
Command.CallData callData,
|
||||||
|
EPlayer callerPlayer,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
local bool printedSomething;
|
||||||
|
local HashTable parameters, options;
|
||||||
|
local ArrayList commandsToDisplay, commandGroupsToDisplay;
|
||||||
|
|
||||||
|
callerUser = callerPlayer.GetIdentity();
|
||||||
|
parameters = callData.parameters;
|
||||||
|
options = callData.options;
|
||||||
|
// Print command list if "--list" option was specified
|
||||||
|
if (options.HasKey(P("list")))
|
||||||
|
{
|
||||||
|
commandGroupsToDisplay = options.GetArrayListBy(P("/list/groups"));
|
||||||
|
DisplayCommandLists(
|
||||||
|
commandGroupsToDisplay,
|
||||||
|
options.HasKey(P("aliases")));
|
||||||
|
_.memory.Free(commandGroupsToDisplay);
|
||||||
|
printedSomething = true;
|
||||||
|
}
|
||||||
|
// Help pages.
|
||||||
|
// Only need to print them if:
|
||||||
|
// 1. Any commands are specified as parameters;
|
||||||
|
// 2. No commands or "--list" option was specified, then we want to
|
||||||
|
// print a help page for this command.
|
||||||
|
if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
|
||||||
|
{
|
||||||
|
commandsToDisplay = parameters.GetArrayList(P("commands"));
|
||||||
|
DisplayCommandHelpPages(commandsToDisplay, printedSomething);
|
||||||
|
_.memory.Free(commandsToDisplay);
|
||||||
|
}
|
||||||
|
_.memory.Free(callerUser);
|
||||||
|
callerUser = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If instance of the `Aliases_Feature` is passed as an argument (allowing this
|
||||||
|
// method to be used as a slot for `OnFeatureEnabled` signal) and
|
||||||
|
// `commandAliasSource` is available - empties current `commandToAliasesMap`
|
||||||
|
// and refills it with available command aliases.
|
||||||
|
private final function FillCommandToAliasesMap(Feature enabledFeature)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text aliasValue;
|
||||||
|
local array<Text> availableAliases;
|
||||||
|
local BaseAliasSource commandAliasSource;
|
||||||
|
local MutableText commandName, subcommandName;
|
||||||
|
|
||||||
|
if (enabledFeature == none) return;
|
||||||
|
if (enabledFeature.class != class'Aliases_Feature') return;
|
||||||
|
commandAliasSource = _.alias.GetCommandSource();
|
||||||
|
if (commandAliasSource == none) return;
|
||||||
|
|
||||||
|
// Drop map built by previous aliases (if it was even build up until now)
|
||||||
|
_.memory.Free(commandToAliasesMap);
|
||||||
|
commandToAliasesMap = _.collections.EmptyHashTable();
|
||||||
|
// Construct a command -> alias map by iterating over aliases
|
||||||
|
availableAliases = commandAliasSource.GetAllAliases();
|
||||||
|
for (i = 0; i < availableAliases.length; i += 1)
|
||||||
|
{
|
||||||
|
aliasValue = _.alias.ResolveCommand(availableAliases[i]);
|
||||||
|
ParseCommandNames(aliasValue, commandName, subcommandName);
|
||||||
|
aliasValue.FreeSelf();
|
||||||
|
InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]);
|
||||||
|
commandName.FreeSelf();
|
||||||
|
subcommandName.FreeSelf();
|
||||||
|
commandName = none;
|
||||||
|
subcommandName = none;
|
||||||
|
}
|
||||||
|
// Clean up
|
||||||
|
_.memory.FreeMany(availableAliases);
|
||||||
|
commandAliasSource.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breaks command name as it is intended to be specified in command aliases
|
||||||
|
// ("<command>" or "<command>.<subcommand>") into command and subcommand,
|
||||||
|
// returning both as `out` parameters.
|
||||||
|
private final function ParseCommandNames(
|
||||||
|
BaseText source,
|
||||||
|
out MutableText commandName,
|
||||||
|
out MutableText subcommandName)
|
||||||
|
{
|
||||||
|
local Parser valueParser;
|
||||||
|
|
||||||
|
if (source == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (subcommandName != none) {
|
||||||
|
subcommandName.FreeSelf();
|
||||||
|
}
|
||||||
|
valueParser = source.Parse();
|
||||||
|
subcommandName = valueParser
|
||||||
|
.MUntil(commandName, T(TDOT).GetCharacter(0))
|
||||||
|
.Match(T(TDOT))
|
||||||
|
.GetRemainderM();
|
||||||
|
_.memory.Free(valueParser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes that `commandToAliasesMap` and its arguments are not `none`.
|
||||||
|
private final function InsertIntoAliasesMap(
|
||||||
|
BaseText commandName,
|
||||||
|
BaseText subcommandName,
|
||||||
|
BaseText alias)
|
||||||
|
{
|
||||||
|
local Text commandKey, subcommandKey;
|
||||||
|
local HashTable subcommandToAliasesMap;
|
||||||
|
local ArrayList aliasesArray;
|
||||||
|
|
||||||
|
commandKey = commandName.LowerCopy();
|
||||||
|
subcommandKey = subcommandName.LowerCopy();
|
||||||
|
subcommandToAliasesMap = commandToAliasesMap.GetHashTable(commandKey);
|
||||||
|
if (subcommandToAliasesMap == none) {
|
||||||
|
subcommandToAliasesMap = _.collections.EmptyHashTable();
|
||||||
|
}
|
||||||
|
commandToAliasesMap.SetItem(commandKey, subcommandToAliasesMap);
|
||||||
|
aliasesArray = subcommandToAliasesMap.GetArrayList(subcommandKey);
|
||||||
|
if (aliasesArray == none) {
|
||||||
|
aliasesArray = _.collections.EmptyArrayList();
|
||||||
|
}
|
||||||
|
subcommandToAliasesMap.SetItem(subcommandKey, aliasesArray);
|
||||||
|
if (aliasesArray.Find(alias) < 0) {
|
||||||
|
aliasesArray.AddItem(alias);
|
||||||
|
}
|
||||||
|
// Cleaning up local references to collections and keys
|
||||||
|
subcommandToAliasesMap.FreeSelf();
|
||||||
|
aliasesArray.FreeSelf();
|
||||||
|
commandKey.FreeSelf();
|
||||||
|
subcommandKey.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function DisplayCommandLists(
|
||||||
|
ArrayList commandGroupsToDisplay,
|
||||||
|
bool displayAliases)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<Text> commandNames, groupsNames;
|
||||||
|
|
||||||
|
if (commandGroupsToDisplay == none) {
|
||||||
|
groupsNames = _.commands.GetGroupsNames();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (i = 0; i < commandGroupsToDisplay.GetLength(); i += 1) {
|
||||||
|
groupsNames[groupsNames.length] = commandGroupsToDisplay.GetText(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callerConsole.WriteLine(T(TLIST_REGIRESTED_CMDS));
|
||||||
|
for (i = 0; i < groupsNames.length; i += 1)
|
||||||
|
{
|
||||||
|
if (groupsNames[i] == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
commandNames = _.commands.GetCommandNamesInGroup(groupsNames[i]);
|
||||||
|
if (commandNames.length > 0)
|
||||||
|
{
|
||||||
|
callerConsole.UseColorOnce(_.color.TextSubHeader);
|
||||||
|
if (groupsNames[i].IsEmpty()) {
|
||||||
|
callerConsole.WriteLine(T(TEMPTY_GROUP));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callerConsole.WriteLine(groupsNames[i]);
|
||||||
|
}
|
||||||
|
PrintCommandsNamesArray(
|
||||||
|
commandNames,
|
||||||
|
displayAliases);
|
||||||
|
_.memory.FreeMany(commandNames);
|
||||||
|
} else {
|
||||||
|
callerConsole.UseColor(_.color.TextFailure);
|
||||||
|
callerConsole.Write(T(TEMPTY_GROUP_BEGIN));
|
||||||
|
callerConsole.Write(groupsNames[i]);
|
||||||
|
callerConsole.WriteLine(T(TEMPTY_GROUP_END));
|
||||||
|
callerConsole.ResetColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.FreeMany(groupsNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintCommandsNamesArray(
|
||||||
|
array<Text> commandsNamesArray,
|
||||||
|
bool displayAliases
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
local Command.Data nextData;
|
||||||
|
local CommandAPI.CommandConfigInfo nextCommandPair;
|
||||||
|
|
||||||
|
for (i = 0; i < commandsNamesArray.length; i += 1)
|
||||||
|
{
|
||||||
|
nextCommandPair = _.commands.ResolveCommandForUser(
|
||||||
|
commandsNamesArray[i],
|
||||||
|
callerUser);
|
||||||
|
if (nextCommandPair.instance != none && !nextCommandPair.usageForbidden) {
|
||||||
|
nextData = nextCommandPair.instance.BorrowData();
|
||||||
|
callerConsole
|
||||||
|
.UseColorOnce(_.color.textEmphasis)
|
||||||
|
.Write(commandsNamesArray[i])
|
||||||
|
.Write(T(TCOLON_SPACE))
|
||||||
|
.WriteLine(nextData.summary);
|
||||||
|
if (displayAliases) {
|
||||||
|
PrintCommandAliases(commandsNamesArray[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(nextCommandPair.instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintCommandAliases(BaseText commandName)
|
||||||
|
{
|
||||||
|
local CollectionIterator iter;
|
||||||
|
local Text commandKey;
|
||||||
|
local Text nextSubCommand;
|
||||||
|
local ArrayList nextAliasesArray;
|
||||||
|
local HashTable subCommandToAliasesMap;
|
||||||
|
|
||||||
|
if (commandName == none) return;
|
||||||
|
if (commandToAliasesMap == none) return;
|
||||||
|
|
||||||
|
commandKey = commandName.LowerCopy();
|
||||||
|
subCommandToAliasesMap = commandToAliasesMap.GetHashTable(commandName);
|
||||||
|
commandKey.FreeSelf();
|
||||||
|
if (subCommandToAliasesMap == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Display aliases to command itself first
|
||||||
|
nextAliasesArray = subCommandToAliasesMap.GetArrayList(T(TEMPTY));
|
||||||
|
PrintAliasesArray(commandName, none, nextAliasesArray);
|
||||||
|
_.memory.Free(nextAliasesArray);
|
||||||
|
// Then aliases to all of its subcommands, in no particular order
|
||||||
|
iter = subCommandToAliasesMap.Iterate();
|
||||||
|
while (!iter.HasFinished())
|
||||||
|
{
|
||||||
|
nextSubCommand = Text(iter.Getkey()); // cannot be `none`
|
||||||
|
if (nextSubCommand.IsEmpty())
|
||||||
|
{
|
||||||
|
nextSubCommand.FreeSelf();
|
||||||
|
iter.Next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextAliasesArray = ArrayList(iter.Get());
|
||||||
|
PrintAliasesArray(commandName, nextSubCommand, nextAliasesArray);
|
||||||
|
_.memory.Free(nextAliasesArray);
|
||||||
|
_.memory.Free(nextSubCommand);
|
||||||
|
iter.Next();
|
||||||
|
}
|
||||||
|
iter.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintAliasesArray(
|
||||||
|
BaseText commandName,
|
||||||
|
BaseText subcommandName,
|
||||||
|
ArrayList aliasesArray)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text nextAlias;
|
||||||
|
|
||||||
|
if (commandName == none) return;
|
||||||
|
if (aliasesArray == none) return;
|
||||||
|
|
||||||
|
callerConsole
|
||||||
|
.Write(T(TALIASES_FOR))
|
||||||
|
.Write(T(TSPACE))
|
||||||
|
.UseColorOnce(_.color.TextEmphasis)
|
||||||
|
.Write(commandName);
|
||||||
|
if (subcommandName != none)
|
||||||
|
{
|
||||||
|
callerConsole
|
||||||
|
.Write(T(TSPACE))
|
||||||
|
.UseColorOnce(_.color.TextEmphasis)
|
||||||
|
.Write(subCommandName);
|
||||||
|
}
|
||||||
|
callerConsole.Write(T(TCOLON_SPACE));
|
||||||
|
for (i = 0; i < aliasesArray.GetLength(); i += 1)
|
||||||
|
{
|
||||||
|
if (i > 0) {
|
||||||
|
callerConsole.Write(T(TCOMMA_SPACE));
|
||||||
|
}
|
||||||
|
nextAlias = aliasesArray.GetText(i);
|
||||||
|
callerConsole.UseColorOnce(_.color.TextNeutral).Write(nextAlias);
|
||||||
|
nextAlias.FreeSelf();
|
||||||
|
}
|
||||||
|
callerConsole.WriteBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function DisplayCommandHelpPages(ArrayList commandList, bool printedSomething) {
|
||||||
|
local int i;
|
||||||
|
local Text nextUserProvidedName;
|
||||||
|
local MutableText referredSubcommand;
|
||||||
|
local CommandAPI.CommandConfigInfo nextPair;
|
||||||
|
|
||||||
|
// If arguments were empty - at least display our own help page
|
||||||
|
if (commandList == none) {
|
||||||
|
nextPair.instance = self;
|
||||||
|
PrintHelpPageFor(usedName, none, nextPair);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise - print help for specified commands
|
||||||
|
for (i = 0; i < commandList.GetLength(); i += 1) {
|
||||||
|
nextUserProvidedName = commandList.GetText(i);
|
||||||
|
nextPair = GetCommandFromUserProvidedName(
|
||||||
|
nextUserProvidedName,
|
||||||
|
/*out*/ referredSubcommand);
|
||||||
|
if (nextPair.instance != none && !nextPair.usageForbidden) {
|
||||||
|
if (printedSomething) {
|
||||||
|
callerConsole.WriteLine(T(TSEPARATOR));
|
||||||
|
}
|
||||||
|
PrintHelpPageFor(
|
||||||
|
nextUserProvidedName,
|
||||||
|
referredSubcommand,
|
||||||
|
nextPair);
|
||||||
|
printedSomething = true;
|
||||||
|
} else if (nextPair.instance != none) {
|
||||||
|
callerConsole.UseColor(_.color.TextFailure);
|
||||||
|
callerConsole.Write(T(TNO_COMMAND_BEGIN));
|
||||||
|
callerConsole.Write(nextUserProvidedName);
|
||||||
|
callerConsole.WriteLine(T(TNO_COMMAND_END));
|
||||||
|
callerConsole.ResetColor();
|
||||||
|
}
|
||||||
|
_.memory.Free(nextPair.instance);
|
||||||
|
_.memory.Free(nextUserProvidedName);
|
||||||
|
_.memory.Free(referredSubcommand);
|
||||||
|
// `referredSubcommand` is passed as an `out` parameter on
|
||||||
|
// every iteration, so we need to prevent the possibility of its value
|
||||||
|
// being used.
|
||||||
|
// NOTE: `nextCommand` and `nextUserProvidedName` are just rewritten.
|
||||||
|
referredSubcommand = none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns `Command` based on the name, given by user.
|
||||||
|
// `referredSubcommand` is always overwritten (freed if non-`none` value
|
||||||
|
// is passed) and is used to return name of the subcommand for returned
|
||||||
|
// `Command` that is specified by `nextUserProvidedName` (only relevant for
|
||||||
|
// aliases that refer to a particular subcommand).
|
||||||
|
private final function CommandAPI.CommandConfigInfo GetCommandFromUserProvidedName(
|
||||||
|
BaseText nextUserProvidedName,
|
||||||
|
out MutableText referredSubcommand)
|
||||||
|
{
|
||||||
|
local CommandAPI.CommandConfigInfo result;
|
||||||
|
local Text commandAliasValue;
|
||||||
|
local MutableText parsedCommandName;
|
||||||
|
|
||||||
|
// Clear `out` parameter no matter what
|
||||||
|
if (referredSubcommand != none) {
|
||||||
|
referredSubcommand.FreeSelf();
|
||||||
|
referredSubcommand = none;
|
||||||
|
}
|
||||||
|
// Try getting command using `nextUserProvidedName` as a literal name
|
||||||
|
result = _.commands.ResolveCommandForUser(nextUserProvidedName, callerUser);
|
||||||
|
if (result.instance != none) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// On failure - try resolving it as an alias
|
||||||
|
commandAliasValue = _.alias.ResolveCommand(nextUserProvidedName);
|
||||||
|
ParseCommandNames(commandAliasValue, parsedCommandName, referredSubcommand);
|
||||||
|
result = _.commands.ResolveCommandForUser(parsedCommandName, callerUser);
|
||||||
|
if ( result.instance == none
|
||||||
|
|| !result.instance.IsSubCommandAllowed(referredSubcommand, result.config)) {
|
||||||
|
_.memory.Free(result.instance);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Empty subcommand name from the alias is essentially no subcommand name
|
||||||
|
if (referredSubcommand != none && referredSubcommand.IsEmpty()) {
|
||||||
|
referredSubcommand.FreeSelf();
|
||||||
|
referredSubcommand = none;
|
||||||
|
}
|
||||||
|
_.memory.Free2(commandAliasValue, parsedCommandName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintHelpPageFor(
|
||||||
|
BaseText commandAlias,
|
||||||
|
BaseText referredSubcommand,
|
||||||
|
CommandAPI.CommandConfigInfo commandPair
|
||||||
|
) {
|
||||||
|
local Text commandNameLowerCase, commandNameUpperCase;
|
||||||
|
|
||||||
|
// Get capitalized command name
|
||||||
|
commandNameUpperCase = commandAlias.UpperCopy();
|
||||||
|
// Print header: name + basic info
|
||||||
|
callerConsole.UseColor(_.color.textHeader)
|
||||||
|
.Write(commandNameUpperCase)
|
||||||
|
.UseColor(_.color.textDefault);
|
||||||
|
commandNameUpperCase.FreeSelf();
|
||||||
|
if (commandPair.instance.BorrowData().requiresTarget) {
|
||||||
|
callerConsole.WriteLine(T(TCMD_WITH_TARGET));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callerConsole.WriteLine(T(TCMD_WITHOUT_TARGET));
|
||||||
|
}
|
||||||
|
// Print commands and options
|
||||||
|
commandNameLowerCase = commandAlias.LowerCopy();
|
||||||
|
PrintCommands(commandPair, commandNameLowerCase, referredSubcommand);
|
||||||
|
commandNameLowerCase.FreeSelf();
|
||||||
|
PrintOptions(commandPair.instance.BorrowData());
|
||||||
|
// Clean up
|
||||||
|
callerConsole.ResetColor().Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintCommands(
|
||||||
|
CommandAPI.CommandConfigInfo commandPair,
|
||||||
|
BaseText commandName,
|
||||||
|
BaseText referredSubcommand
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
local array<Command.SubCommand> subCommands;
|
||||||
|
|
||||||
|
subCommands = commandPair.instance.BorrowData().subCommands;
|
||||||
|
for (i = 0; i < subCommands.length; i += 1) {
|
||||||
|
if (referredSubcommand == none || referredSubcommand.Compare(subCommands[i].name)) {
|
||||||
|
if (commandPair.instance.IsSubCommandAllowed(subCommands[i].name, commandPair.config)) {
|
||||||
|
PrintSubCommand(subCommands[i], commandName, referredSubcommand != none);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintSubCommand(
|
||||||
|
SubCommand subCommand,
|
||||||
|
BaseText commandName,
|
||||||
|
bool skipSubcommandName)
|
||||||
|
{
|
||||||
|
// Command + parameters
|
||||||
|
// Command name + sub command name
|
||||||
|
callerConsole.UseColor(_.color.textEmphasis)
|
||||||
|
.Write(commandName)
|
||||||
|
.Write(T(TSPACE));
|
||||||
|
if ( !skipSubcommandName
|
||||||
|
&& subCommand.name != none
|
||||||
|
&& !subCommand.name.IsEmpty())
|
||||||
|
{
|
||||||
|
callerConsole.Write(subCommand.name).Write(T(TSPACE));
|
||||||
|
}
|
||||||
|
callerConsole.UseColor(_.color.textDefault);
|
||||||
|
// Parameters
|
||||||
|
PrintParameters(subCommand.required, subCommand.optional);
|
||||||
|
callerConsole.Flush();
|
||||||
|
// Description
|
||||||
|
if (subCommand.description != none && !subCommand.description.IsEmpty()) {
|
||||||
|
callerConsole.WriteBlock(subCommand.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintOptions(Command.Data data)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<Option> options;
|
||||||
|
options = data.options;
|
||||||
|
if (options.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.textSubHeader)
|
||||||
|
.WriteLine(T(TOPTIONS))
|
||||||
|
.UseColor(_.color.textDefault);
|
||||||
|
for (i = 0; i < options.length; i += 1) {
|
||||||
|
PrintOption(options[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintOption(Option option)
|
||||||
|
{
|
||||||
|
local Text shortNameAsText;
|
||||||
|
// Option short and long names with added key characters
|
||||||
|
shortNameAsText = _.text.FromCharacter(option.shortName);
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.textEmphasis)
|
||||||
|
.Write(T(TKEY)).Write(shortNameAsText) // "-"
|
||||||
|
.UseColor(_.color.textDefault)
|
||||||
|
.Write(T(TCOMMA_SPACE)) // ", "
|
||||||
|
.UseColor(_.color.textEmphasis)
|
||||||
|
.Write(T(TDOUBLE_KEY)).Write(option.longName) // "--"
|
||||||
|
.UseColor(_.color.textDefault);
|
||||||
|
shortNameAsText.FreeSelf();
|
||||||
|
// Parameters
|
||||||
|
if (option.required.length != 0 || option.optional.length != 0)
|
||||||
|
{
|
||||||
|
callerConsole.Write(T(TSPACE));
|
||||||
|
PrintParameters(option.required, option.optional);
|
||||||
|
}
|
||||||
|
callerConsole.Flush();
|
||||||
|
// Description
|
||||||
|
if (option.description != none && !option.description.IsEmpty()) {
|
||||||
|
callerConsole.WriteBlock(option.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintParameters(
|
||||||
|
array<Parameter> required,
|
||||||
|
array<Parameter> optional)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
// Print required
|
||||||
|
for (i = 0; i < required.length; i += 1)
|
||||||
|
{
|
||||||
|
PrintParameter(required[i]);
|
||||||
|
if (i < required.length - 1) {
|
||||||
|
callerConsole.Write(T(TSPACE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (optional.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Print optional
|
||||||
|
callerConsole.Write(T(TSPACE)).Write(T(TOPEN_BRACKET));
|
||||||
|
for (i = 0; i < optional.length; i += 1)
|
||||||
|
{
|
||||||
|
PrintParameter(optional[i]);
|
||||||
|
if (i < optional.length - 1) {
|
||||||
|
callerConsole.Write(T(TSPACE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callerConsole.Write(T(TCLOSE_BRACKET));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function PrintParameter(Parameter parameter)
|
||||||
|
{
|
||||||
|
switch (parameter.type)
|
||||||
|
{
|
||||||
|
case CPT_Boolean:
|
||||||
|
callerConsole.UseColor(_.color.typeBoolean);
|
||||||
|
break;
|
||||||
|
case CPT_Integer:
|
||||||
|
callerConsole.UseColor(_.color.typeNumber);
|
||||||
|
break;
|
||||||
|
case CPT_Number:
|
||||||
|
callerConsole.UseColor(_.color.typeNumber);
|
||||||
|
break;
|
||||||
|
case CPT_Text:
|
||||||
|
case CPT_Remainder:
|
||||||
|
callerConsole.UseColor(_.color.typeString);
|
||||||
|
break;
|
||||||
|
case CPT_Object:
|
||||||
|
case CPT_JSON:
|
||||||
|
case CPT_Array:
|
||||||
|
case CPT_PLAYERS:
|
||||||
|
callerConsole.UseColor(_.color.typeLiteral);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
callerConsole.UseColor(_.color.textDefault);
|
||||||
|
}
|
||||||
|
callerConsole.Write(parameter.displayName);
|
||||||
|
if (parameter.allowsList) {
|
||||||
|
callerConsole.Write(T(TPLUS));
|
||||||
|
}
|
||||||
|
callerConsole.UseColor(_.color.textDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
TSPACE = 0
|
||||||
|
stringConstants(0) = " "
|
||||||
|
TPLUS = 1
|
||||||
|
stringConstants(1) = "(+)"
|
||||||
|
TOPEN_BRACKET = 2
|
||||||
|
stringConstants(2) = "["
|
||||||
|
TCLOSE_BRACKET = 3
|
||||||
|
stringConstants(3) = "]"
|
||||||
|
TKEY = 4
|
||||||
|
stringConstants(4) = "-"
|
||||||
|
TDOUBLE_KEY = 5
|
||||||
|
stringConstants(5) = "--"
|
||||||
|
TCOMMA_SPACE = 6
|
||||||
|
stringConstants(6) = ", "
|
||||||
|
TCOLON_SPACE = 7
|
||||||
|
stringConstants(7) = ": "
|
||||||
|
TINDENT = 8
|
||||||
|
stringConstants(8) = " "
|
||||||
|
TBOOLEAN = 9
|
||||||
|
stringConstants(9) = "boolean"
|
||||||
|
TBOOLEAN_TRUE_FALSE = 10
|
||||||
|
stringConstants(10) = "true/false"
|
||||||
|
TBOOLEAN_ENABLE_DISABLE = 11
|
||||||
|
stringConstants(11) = "enable/disable"
|
||||||
|
TBOOLEAN_ON_OFF = 12
|
||||||
|
stringConstants(12) = "on/off"
|
||||||
|
TBOOLEAN_YES_NO = 13
|
||||||
|
stringConstants(13) = "yes/no"
|
||||||
|
TCMD_WITH_TARGET = 14
|
||||||
|
stringConstants(14) = ": This command requires target to be specified."
|
||||||
|
TCMD_WITHOUT_TARGET = 15
|
||||||
|
stringConstants(15) = ": This command does not require target to be specified."
|
||||||
|
TOPTIONS = 16
|
||||||
|
stringConstants(16) = "OPTIONS"
|
||||||
|
TSEPARATOR = 17
|
||||||
|
stringConstants(17) = "============================="
|
||||||
|
TLIST_REGIRESTED_CMDS = 18
|
||||||
|
stringConstants(18) = "{$TextHeader List of registered commands}"
|
||||||
|
TEMPTY_GROUP = 19
|
||||||
|
stringConstants(19) = "Empty group"
|
||||||
|
TALIASES_FOR = 20
|
||||||
|
stringConstants(20) = "Aliases for"
|
||||||
|
TEMPTY = 21
|
||||||
|
stringConstants(21) = ""
|
||||||
|
TDOT = 22
|
||||||
|
stringConstants(22) = "."
|
||||||
|
TNO_COMMAND_BEGIN = 23
|
||||||
|
stringConstants(23) = "Command `"
|
||||||
|
TNO_COMMAND_END = 24
|
||||||
|
stringConstants(24) = "` not found!"
|
||||||
|
TEMPTY_GROUP_BEGIN = 25
|
||||||
|
stringConstants(25) = "No commands in group \""
|
||||||
|
TEMPTY_GROUP_END = 26
|
||||||
|
stringConstants(26) = "\"!"
|
||||||
|
preferredName = "help"
|
||||||
|
}
|
||||||
69
kf_sources/AcediaCore/Classes/ACommandNotify.uc
Normal file
69
kf_sources/AcediaCore/Classes/ACommandNotify.uc
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ACommandNotify extends Command
|
||||||
|
dependsOn(ChatApi);
|
||||||
|
|
||||||
|
protected function BuildData(CommandDataBuilder builder) {
|
||||||
|
builder.Group(P("core"));
|
||||||
|
builder.Summary(P("Notifies players with provided message."));
|
||||||
|
builder.ParamText(P("message"));
|
||||||
|
builder.OptionalParams();
|
||||||
|
builder.ParamNumber(P("duration"));
|
||||||
|
builder.Describe(P("Notify to players message with distinct header and body."));
|
||||||
|
builder.RequireTarget();
|
||||||
|
|
||||||
|
builder.Option(P("title"));
|
||||||
|
builder.Describe(P("Specify the optional title of the notification."));
|
||||||
|
builder.ParamText(P("title"));
|
||||||
|
|
||||||
|
builder.Option(P("channel"));
|
||||||
|
builder.Describe(P("Specify the optional channel. A channel is a grouping mechanism used to"
|
||||||
|
@ "control the display of related notifications. Only last message from the same channel is"
|
||||||
|
@ "stored in queue."));
|
||||||
|
builder.ParamText(P("channel_name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function ExecutedFor(
|
||||||
|
EPlayer target,
|
||||||
|
CallData arguments,
|
||||||
|
EPlayer instigator,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
local Text title, message, plainTitle, plainMessage;
|
||||||
|
|
||||||
|
plainMessage = arguments.parameters.GetText(P("message"));
|
||||||
|
if (arguments.options.HasKey(P("title"))) {
|
||||||
|
plainTitle = arguments.options.GetTextBy(P("/title/title"));
|
||||||
|
}
|
||||||
|
title = _.text.FromFormatted(plainTitle);
|
||||||
|
message = _.text.FromFormatted(plainMessage);
|
||||||
|
target.Notify(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
arguments.parameters.GetFloat(P("duration")),
|
||||||
|
arguments.options.GetTextBy(P("/channel/channel_name")));
|
||||||
|
_.memory.Free4(title, message, plainTitle, plainMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
preferredName = "notify"
|
||||||
|
}
|
||||||
197
kf_sources/AcediaCore/Classes/ACommandSideEffects.uc
Normal file
197
kf_sources/AcediaCore/Classes/ACommandSideEffects.uc
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ACommandSideEffects extends Command;
|
||||||
|
|
||||||
|
// Maps `UserID` to `ArrayList` with side effects listed for that player last time
|
||||||
|
var private HashTable displayedLists;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
super.Constructor();
|
||||||
|
displayedLists = _.collections.EmptyHashTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
_.memory.Free(displayedLists);
|
||||||
|
displayedLists = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function BuildData(CommandDataBuilder builder) {
|
||||||
|
builder.Group(P("debug"));
|
||||||
|
builder.Summary(P("Displays information about current side effects."));
|
||||||
|
builder.Describe(P("This command allows to display current side effects, optionally filtering"
|
||||||
|
@ "them by specified package names."));
|
||||||
|
builder.OptionalParams();
|
||||||
|
builder.ParamTextList(P("package_names"));
|
||||||
|
|
||||||
|
builder.SubCommand(P("show"));
|
||||||
|
builder.Describe(P("This sub-command is only usable after side effects have been shown"
|
||||||
|
@ "at least once. It takes an index from the last displayed list and displays a verbose"
|
||||||
|
@ "information about it."));
|
||||||
|
builder.ParamInteger(P("side_effect_number"));
|
||||||
|
|
||||||
|
builder.Option(P("verbose"));
|
||||||
|
builder.Describe(P("Display verbose information about each side effect."));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Executed(
|
||||||
|
CallData arguments,
|
||||||
|
EPlayer instigator,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
local UserID playerID;
|
||||||
|
local array<SideEffect> relevantSideEffects;
|
||||||
|
local ArrayList packagesList, storedSideEffectsList;
|
||||||
|
|
||||||
|
playerID = instigator.GetUserID();
|
||||||
|
if (arguments.subCommandName.IsEmpty()) {
|
||||||
|
relevantSideEffects = _.sideEffects.GetAll();
|
||||||
|
packagesList = arguments.parameters.GetArrayList(P("package_names"));
|
||||||
|
FilterSideEffects(/*out*/ relevantSideEffects, packagesList);
|
||||||
|
_.memory.Free(packagesList);
|
||||||
|
DisplaySideEffects(relevantSideEffects, arguments.options.HasKey(P("verbose")));
|
||||||
|
// Store new side effect list
|
||||||
|
storedSideEffectsList = _.collections.NewArrayList(relevantSideEffects);
|
||||||
|
displayedLists.SetItem(playerID, storedSideEffectsList);
|
||||||
|
_.memory.FreeMany(relevantSideEffects);
|
||||||
|
_.memory.Free(storedSideEffectsList);
|
||||||
|
} else {
|
||||||
|
ShowInfoFor(playerID, arguments.parameters.GetInt(P("side_effect_number")));
|
||||||
|
}
|
||||||
|
_.memory.Free(playerID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function FilterSideEffects(out array<SideEffect> sideEffects, ArrayList allowedPackages) {
|
||||||
|
local int i, j;
|
||||||
|
local int packagesLength;
|
||||||
|
local bool matchedPackage;
|
||||||
|
local Text nextSideEffectPackage, nextAllowedPackage;
|
||||||
|
|
||||||
|
if (allowedPackages == none) return;
|
||||||
|
if (allowedPackages.GetLength() <= 0) return;
|
||||||
|
|
||||||
|
packagesLength = allowedPackages.GetLength();
|
||||||
|
while (i < sideEffects.length) {
|
||||||
|
nextSideEffectPackage = sideEffects[i].GetPackage();
|
||||||
|
matchedPackage = false;
|
||||||
|
for (j = 0; j < packagesLength; j += 1) {
|
||||||
|
nextAllowedPackage = allowedPackages.GetText(j);
|
||||||
|
if (nextAllowedPackage.Compare(nextSideEffectPackage, SCASE_INSENSITIVE)) {
|
||||||
|
matchedPackage = true;
|
||||||
|
_.memory.Free(nextAllowedPackage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_.memory.Free(nextAllowedPackage);
|
||||||
|
}
|
||||||
|
if (!matchedPackage) {
|
||||||
|
sideEffects.Remove(i, 1);
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
_.memory.Free(nextSideEffectPackage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function DisplaySideEffects(array<SideEffect> toDisplay, bool verbose) {
|
||||||
|
local int i;
|
||||||
|
local MutableText nextPrefix;
|
||||||
|
|
||||||
|
if (toDisplay.length <= 0) {
|
||||||
|
callerConsole.Write(F("List of side effects is {$TextNeutral empty}."));
|
||||||
|
}
|
||||||
|
for (i = 0; i < toDisplay.length; i += 1) {
|
||||||
|
nextPrefix = _.text.FromIntM(i + 1);
|
||||||
|
nextPrefix.Append(P("."));
|
||||||
|
DisplaySideEffect(toDisplay[i], nextPrefix, verbose);
|
||||||
|
_.memory.Free(nextPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function DisplaySideEffect(SideEffect toDisplay, BaseText prefix, bool verbose) {
|
||||||
|
local Text effectName, effectDescription, effectPackage, effectSource, effectStatus;
|
||||||
|
|
||||||
|
if (toDisplay == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prefix != none) {
|
||||||
|
callerConsole.Write(prefix);
|
||||||
|
callerConsole.Write(P(" "));
|
||||||
|
}
|
||||||
|
effectName = toDisplay.GetName();
|
||||||
|
effectPackage = toDisplay.GetPackage();
|
||||||
|
effectSource = toDisplay.GetSource();
|
||||||
|
effectStatus = toDisplay.GetStatus();
|
||||||
|
callerConsole.UseColor(_.color.TextEmphasis);
|
||||||
|
callerConsole.Write(P("["));
|
||||||
|
callerConsole.Write(effectPackage);
|
||||||
|
callerConsole.Write(P(" \\ "));
|
||||||
|
callerConsole.Write(effectSource);
|
||||||
|
callerConsole.Write(P("] "));
|
||||||
|
callerConsole.ResetColor();
|
||||||
|
callerConsole.Write(effectName);
|
||||||
|
callerConsole.Write(P(" {"));
|
||||||
|
callerConsole.Write(effectStatus);
|
||||||
|
callerConsole.WriteLine(P("}"));
|
||||||
|
if (verbose) {
|
||||||
|
effectDescription = toDisplay.GetDescription();
|
||||||
|
callerConsole.WriteBlock(effectDescription);
|
||||||
|
}
|
||||||
|
_.memory.Free5(effectName, effectDescription, effectPackage, effectSource, effectStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ShowInfoFor(UserID playerID, int sideEffectIndex) {
|
||||||
|
local SideEffect toDisplay;
|
||||||
|
local ArrayList sideEffectList;
|
||||||
|
|
||||||
|
if (playerID == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sideEffectIndex <= 0) {
|
||||||
|
callerConsole.WriteLine(F("Specified side effect index {$TextNegative isn't positive}!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sideEffectList = displayedLists.GetArrayList(playerID);
|
||||||
|
if (sideEffectList == none) {
|
||||||
|
callerConsole.WriteLine(F("{$TextNegative Cannot display} side effect by index without"
|
||||||
|
@ "first listing them. Call {$TextEmphasis sideeffects} command without"
|
||||||
|
@ "{$TextEmphasis show} subcommand first."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sideEffectIndex > sideEffectList.GetLength()) {
|
||||||
|
callerConsole.WriteLine(F("Specified side effect index is {$TextNegative out of bounds}."));
|
||||||
|
_.memory.Free(sideEffectList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Above we checked that `sideEffectIndex` lies within `[0; sideEffectList.GetLength()]` segment
|
||||||
|
// This means that `sideEffectIndex - 1` points at non-`none` value
|
||||||
|
toDisplay = SideEffect(sideEffectList.GetItem(sideEffectIndex - 1));
|
||||||
|
if (!_.sideEffects.IsRegistered(toDisplay)) {
|
||||||
|
callerConsole.UseColorOnce(_.color.TextWarning);
|
||||||
|
callerConsole.WriteLine(P("Selected side effect is no longer active!"));
|
||||||
|
}
|
||||||
|
DisplaySideEffect(toDisplay, none, true);
|
||||||
|
_.memory.Free2(toDisplay, sideEffectList);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
preferredName = "sideeffects"
|
||||||
|
}
|
||||||
220
kf_sources/AcediaCore/Classes/ACommandVote.uc
Normal file
220
kf_sources/AcediaCore/Classes/ACommandVote.uc
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ACommandVote extends Command
|
||||||
|
dependson(CommandAPI)
|
||||||
|
dependson(VotingModel);
|
||||||
|
|
||||||
|
var private CommandDataBuilder dataBuilder;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
ResetVotingInfo();
|
||||||
|
_.commands.OnVotingAdded(self).connect = AddVotingInfo;
|
||||||
|
_.commands.OnVotingRemoved(self).connect = HandleRemovedVoting;
|
||||||
|
_.chat.OnVoiceMessage(self).connect = VoteWithVoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
_.memory.Free(dataBuilder);
|
||||||
|
dataBuilder = none;
|
||||||
|
_.commands.OnVotingAdded(self).Disconnect();
|
||||||
|
_.commands.OnVotingRemoved(self).Disconnect();
|
||||||
|
_.chat.OnVoiceMessage(self).Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function BuildData(CommandDataBuilder builder) {
|
||||||
|
builder.Group(P("core"));
|
||||||
|
builder.Summary(P("Allows players to initiate any available voting."
|
||||||
|
@ "Voting options themselves are specified as sub-commands."));
|
||||||
|
builder.Describe(P("Default command simply displaces information about current vote."));
|
||||||
|
|
||||||
|
dataBuilder.SubCommand(P("yes"));
|
||||||
|
builder.Describe(P("Vote `yes` on the current vote."));
|
||||||
|
dataBuilder.SubCommand(P("no"));
|
||||||
|
builder.Describe(P("Vote `no` on the current vote."));
|
||||||
|
|
||||||
|
builder.Option(P("force"));
|
||||||
|
builder.Describe(P("Tries to force voting to end immediately with the desired result."));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Executed(
|
||||||
|
CallData arguments,
|
||||||
|
EPlayer instigator,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
local bool forcingVoting;
|
||||||
|
local VotingModel.ForceEndingType forceType;
|
||||||
|
local Voting currentVoting;
|
||||||
|
|
||||||
|
forcingVoting = arguments.options.HasKey(P("force"));
|
||||||
|
currentVoting = _.commands.GetCurrentVoting();
|
||||||
|
if (arguments.subCommandName.IsEmpty()) {
|
||||||
|
DisplayInfoAboutVoting(instigator, currentVoting);
|
||||||
|
} else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) {
|
||||||
|
CastVote(currentVoting, instigator, true);
|
||||||
|
forceType = FET_Success;
|
||||||
|
} else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) {
|
||||||
|
CastVote(currentVoting, instigator, false);
|
||||||
|
forceType = FET_Failure;
|
||||||
|
} else if (StartVoting(arguments, currentVoting, instigator)) {
|
||||||
|
_.memory.Free(currentVoting);
|
||||||
|
currentVoting = _.commands.GetCurrentVoting();
|
||||||
|
forceType = FET_Success;
|
||||||
|
} else {
|
||||||
|
forcingVoting = false;
|
||||||
|
}
|
||||||
|
if (currentVoting != none && !currentVoting.HasEnded() && forcingVoting) {
|
||||||
|
if (currentVoting.ForceEnding(instigator, forceType) == FEO_Forbidden) {
|
||||||
|
callerConsole
|
||||||
|
.WriteLine(F("You {$TextNegative aren't allowed} to forcibly end current voting"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(currentVoting);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function VoteWithVoice(EPlayer sender, ChatApi.BuiltInVoiceMessage message) {
|
||||||
|
local Voting currentVoting;
|
||||||
|
|
||||||
|
currentVoting = _.commands.GetCurrentVoting();
|
||||||
|
if (message == BIVM_AckYes) {
|
||||||
|
CastVote(currentVoting, sender, true);
|
||||||
|
}
|
||||||
|
if (message == BIVM_AckNo) {
|
||||||
|
CastVote(currentVoting, sender, false);
|
||||||
|
}
|
||||||
|
_.memory.Free(currentVoting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds sub-command information about given voting with a given name.
|
||||||
|
public final function AddVotingInfo(class<Voting> processClass, Text processName) {
|
||||||
|
if (processName == none) return;
|
||||||
|
if (processClass == none) return;
|
||||||
|
if (dataBuilder == none) return;
|
||||||
|
|
||||||
|
dataBuilder.SubCommand(processName);
|
||||||
|
processClass.static.AddInfo(dataBuilder);
|
||||||
|
commandData = dataBuilder.BorrowData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function HandleRemovedVoting(class<Voting> votingClass) {
|
||||||
|
local int i;
|
||||||
|
local array<Text> votingsNames;
|
||||||
|
|
||||||
|
ResetVotingInfo();
|
||||||
|
// Rebuild the whole voting data
|
||||||
|
votingsNames = _.commands.GetAllVotingsNames();
|
||||||
|
for (i = 0; i < votingsNames.length; i += 1) {
|
||||||
|
AddVotingInfo(_.commands.GetVotingClass(votingsNames[i]), votingsNames[i]);
|
||||||
|
}
|
||||||
|
_.memory.FreeMany(votingsNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all sub-command information added from [`Voting`]s.
|
||||||
|
public final function ResetVotingInfo() {
|
||||||
|
_.memory.Free(dataBuilder);
|
||||||
|
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
|
||||||
|
BuildData(dataBuilder);
|
||||||
|
commandData = dataBuilder.BorrowData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function DisplayInfoAboutVoting(EPlayer instigator, Voting currentVoting) {
|
||||||
|
if (currentVoting == none) {
|
||||||
|
callerConsole.WriteLine(P("No voting is active right now."));
|
||||||
|
} else {
|
||||||
|
currentVoting.PrintVotingInfoFor(instigator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function CastVote(Voting currentVoting, EPlayer voter, bool voteForSuccess) {
|
||||||
|
if (currentVoting != none) {
|
||||||
|
currentVoting.CastVote(voter, voteForSuccess);
|
||||||
|
} else {
|
||||||
|
callerConsole.UseColor(_.color.TextWarning).WriteLine(P("No voting is active right now."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes all arguments aren't `none`.
|
||||||
|
private final function bool StartVoting(
|
||||||
|
CallData arguments,
|
||||||
|
Voting currentVoting,
|
||||||
|
EPlayer instigator
|
||||||
|
) {
|
||||||
|
local Voting newVoting;
|
||||||
|
local User callerUser;
|
||||||
|
local CommandAPI.VotingConfigInfo pair;
|
||||||
|
local CommandAPI.StartVotingResult result;
|
||||||
|
|
||||||
|
callerUser = instigator.GetIdentity();
|
||||||
|
pair = _.commands.ResolveVotingForUser(arguments.subCommandName, callerUser);
|
||||||
|
_.memory.Free(callerUser);
|
||||||
|
if (pair.votingClass == none) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextFailure)
|
||||||
|
.Write(P("Unknown voting option \""))
|
||||||
|
.Write(arguments.subCommandName)
|
||||||
|
.WriteLine(P("\""));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pair.usageForbidden) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextFailure)
|
||||||
|
.Write(P("You aren't allowed to start \""))
|
||||||
|
.Write(arguments.subCommandName)
|
||||||
|
.WriteLine(P("\" voting"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
result = _.commands.StartVoting(pair, arguments.parameters);
|
||||||
|
Log("Result:" @ result);
|
||||||
|
// Handle errors.
|
||||||
|
// `SVR_UnknownVoting` is impossible, since we've already checked that
|
||||||
|
// `pair.votingClass != none`)
|
||||||
|
if (result == SVR_AlreadyInProgress) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextWarning)
|
||||||
|
.WriteLine(P("Another voting is already in progress!"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result == SVR_NoVoters) {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextWarning)
|
||||||
|
.WriteLine(P("There are no players eligible for that voting."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Cast a vote from instigator
|
||||||
|
newVoting = _.commands.GetCurrentVoting();
|
||||||
|
if (newVoting != none) {
|
||||||
|
newVoting.CastVote(instigator, true);
|
||||||
|
} else {
|
||||||
|
callerConsole
|
||||||
|
.UseColor(_.color.TextFailure)
|
||||||
|
.WriteLine(P("Voting should be available, but it isn't."
|
||||||
|
@ "This is unexpected, something broke terribly."));
|
||||||
|
_.memory.Free(newVoting);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_.memory.Free(newVoting);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
preferredName = "vote"
|
||||||
|
}
|
||||||
116
kf_sources/AcediaCore/Classes/ASquad.uc
Normal file
116
kf_sources/AcediaCore/Classes/ASquad.uc
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Object that is meant to describe a squad of zeds for Killing Floor spawning
|
||||||
|
* system. Zeds can be either added individually or through native squad
|
||||||
|
* definitions "3A1B2D1G1H".
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ASquad extends AcediaObject;
|
||||||
|
|
||||||
|
struct ZedCountPair
|
||||||
|
{
|
||||||
|
var Text template;
|
||||||
|
var int count;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets caller squad, removing all zeds inside.
|
||||||
|
*/
|
||||||
|
public function Reset();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes amount of zeds with given template inside the caller squad.
|
||||||
|
*
|
||||||
|
* @param template Template for which to change stored count.
|
||||||
|
* @param delta By how much to change zed count in squad. Negative
|
||||||
|
* values are allowed.
|
||||||
|
*/
|
||||||
|
public function ChangeCount(BaseText template, int delta);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes amount of zeds with given template inside the caller squad.
|
||||||
|
*
|
||||||
|
* @param template Template for which to change stored count.
|
||||||
|
* @param delta By how much to change zed count in squad. Negative
|
||||||
|
* values are allowed.
|
||||||
|
*/
|
||||||
|
public final function ChangeCount_S(string template, int delta)
|
||||||
|
{
|
||||||
|
local MutableText wrapper;
|
||||||
|
|
||||||
|
wrapper = _.text.FromStringM(template);
|
||||||
|
ChangeCount(wrapper, delta);
|
||||||
|
wrapper.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds zeds into the squad from the text definition format.
|
||||||
|
*
|
||||||
|
* For example "3clot5crawer1fp" will add 3 clots, 5 crawlers and 1 fleshpound
|
||||||
|
* into the squad (names are resolved via entity aliases).
|
||||||
|
* Counts inside a definition cannot be negative.
|
||||||
|
*
|
||||||
|
* @param definition Text definition of the addition to squad.
|
||||||
|
*/
|
||||||
|
public function AddFromDefinition(BaseText definition);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds zeds into the squad from the text definition format.
|
||||||
|
*
|
||||||
|
* For example "3clot5crawer1fp" will add 3 clots, 5 crawlers and 1 fleshpound
|
||||||
|
* into the squad (names are resolved via entity aliases).
|
||||||
|
* Counts inside a definition cannot be negative.
|
||||||
|
*
|
||||||
|
* @param definition Text definition of the addition to squad.
|
||||||
|
*/
|
||||||
|
public final function AddFromDefinition_S(string definition)
|
||||||
|
{
|
||||||
|
local MutableText wrapper;
|
||||||
|
|
||||||
|
wrapper = _.text.FromStringM(definition);
|
||||||
|
AddFromDefinition(wrapper);
|
||||||
|
wrapper.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of zeds inside caller squad as an array of zed template and
|
||||||
|
* the count of that zed in a squad.
|
||||||
|
*
|
||||||
|
* @return Array of pairs of template and corresponding zed count inside
|
||||||
|
* a squad. Guaranteed that there cannot be two array elements with
|
||||||
|
* the same template.
|
||||||
|
*/
|
||||||
|
public function array<ZedCountPair> GetZedList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns amount of zeds of the given template inside the caller squad.
|
||||||
|
*
|
||||||
|
* @return Current amount of zeds of the given template inside the caller
|
||||||
|
* squad.
|
||||||
|
*/
|
||||||
|
public function int GetZedCount(BaseText template);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current total zed count inside a squad.
|
||||||
|
*
|
||||||
|
* @return Current total zed count inside a caller squad.
|
||||||
|
*/
|
||||||
|
public function int GetTotalZedCount();
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
101
kf_sources/AcediaCore/Classes/AWorldComponent.uc
Normal file
101
kf_sources/AcediaCore/Classes/AWorldComponent.uc
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Subset of functionality for dealing with basic game world interactions:
|
||||||
|
* searching for entities, tracing, etc..
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AWorldComponent extends AcediaObject
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traces world for entities starting from `start` point and continuing into
|
||||||
|
* the direction `direction` for a "far distance". What is considered
|
||||||
|
* a "far distance" depends on the implementation (can potentially be infinite
|
||||||
|
* distance).
|
||||||
|
*
|
||||||
|
* @param start Point from which to start tracing.
|
||||||
|
* @param direction Direction alongside which to trace.
|
||||||
|
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
|
||||||
|
* for traced entities. Iteration is done in order from the entity closest
|
||||||
|
* to `start` point.
|
||||||
|
*/
|
||||||
|
public function TracingIterator Trace(Vector start, Rotator direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traces world for entities starting from `start` point and until `end` point.
|
||||||
|
*
|
||||||
|
* @param start Point from which to start tracing.
|
||||||
|
* @param end Point at which to stop tracing.
|
||||||
|
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
|
||||||
|
* for traced entities. Iteration is done in order from the entity closest
|
||||||
|
* to `start` point.
|
||||||
|
*/
|
||||||
|
public function TracingIterator TraceBetween(Vector start, Vector end);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traces world for entities starting from the `player`'s camera position and
|
||||||
|
* along the direction of his sight.
|
||||||
|
*
|
||||||
|
* This method works for both players with and without pawns. For player with
|
||||||
|
* a pawn it produce identical results to `TraceSight()` method.
|
||||||
|
*
|
||||||
|
* @param player Player alongside whos sight method is supposed to trace.
|
||||||
|
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
|
||||||
|
* for traced entities. Iteration is done in order from the entity closest
|
||||||
|
* to `player`'s camera location.
|
||||||
|
*/
|
||||||
|
public function TracingIterator TracePlayerSight(EPlayer player);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traces world for entities starting from the `pawn`'s camera position and
|
||||||
|
* along the direction of his sight.
|
||||||
|
*
|
||||||
|
* @param pawn Pawn alongside whos sight method is supposed to trace.
|
||||||
|
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
|
||||||
|
* for traced entities. Iteration is done in order from the entity closest
|
||||||
|
* to `pawn`'s eyes location.
|
||||||
|
*/
|
||||||
|
public function TracingIterator TraceSight(EPawn pawn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawns a new `EPlaceable` based on the given `template` at a given location
|
||||||
|
* `location`, facing it into the given direction `direction`.
|
||||||
|
*
|
||||||
|
* @param template Describes entity (supporting `EPlaceable` interface) to
|
||||||
|
* spawn into the world.
|
||||||
|
* @param location At what location to spawn that entity.
|
||||||
|
* @param direction In what direction spawned entity must face.
|
||||||
|
* @return `EPlaceable` interface for the spawned entity. `none` if spawning it
|
||||||
|
* has failed. If method returns interface to a non-existent entity, it
|
||||||
|
* means that entity has successfully spawned, but then something handled
|
||||||
|
* its spawning and destroyed it.
|
||||||
|
*/
|
||||||
|
public function EPlaceable Spawn(
|
||||||
|
BaseText template,
|
||||||
|
optional Vector location,
|
||||||
|
optional Rotator direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns iterator for going through all entities in the game world.
|
||||||
|
*
|
||||||
|
* @return `EntityIterator` that will iterate through world entities.
|
||||||
|
*/
|
||||||
|
public function EntityIterator Entities();
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
49
kf_sources/AcediaCore/Classes/AcediaAdapter.uc
Normal file
49
kf_sources/AcediaCore/Classes/AcediaAdapter.uc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Base class for describing what API Acedia should load into its client- and
|
||||||
|
* server- `...Global`s objects.
|
||||||
|
* Copyright 2022-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AcediaAdapter extends AcediaObject
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # `AcediaAdapter`
|
||||||
|
*
|
||||||
|
* Acedia provides a set of APIs through `...Global`s objects
|
||||||
|
* `_` (for everything), `_server` (for servers) and `_client` (for clients).
|
||||||
|
* The functionality in common API set `_` is hardcoded, but Acedia allows
|
||||||
|
* users to provide their own implementation of `_server`'s and/or `_client`'s
|
||||||
|
* functionality to extend them, offer compatibility with other mods, etc..
|
||||||
|
* This replacement is done through `AcediaAdapter` classes that serve as
|
||||||
|
* a collection of API classes to be used in `_server` or `_client`.
|
||||||
|
* All one needs to do to use a different set of server/client APIs is to
|
||||||
|
* specify desired `AcediaAdapter` before loading server/client core.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var public const class<TimeAPI> /* surprise! */ timeAPIClass;
|
||||||
|
/*And
|
||||||
|
like that!*//*
|
||||||
|
and gain!*/
|
||||||
|
var public/*A*//*B*/ const class<DBAPI> dbAPIClass;
|
||||||
|
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}/*
|
||||||
|
|
||||||
|
|
||||||
365
kf_sources/AcediaCore/Classes/AcediaConfig.uc
Normal file
365
kf_sources/AcediaCore/Classes/AcediaConfig.uc
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
/**
|
||||||
|
* Acedia makes extensive use of `perobjectconfig` for storing information
|
||||||
|
* in ini config files. Their data is usually stored in ini config files as:
|
||||||
|
* "[<config_object_name> <data_class_name>]".
|
||||||
|
* Making a child class for `AcediaConfig` defines "<data_class_name>" and
|
||||||
|
* contents of appropriate section. Then any such class can have multiple
|
||||||
|
* records with different "<config_object_name>" values. The only requirement
|
||||||
|
* is that "<config_object_name>" must be considered *valid* by
|
||||||
|
* `BaseText.IsValidName()` standards.
|
||||||
|
* Copyright 2021-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AcediaConfig extends AcediaObject
|
||||||
|
dependson(HashTable)
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # `AcediaConfig`
|
||||||
|
*
|
||||||
|
* This class deals with several issues related to use of such objects,
|
||||||
|
* stemming from the lack of documentation:
|
||||||
|
*
|
||||||
|
* 1. Not all `Object` names are a usable.
|
||||||
|
* [Beyond Unreal wiki](
|
||||||
|
* https://wiki.beyondunreal.com/Legacy:PerObjectConfig)
|
||||||
|
* lists a couple of limitations: whitespace and ']' character.
|
||||||
|
* However there are more, including '.'.
|
||||||
|
* We limit available character set to ASCII latin letters, digits and
|
||||||
|
* the dot ('.') / underscore ('_'). Dot is a forbidden character, but it
|
||||||
|
* is often used in class names and, therefore, was added via workaround:
|
||||||
|
* it is automatically converted into colon ':' character to allow its
|
||||||
|
* storage inside ini config files. It also will not lead to the name
|
||||||
|
* conflicts, since colon is a forbidden character for `AcediaConfig`.
|
||||||
|
* Unreal Engine also doesn't handle long object names very well:
|
||||||
|
* if it considers "[<config_object_name> <data_class_name>]" too long, it
|
||||||
|
* might cut some of the trailing letters of what is between brackets '['
|
||||||
|
* and ']' basically mangling class' name. Since such config header seems
|
||||||
|
* to be able to contain at least 100 character (at least in our tests),
|
||||||
|
* we deal with that issue by limiting name to 50 characters at most.
|
||||||
|
* Depending on the class' name it might still cause problems, so don't
|
||||||
|
* make them too long.
|
||||||
|
* 2. Behavior of loading `perobjectconfig`-objects a second time is wonky and
|
||||||
|
* is fixed by `AcediaConfig`: it provides concrete behavior guarantees
|
||||||
|
* for all of its config object-managing methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// All config objects of a particular class only get loaded once
|
||||||
|
// per session (unless new one is created) and then accessed through this
|
||||||
|
// collection.
|
||||||
|
// This array stores `AcediaConfig` values with `Text` keys in
|
||||||
|
// a case-insensitive way (by converting keys into lower case).
|
||||||
|
// In case it has a `none` value stored under some key - it means that value
|
||||||
|
// was detected in config, but not yet loaded.
|
||||||
|
// Only its default value is ever used.
|
||||||
|
var private HashTable existingConfigs;
|
||||||
|
// TODO: comment and add static cleanup
|
||||||
|
var private array<AcediaConfig> clearQueue;
|
||||||
|
var private bool syncScheduled;
|
||||||
|
|
||||||
|
// Stores name of the config where settings are to be stored.
|
||||||
|
// Must correspond to value in `config(...)` modifier in class definition.
|
||||||
|
var protected const string configName;
|
||||||
|
|
||||||
|
// Set this to `true` if you implement `ToData()` / `FromData()` pair of
|
||||||
|
// methods.
|
||||||
|
// This will tell Acedia that your config can be converted into
|
||||||
|
// JSON-compatible types.
|
||||||
|
var public const bool supportsDataConversion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These methods must be overloaded to store and load all the config
|
||||||
|
* variables inside an `HashTable` collection. How exactly to store
|
||||||
|
* them is up to each config class to decide, as long as it allows conversion
|
||||||
|
* into JSON (see `JSONAPI.IsCompatible()` for details).
|
||||||
|
* Note that `HashTable` reference `FromData()` receives is
|
||||||
|
* not necessarily the same one your `ToData()` method returns - any particular
|
||||||
|
* value boxes can be replaced with value references and vice versa.
|
||||||
|
* NOTE: DO NOT use `P()`, `C()`, `F()` or `T()` methods for keys or
|
||||||
|
* values in collections you return. All keys and values will be automatically
|
||||||
|
* deallocated when necessary, so these methods for creating `Text` values are
|
||||||
|
* not suitable.
|
||||||
|
*/
|
||||||
|
protected function HashTable ToData() { return none; }
|
||||||
|
protected function FromData(HashTable source) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method must be overloaded to setup default values for all config
|
||||||
|
* variables. You should use it instead of the `defaultproperties` block.
|
||||||
|
*/
|
||||||
|
protected function DefaultIt() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This reads all of the `AcediaConfig`'s settings objects into internal
|
||||||
|
* storage. Must be called before any other methods. Actual loading might be
|
||||||
|
* postponed until a particular config is needed.
|
||||||
|
*/
|
||||||
|
public static function Initialize()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text nextName, lowerName;
|
||||||
|
local array<string> names;
|
||||||
|
if (default.existingConfigs != none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default.existingConfigs = __().collections.EmptyHashTable();
|
||||||
|
names = GetPerObjectNames( default.configName, string(default.class.name),
|
||||||
|
MaxInt);
|
||||||
|
for (i = 0; i < names.length; i += 1)
|
||||||
|
{
|
||||||
|
if (names[i] == "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextName = __().text.FromString(NameToActualVersion(names[i]));
|
||||||
|
if (nextName.IsValidName())
|
||||||
|
{
|
||||||
|
lowerName = nextName.LowerCopy();
|
||||||
|
default.existingConfigs.SetItem(lowerName, none);
|
||||||
|
lowerName.FreeSelf();
|
||||||
|
}
|
||||||
|
nextName.FreeSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function string NameToStorageVersion(string configObjectName)
|
||||||
|
{
|
||||||
|
return Repl(configObjectName, ".", ":");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function string NameToActualVersion(string configObjectName)
|
||||||
|
{
|
||||||
|
return Repl(configObjectName, ":", ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a brand new config object with a given name.
|
||||||
|
*
|
||||||
|
* Fails if config object with that name already exists.
|
||||||
|
* Config name must be considered *valid* by `BaseText.IsValidName()`
|
||||||
|
* standards.
|
||||||
|
*
|
||||||
|
* Always writes new config inside the ini file on disk.
|
||||||
|
*
|
||||||
|
* @param name Name of the new config object.
|
||||||
|
* Must be considered *valid* by `BaseText.IsValidName()`
|
||||||
|
* standards, otherwise method will fail.
|
||||||
|
* @return `false` iff config object name `name` already exists
|
||||||
|
* or `name` is invalid for config object.
|
||||||
|
*/
|
||||||
|
public final static function bool NewConfig(BaseText name)
|
||||||
|
{
|
||||||
|
local AcediaConfig newConfig;
|
||||||
|
if (name == none) return false;
|
||||||
|
if (!name.IsValidName()) return false;
|
||||||
|
if (default.existingConfigs == none) return false;
|
||||||
|
|
||||||
|
name = name.LowerCopy();
|
||||||
|
if (default.existingConfigs.HasKey(name))
|
||||||
|
{
|
||||||
|
name.FreeSelf();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
newConfig =
|
||||||
|
new(none, NameToStorageVersion(name.ToString())) default.class;
|
||||||
|
newConfig._ = __();
|
||||||
|
newConfig.DefaultIt();
|
||||||
|
newConfig.SyncSave();
|
||||||
|
default.existingConfigs.SetItem(name, newConfig);
|
||||||
|
name.FreeSelf();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a config object with a given name exists.
|
||||||
|
*
|
||||||
|
* @param name Name of the new config object.
|
||||||
|
* Must be considered *valid* by `BaseText.IsValidName()` standards.
|
||||||
|
* @return `true` iff new config object was created.
|
||||||
|
*/
|
||||||
|
public final static function bool Exists(BaseText name)
|
||||||
|
{
|
||||||
|
local bool result;
|
||||||
|
if (name == none) return false;
|
||||||
|
if (!name.IsValidName()) return false;
|
||||||
|
if (default.existingConfigs == none) return false;
|
||||||
|
|
||||||
|
name = name.LowerCopy();
|
||||||
|
result = default.existingConfigs.HasKey(name);
|
||||||
|
name.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes config object with a given name.
|
||||||
|
*
|
||||||
|
* If given config object exists, this method cannot fail.
|
||||||
|
* `Exists()` is guaranteed to return `false` after this method call.
|
||||||
|
*
|
||||||
|
* Always removes any present config entries from ini files.
|
||||||
|
*
|
||||||
|
* @param name Name of the config object to delete.
|
||||||
|
*/
|
||||||
|
public final static function DeleteConfig(BaseText name)
|
||||||
|
{
|
||||||
|
local AcediaConfig value;
|
||||||
|
|
||||||
|
if (name == none) return;
|
||||||
|
if (default.existingConfigs == none) return;
|
||||||
|
|
||||||
|
name = name.LowerCopy();
|
||||||
|
value = AcediaConfig(default.existingConfigs.TakeItem(name));
|
||||||
|
if (value != none)
|
||||||
|
{
|
||||||
|
__().scheduler.RequestDiskAccess(default.existingConfigs).connect =
|
||||||
|
HandleClearQueue;
|
||||||
|
default.clearQueue[default.clearQueue.length] = value;
|
||||||
|
}
|
||||||
|
__().memory.Free(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array containing names of all available config objects.
|
||||||
|
*
|
||||||
|
* @return Array with names of all available config objects.
|
||||||
|
* Guaranteed to not contain `none` values.
|
||||||
|
*/
|
||||||
|
public static function array<Text> AvailableConfigs()
|
||||||
|
{
|
||||||
|
local array<Text> emptyResult;
|
||||||
|
if (default.existingConfigs != none) {
|
||||||
|
return default.existingConfigs.GetTextKeys();
|
||||||
|
}
|
||||||
|
return emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `AcediaConfig` of caller class with name `name`.
|
||||||
|
*
|
||||||
|
* @param name Name of the config object, whos settings data is to
|
||||||
|
* be loaded. Must be considered *valid* by `BaseText.IsValidName()`
|
||||||
|
* standards.
|
||||||
|
* @return `AcediaConfig` of caller class with name `name`.
|
||||||
|
* Returns `none` if config with given name doesn't exist.
|
||||||
|
*/
|
||||||
|
public final static function AcediaConfig GetConfigInstance(BaseText name)
|
||||||
|
{
|
||||||
|
local HashTable.Entry configEntry;
|
||||||
|
|
||||||
|
if (name == none) return none;
|
||||||
|
if (!name.IsValidName()) return none;
|
||||||
|
if (default.existingConfigs == none) return none;
|
||||||
|
|
||||||
|
name = name.LowerCopy();
|
||||||
|
configEntry = default.existingConfigs.GetEntry(name);
|
||||||
|
if (configEntry.value == none && configEntry.key != none)
|
||||||
|
{
|
||||||
|
configEntry.value =
|
||||||
|
new(none, NameToStorageVersion(name.ToString())) default.class;
|
||||||
|
configEntry.value._ = __();
|
||||||
|
default.existingConfigs.SetItem(configEntry.key, configEntry.value);
|
||||||
|
}
|
||||||
|
__().memory.Free(name);
|
||||||
|
// We return value, so do not deallocate it
|
||||||
|
__().memory.Free(configEntry.key);
|
||||||
|
return AcediaConfig(configEntry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads Acedia's representation of settings data of a particular config
|
||||||
|
* object, given by the `name`.
|
||||||
|
*
|
||||||
|
* Should only be called if caller class has `supportsDataConversion` set to
|
||||||
|
* `true`.
|
||||||
|
*
|
||||||
|
* @param name Name of the config object, whos data is to be loaded.
|
||||||
|
* Name must be considered *valid* by `BaseText.IsValidName()` standards.
|
||||||
|
* @return Data of a particular config object, given by the `name`.
|
||||||
|
* Expected to be in format that allows for JSON serialization
|
||||||
|
* (see `JSONAPI.IsCompatible()` for details).
|
||||||
|
* Returns `none` if config with specified name is missing (or their class
|
||||||
|
* was not yet initialized: see `self.Initialize()` method).
|
||||||
|
*/
|
||||||
|
public final static function HashTable LoadData(BaseText name)
|
||||||
|
{
|
||||||
|
local HashTable result;
|
||||||
|
local AcediaConfig requiredConfig;
|
||||||
|
requiredConfig = GetConfigInstance(name);
|
||||||
|
if (requiredConfig != none) {
|
||||||
|
result = requiredConfig.ToData();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves Acedia's representation of settings data (`data`) for a particular
|
||||||
|
* config object, given by the `name`.
|
||||||
|
*
|
||||||
|
* Should only be called if caller class has `supportsDataConversion` set to
|
||||||
|
* `true`.
|
||||||
|
*
|
||||||
|
* @param name Name of the config object, whos data is to be modified.
|
||||||
|
* Name must be considered *valid* by `BaseText.IsValidName()` standards.
|
||||||
|
* @param data New data for config variables. Expected to be in format that
|
||||||
|
* allows for JSON deserialization (see `JSONAPI.IsCompatible()` for
|
||||||
|
* details).
|
||||||
|
*/
|
||||||
|
public final static function SaveData(BaseText name, HashTable data)
|
||||||
|
{
|
||||||
|
local AcediaConfig requiredConfig;
|
||||||
|
requiredConfig = GetConfigInstance(name);
|
||||||
|
if (requiredConfig != none)
|
||||||
|
{
|
||||||
|
requiredConfig.FromData(data);
|
||||||
|
requiredConfig.SyncSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes current in-memory data by saving it onto the disk (into
|
||||||
|
* the config file). Can be performed asynchronously (actual saving can be
|
||||||
|
* postponed for performance reasons).
|
||||||
|
*/
|
||||||
|
public final function SyncSave()
|
||||||
|
{
|
||||||
|
if (syncScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncScheduled = true;
|
||||||
|
__().scheduler.RequestDiskAccess(default.existingConfigs).connect = DoSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does actual saving
|
||||||
|
private final function DoSync()
|
||||||
|
{
|
||||||
|
syncScheduled = false;
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static function HandleClearQueue()
|
||||||
|
{
|
||||||
|
if (default.clearQueue.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default.clearQueue[0].ClearConfig();
|
||||||
|
default.clearQueue.Remove(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
supportsDataConversion = false
|
||||||
|
usesObjectPool = false
|
||||||
|
}
|
||||||
518
kf_sources/AcediaCore/Classes/AcediaEnvironment.uc
Normal file
518
kf_sources/AcediaCore/Classes/AcediaEnvironment.uc
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2022-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AcediaEnvironment extends AcediaObject
|
||||||
|
config(AcediaSystem);
|
||||||
|
|
||||||
|
//! API for management of running [`Feature`]s and loaded packages.
|
||||||
|
//!
|
||||||
|
//! Instance of this class will be used by Acedia to manage resources available
|
||||||
|
//! from different packages like [`Feature`]s and such other etc..
|
||||||
|
//! This is mostly necessary to implement Acedia loader (and, possibly,
|
||||||
|
//! its alternatives) that would load available packages and enable [`Feature`]s
|
||||||
|
//! admin wants to be enabled.
|
||||||
|
|
||||||
|
/// Flag indicating whether Acedia has already undergone its shutdown process.
|
||||||
|
var private bool acediaShutDown;
|
||||||
|
|
||||||
|
/// Global data that can be changed by any active mods, allowing limited
|
||||||
|
/// interaction between them.
|
||||||
|
var private HashTable globalData;
|
||||||
|
|
||||||
|
/// Collection of all registered package manifests, representing different sets
|
||||||
|
/// of features and resources.
|
||||||
|
var private array< class<_manifest> > availablePackages;
|
||||||
|
|
||||||
|
/// List of all feature classes available from currently registered packages.
|
||||||
|
var private array< class<Feature> > availableFeatures;
|
||||||
|
/// Instances of features that are currently enabled and active within
|
||||||
|
/// this environment.
|
||||||
|
var private array<Feature> enabledFeatures;
|
||||||
|
var private array<int> enabledFeaturesLifeVersions;
|
||||||
|
|
||||||
|
/// Suffix appended to package names when searching for their manifest classes.
|
||||||
|
var private string manifestSuffix;
|
||||||
|
|
||||||
|
/// Indicates if the system should run in debug mode, enabling extra
|
||||||
|
/// development-related functionality.
|
||||||
|
var private const config bool debugMode;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered;
|
||||||
|
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled;
|
||||||
|
var private LoggerAPI.Definition warnFeatureAlreadyEnabled;
|
||||||
|
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled, errNoGlobals;
|
||||||
|
|
||||||
|
var private SimpleSignal onShutdownSignal;
|
||||||
|
var private SimpleSignal onShutdownSystemSignal;
|
||||||
|
var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal;
|
||||||
|
var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
globalData = _.collections.EmptyHashTable();
|
||||||
|
// Always register our core package
|
||||||
|
RegisterPackage_S("AcediaCore");
|
||||||
|
onShutdownSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
|
||||||
|
onShutdownSystemSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
|
||||||
|
onFeatureEnabledSignal = Environment_FeatureEnabled_Signal(
|
||||||
|
_.memory.Allocate(class'Environment_FeatureEnabled_Signal'));
|
||||||
|
onFeatureDisabledSignal = Environment_FeatureDisabled_Signal(
|
||||||
|
_.memory.Allocate(class'Environment_FeatureDisabled_Signal'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
_.memory.Free(globalData);
|
||||||
|
_.memory.Free(onShutdownSignal);
|
||||||
|
_.memory.Free(onShutdownSystemSignal);
|
||||||
|
_.memory.Free(onFeatureEnabledSignal);
|
||||||
|
_.memory.Free(onFeatureDisabledSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal that will be emitted right before Acedia shuts down.
|
||||||
|
///
|
||||||
|
/// At the point of emission all APIs should still exist and function.
|
||||||
|
///
|
||||||
|
/// # Signature
|
||||||
|
///
|
||||||
|
/// void <slot>()
|
||||||
|
public final /*signal*/ function SimpleSlot OnShutDown(AcediaObject receiver) {
|
||||||
|
return SimpleSlot(onShutdownSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal that will be emitted during Acedia shut down.
|
||||||
|
///
|
||||||
|
/// System API will use it to clean up after themselves, so one shouldn't rely
|
||||||
|
/// on using them.
|
||||||
|
///
|
||||||
|
/// There is no reason to use this signal unless you're reimplementing one of
|
||||||
|
/// the APIs.
|
||||||
|
/// Otherwise you probably want to use [`AcediaEnvironment::OnShutDown()`]
|
||||||
|
/// signal instead.
|
||||||
|
///
|
||||||
|
/// # Signature
|
||||||
|
///
|
||||||
|
/// void <slot>()
|
||||||
|
public final /*signal*/ function SimpleSlot OnShutDownSystem(AcediaObject receiver) {
|
||||||
|
return SimpleSlot(onShutdownSystemSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal that will be emitted right after a new [`Feature`] is enabled and its
|
||||||
|
/// [`Feature::OnEnabled()`] method was called.
|
||||||
|
///
|
||||||
|
/// # Signature
|
||||||
|
///
|
||||||
|
/// void <slot>(Feature enabledFeature)
|
||||||
|
///
|
||||||
|
/// [`enabledFeature`] argument is an instance of [`Feature`] that was just
|
||||||
|
/// enabled.
|
||||||
|
public final /*signal*/ function Environment_FeatureEnabled_Slot OnFeatureEnabled(
|
||||||
|
AcediaObject receiver
|
||||||
|
) {
|
||||||
|
return Environment_FeatureEnabled_Slot(onFeatureEnabledSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal that will be emitted right after when a [`Feature`] is disabled
|
||||||
|
/// and its [`Feature::OnDisabled()`] method was called.
|
||||||
|
///
|
||||||
|
/// # Signature
|
||||||
|
///
|
||||||
|
/// void <slot>(class<Feature> disabledFeatureClass)
|
||||||
|
///
|
||||||
|
/// Argument [`disabledFeatureClass`] describes class of the [`Feature`]
|
||||||
|
/// instance that was just disabled.
|
||||||
|
public final /*signal*/ function Environment_FeatureDisabled_Slot OnFeatureDisabled(
|
||||||
|
AcediaObject receiver
|
||||||
|
) {
|
||||||
|
return Environment_FeatureDisabled_Slot(onFeatureDisabledSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shuts AcediaCore down, performing all the necessary clean up.
|
||||||
|
public final function Shutdown() {
|
||||||
|
local LevelCore core;
|
||||||
|
|
||||||
|
if (acediaShutDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DisableAllFeatures();
|
||||||
|
onShutdownSignal.Emit();
|
||||||
|
onShutdownSystemSignal.Emit();
|
||||||
|
core = class'ServerLevelCore'.static.GetInstance();
|
||||||
|
if (core != none) {
|
||||||
|
core.Destroy();
|
||||||
|
}
|
||||||
|
core = class'ClientLevelCore'.static.GetInstance();
|
||||||
|
if (core != none) {
|
||||||
|
core.Destroy();
|
||||||
|
}
|
||||||
|
_.DropCoreAPI();
|
||||||
|
acediaShutDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers an Acedia package with a given name.
|
||||||
|
///
|
||||||
|
/// Returns `true` if package was successfully registered, `false` if it either
|
||||||
|
/// does not exist, was already registered or [`packageName`] is `none`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will log an error if the package has failed to get registered (it is either
|
||||||
|
/// missing or not an Acedia package).
|
||||||
|
public final function bool RegisterPackage(BaseText packageName) {
|
||||||
|
local class<_manifest> manifestClass;
|
||||||
|
|
||||||
|
if (packageName == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy());
|
||||||
|
manifestClass = class<_manifest>(DynamicLoadObject(
|
||||||
|
packageName.ToString() $ manifestSuffix, class'Class', true));
|
||||||
|
if (manifestClass == none) {
|
||||||
|
_.logger.Auto(errNotRegistered).Arg(packageName.Copy());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (IsManifestRegistered(manifestClass)) {
|
||||||
|
_.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
availablePackages[availablePackages.length] = manifestClass;
|
||||||
|
ReadManifest(manifestClass);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers an Acedia package with a given name.
|
||||||
|
///
|
||||||
|
/// Returns `true` if package was successfully registered, `false` if it either
|
||||||
|
/// does not exist or was already registered.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will log an error if the package has failed to get registered (it is either
|
||||||
|
/// missing or not an Acedia package).
|
||||||
|
public final function RegisterPackage_S(string packageName) {
|
||||||
|
local Text wrapper;
|
||||||
|
|
||||||
|
wrapper = _.text.FromString(packageName);
|
||||||
|
RegisterPackage(wrapper);
|
||||||
|
_.memory.Free(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool IsManifestRegistered(class<_manifest> manifestClass) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < availablePackages.length; i += 1) {
|
||||||
|
if (manifestClass == availablePackages[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function ReadManifest(class<_manifest> manifestClass) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < manifestClass.default.features.length; i += 1) {
|
||||||
|
if (manifestClass.default.features[i] == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
manifestClass.default.features[i].static.LoadConfigs();
|
||||||
|
availableFeatures[availableFeatures.length] = manifestClass.default.features[i];
|
||||||
|
}
|
||||||
|
for (i = 0; i < manifestClass.default.testCases.length; i += 1) {
|
||||||
|
class'TestingService'.static.RegisterTestCase(manifestClass.default.testCases[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` iff AcediaCore is running in the debug mode.
|
||||||
|
///
|
||||||
|
/// AcediaCore's debug mode allows features to enable functionality that is only
|
||||||
|
/// useful during development.
|
||||||
|
/// Whether AcediaCore is running in a debug mode is decided at launch and
|
||||||
|
/// cannot be changed.
|
||||||
|
public final function bool IsDebugging() {
|
||||||
|
return debugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all packages registered in the caller [`AcediaEnvironment`].
|
||||||
|
public final function array< class<_manifest> > GetAvailablePackages() {
|
||||||
|
return availablePackages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all [`Feature`]s available in the caller [`AcediaEnvironment`].
|
||||||
|
public final function array< class<Feature> > GetAvailableFeatures() {
|
||||||
|
return availableFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all currently enabled [`Feature`]s.
|
||||||
|
public final function array<Feature> GetEnabledFeatures() {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < enabledFeatures.length; i += 1) {
|
||||||
|
enabledFeatures[i].NewRef();
|
||||||
|
}
|
||||||
|
return enabledFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanRemove [`Feature`]s that got deallocated.
|
||||||
|
// This shouldn't happen unless someone messes up.
|
||||||
|
private final function CleanEnabledFeatures()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
while (i < enabledFeatures.length) {
|
||||||
|
if (enabledFeatures[i].GetLifeVersion() != enabledFeaturesLifeVersions[i]) {
|
||||||
|
enabledFeatures.Remove(i, 1);
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if [`Feature`] of given class is enabled.
|
||||||
|
///
|
||||||
|
/// Even if If feature of class [`featureClass`] is enabled, it's not
|
||||||
|
/// necessarily that the instance you have reference to is enabled.
|
||||||
|
///
|
||||||
|
/// Although unlikely, it is possible that someone spawned another instance of
|
||||||
|
/// the same class that isn't considered enabled.
|
||||||
|
/// If you want to check whether some particular instance of given class
|
||||||
|
/// [`featureClass`] is enabled, use [`AcediaEnvironment::IsFeatureEnabled()`]
|
||||||
|
/// method instead.
|
||||||
|
public final function bool IsFeatureClassEnabled(class<Feature> featureClass) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (featureClass == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CleanEnabledFeatures();
|
||||||
|
for (i = 0; i < enabledFeatures.length; i += 1) {
|
||||||
|
if (featureClass == enabledFeatures[i].class) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if given `Feature` instance is enabled.
|
||||||
|
///
|
||||||
|
/// If you want to check if any instance instance of given class
|
||||||
|
/// [`classToCheck`] is enabled (and not [`feature`] specifically),
|
||||||
|
/// use [`AcediaEnvironment::IsFeatureClassEnabled()`] method instead.
|
||||||
|
public final function bool IsFeatureEnabled(Feature feature) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (feature == none) return false;
|
||||||
|
if (!feature.IsAllocated()) return false;
|
||||||
|
|
||||||
|
CleanEnabledFeatures();
|
||||||
|
for (i = 0; i < enabledFeatures.length; i += 1) {
|
||||||
|
if (feature == enabledFeatures[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns enabled [`Feature`] instance of the given class.
|
||||||
|
///
|
||||||
|
/// Returns `none` only if [`featureClass`] is not enabled (or also `none`).
|
||||||
|
public final function Feature GetEnabledFeature(class<Feature> featureClass) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (featureClass == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
CleanEnabledFeatures();
|
||||||
|
for (i = 0; i < enabledFeatures.length; i += 1) {
|
||||||
|
if (featureClass == enabledFeatures[i].class) {
|
||||||
|
enabledFeatures[i].NewRef();
|
||||||
|
return enabledFeatures[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables given [`Feature`] instance [`newEnabledFeature`] with a given
|
||||||
|
/// config.
|
||||||
|
/// Does not change a config for already enabled feature, failing instead.
|
||||||
|
///
|
||||||
|
/// Returns `true` if given [`newEnabledFeature`] was enabled and `false`
|
||||||
|
/// otherwise (including if feature of the same class has already been enabled).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will log an error if feature with the same class as [`newEnabledFeature`] is
|
||||||
|
/// already enabled.
|
||||||
|
/// In case already enabled feature is the same instance as
|
||||||
|
/// [`newEnabledFeature`] a warning will be logged instead.
|
||||||
|
public final function bool EnableFeature(Feature newEnabledFeature, optional BaseText configName) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (newEnabledFeature == none) return false;
|
||||||
|
if (!newEnabledFeature.IsAllocated()) return false;
|
||||||
|
|
||||||
|
CleanEnabledFeatures();
|
||||||
|
for (i = 0; i < enabledFeatures.length; i += 1) {
|
||||||
|
if (newEnabledFeature.class == enabledFeatures[i].class) {
|
||||||
|
if (newEnabledFeature == enabledFeatures[i]) {
|
||||||
|
_.logger
|
||||||
|
.Auto(warnFeatureAlreadyEnabled)
|
||||||
|
.Arg(_.text.FromClass(newEnabledFeature.class));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_.logger
|
||||||
|
.Auto(errFeatureClassAlreadyEnabled)
|
||||||
|
.Arg(_.text.FromClass(newEnabledFeature.class));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newEnabledFeature.NewRef();
|
||||||
|
enabledFeatures[enabledFeatures.length] = newEnabledFeature;
|
||||||
|
enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] =
|
||||||
|
newEnabledFeature.GetLifeVersion();
|
||||||
|
newEnabledFeature.EnableInternal(configName);
|
||||||
|
onFeatureEnabledSignal.Emit(newEnabledFeature);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables given [`Feature`] instance [`featureToDisable`].
|
||||||
|
///
|
||||||
|
/// Returns `true` if given [`newEnabledFeature`] was disabled and `false`
|
||||||
|
/// otherwise (including if it already was disabled).
|
||||||
|
public final function bool DisableFeature(Feature featureToDisable) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (featureToDisable == none) return false;
|
||||||
|
if (!featureToDisable.IsAllocated()) return false;
|
||||||
|
|
||||||
|
CleanEnabledFeatures();
|
||||||
|
for (i = 0; i < enabledFeatures.length; i += 1) {
|
||||||
|
if (featureToDisable == enabledFeatures[i]) {
|
||||||
|
enabledFeatures.Remove(i, 1);
|
||||||
|
enabledFeaturesLifeVersions.Remove(i, 1);
|
||||||
|
featureToDisable.DisableInternal();
|
||||||
|
onFeatureDisabledSignal.Emit(featureToDisable.class);
|
||||||
|
_.memory.Free(featureToDisable);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables all currently enabled [`Feature`]s.
|
||||||
|
///
|
||||||
|
/// Mainly intended for the clean up when Acedia shuts down.
|
||||||
|
public final function DisableAllFeatures() {
|
||||||
|
local int i;
|
||||||
|
local array<Feature> featuresCopy;
|
||||||
|
|
||||||
|
CleanEnabledFeatures();
|
||||||
|
// Move features from `enabledFeatures`, which marks them as disabled,
|
||||||
|
// allowing us to successfully call their `DisableInternal()` methods.
|
||||||
|
featuresCopy = enabledFeatures;
|
||||||
|
enabledFeatures.length = 0;
|
||||||
|
enabledFeaturesLifeVersions.length = 0;
|
||||||
|
for (i = 0; i < featuresCopy.length; i += 1) {
|
||||||
|
featuresCopy[i].DisableInternal();
|
||||||
|
onFeatureDisabledSignal.Emit(featuresCopy[i].class);
|
||||||
|
}
|
||||||
|
_.memory.FreeMany(featuresCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns globally defined value with a specified name.
|
||||||
|
///
|
||||||
|
/// Returns `none` if [`valueName`] is `none`.
|
||||||
|
///
|
||||||
|
/// # Error
|
||||||
|
///
|
||||||
|
/// This shouldn't happen, but in case global data object wasn't created,
|
||||||
|
/// logs an error.
|
||||||
|
/// Also returns `none` no matter the argument.
|
||||||
|
public final function AcediaObject GetGlobalValue(BaseText valueName) {
|
||||||
|
if (valueName == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
if (globalData == none) {
|
||||||
|
_.logger.Auto(errNoGlobals);
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
return globalData.GetItem(valueName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns globally defined value with a specified name.
|
||||||
|
///
|
||||||
|
/// # Error
|
||||||
|
///
|
||||||
|
/// This shouldn't happen, but in case global data object wasn't created,
|
||||||
|
/// logs an error.
|
||||||
|
/// Also returns `none` no matter the argument.
|
||||||
|
public final function AcediaObject GetGlobalValue_S(string valueName) {
|
||||||
|
local Text wrapper;
|
||||||
|
local AcediaObject value;
|
||||||
|
|
||||||
|
wrapper = _.text.FromString(valueName);
|
||||||
|
value = GetGlobalValue(wrapper);
|
||||||
|
_.memory.Free(wrapper);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets globally defined value for a specified name.
|
||||||
|
///
|
||||||
|
/// Does nothing if [`valueName`] is `none`.
|
||||||
|
///
|
||||||
|
/// # Error
|
||||||
|
///
|
||||||
|
/// This shouldn't happen, but in case global data object wasn't created,
|
||||||
|
/// logs an error.
|
||||||
|
public final function SetGlobalValue(BaseText valueName, AcediaObject value) {
|
||||||
|
if (valueName == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (globalData == none) {
|
||||||
|
_.logger.Auto(errNoGlobals);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalData.SetItem(valueName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets globally defined value for a specified name.
|
||||||
|
///
|
||||||
|
/// # Error
|
||||||
|
///
|
||||||
|
/// This shouldn't happen, but in case global data object wasn't created,
|
||||||
|
/// logs an error.
|
||||||
|
public final function SetGlobalValue_S(string valueName, AcediaObject value) {
|
||||||
|
local Text wrapper;
|
||||||
|
|
||||||
|
wrapper = _.text.FromString(valueName);
|
||||||
|
SetGlobalValue(wrapper, value);
|
||||||
|
_.memory.Free(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
manifestSuffix = ".Manifest"
|
||||||
|
debugMode = false
|
||||||
|
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".")
|
||||||
|
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.")
|
||||||
|
errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.")
|
||||||
|
warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Trying to enable already enabled instance of `Feature` class `%1`.")
|
||||||
|
errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.")
|
||||||
|
errNoGlobals = (l=LOG_Error,m="Global data isn't initialize for `AcediaEnvironment`.")
|
||||||
|
}
|
||||||
166
kf_sources/AcediaCore/Classes/AcediaObjectPool.uc
Normal file
166
kf_sources/AcediaCore/Classes/AcediaObjectPool.uc
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2020-2024 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AcediaObjectPool extends Object
|
||||||
|
config(AcediaSystem);
|
||||||
|
|
||||||
|
//! Acedia's implementation for object pool.
|
||||||
|
//!
|
||||||
|
//! Unlike generic built in [`Engine::ObjectPool`], that can only store objects
|
||||||
|
//! of one specific class, it specializes in a single class to allow for both
|
||||||
|
//! faster allocation and faster deallocation (we don't need to look for
|
||||||
|
//! an object of particular class to return an unused instance).
|
||||||
|
//!
|
||||||
|
//! Allows to set a maximum capacity in a config.
|
||||||
|
|
||||||
|
/// Represents config entry about pool capacity.
|
||||||
|
///
|
||||||
|
/// This struct and it's associated array [`poolSizeOverwrite`] allows server
|
||||||
|
/// admins to rewrite
|
||||||
|
/// the pool capacity for each class.
|
||||||
|
struct PoolSizeSetting {
|
||||||
|
var class<AcediaObject> objectClass;
|
||||||
|
var int maxPoolSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
var private config const array<PoolSizeSetting> poolSizeOverwrite;
|
||||||
|
// Class of objects that this `AcediaObjectPool` stores.
|
||||||
|
// if `== none`, - object pool is considered uninitialized.
|
||||||
|
var private class<AcediaObject> storedClass;
|
||||||
|
|
||||||
|
// Capacity for object pool that we are using.
|
||||||
|
// Obtained from [`poolSizeOverwrite`] during initialization and cannot be changed later.
|
||||||
|
var private int usedMaxPoolSize;
|
||||||
|
|
||||||
|
// Actual storage, functions on LIFO principle.
|
||||||
|
var private array<AcediaObject> objectPool;
|
||||||
|
|
||||||
|
// Determines default object pool size for the initialization.
|
||||||
|
private final function int GetMaxPoolSizeForClass(class<AcediaObject> classToCheck) {
|
||||||
|
local int i;
|
||||||
|
local int result;
|
||||||
|
|
||||||
|
if (classToCheck != none) {
|
||||||
|
result = classToCheck.default.defaultMaxPoolSize;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = -1;
|
||||||
|
}
|
||||||
|
// Try to replace it with server's settings
|
||||||
|
for (i = 0; i < poolSizeOverwrite.length; i += 1) {
|
||||||
|
if (poolSizeOverwrite[i].objectClass == classToCheck) {
|
||||||
|
result = poolSizeOverwrite[i].maxPoolSize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes caller object pool to store objects of the given class.
|
||||||
|
///
|
||||||
|
/// Returns `true` if initialization completed, `false` otherwise (including if
|
||||||
|
/// it was already completed with passed [`classToStore`]).
|
||||||
|
///
|
||||||
|
/// If successful, this action is irreversible: same pool cannot be
|
||||||
|
/// re-initialized.
|
||||||
|
///
|
||||||
|
/// [`forcedPoolSize`] defines max pool size for the caller
|
||||||
|
/// [`AcediaObjectPool`].
|
||||||
|
/// Leaving it at default `0` value will cause method to auto-determine
|
||||||
|
/// the size: gives priority to the [`poolSizeOverwrite`] config array;
|
||||||
|
/// if not specified, uses [`AcediaObject`]'s
|
||||||
|
/// [`AcediaObject::defaultMaxPoolSize`]
|
||||||
|
/// (ignoring [`AcediaObject::usesObjectPool`] setting).
|
||||||
|
public final function bool Initialize(
|
||||||
|
class<AcediaObject> classToStore,
|
||||||
|
optional int forcedPoolSize
|
||||||
|
) {
|
||||||
|
if (storedClass != none) return false;
|
||||||
|
if (classToStore == none) return false;
|
||||||
|
|
||||||
|
// If does not matter that we've set those variables until
|
||||||
|
// we set `storedClass`.
|
||||||
|
if (forcedPoolSize == 0) {
|
||||||
|
usedMaxPoolSize = GetMaxPoolSizeForClass(classToStore);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
usedMaxPoolSize = forcedPoolSize;
|
||||||
|
}
|
||||||
|
if (usedMaxPoolSize == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
storedClass = classToStore;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns class of objects stored inside the caller [`AcediaObjectPool`].
|
||||||
|
///
|
||||||
|
/// `none` means object pool was not initialized.
|
||||||
|
public final function class<AcediaObject> GetClassOfStoredObjects() {
|
||||||
|
return storedClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the storage of all its contents.
|
||||||
|
public final function Clear() {
|
||||||
|
objectPool.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds object to the caller storage (that needs to be initialized to store
|
||||||
|
/// [`newObject.class`] classes).
|
||||||
|
///
|
||||||
|
/// Returns `true` on success and `false` on failure (can happen if passed
|
||||||
|
/// [`newObject`] reference was invalid, caller storage is not initialized yet
|
||||||
|
/// or reached it's capacity).
|
||||||
|
///
|
||||||
|
/// For performance purposes does not do duplicates checks, this should be
|
||||||
|
/// verified from outside [`AcediaObjectPool`].
|
||||||
|
///
|
||||||
|
/// Performs type checks and only allows objects of the class that caller
|
||||||
|
/// [`AcediaObjectPool`] was initialized for.
|
||||||
|
public final function bool Store(AcediaObject newObject) {
|
||||||
|
if (newObject == none) return true;
|
||||||
|
if (newObject.class != storedClass) return false;
|
||||||
|
|
||||||
|
if (usedMaxPoolSize >= 0 && objectPool.length >= usedMaxPoolSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
objectPool[objectPool.length] = newObject;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns last stored object from the pool, removing it from that pool in
|
||||||
|
/// the process.
|
||||||
|
///
|
||||||
|
/// Only returns `none` if caller `AcediaObjectPool` is either empty or not
|
||||||
|
/// initialized.
|
||||||
|
public final function AcediaObject Fetch() {
|
||||||
|
local AcediaObject result;
|
||||||
|
|
||||||
|
if (storedClass == none) return none;
|
||||||
|
if (objectPool.length <= 0) return none;
|
||||||
|
|
||||||
|
result = objectPool[objectPool.length - 1];
|
||||||
|
objectPool.length = objectPool.length - 1;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
40
kf_sources/AcediaCore/Classes/AcediaReplicationInfo.uc
Normal file
40
kf_sources/AcediaCore/Classes/AcediaReplicationInfo.uc
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Facilitates some core replicated functions between client and server.
|
||||||
|
* Copyright 2019-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AcediaReplicationInfo extends AcediaActor;
|
||||||
|
|
||||||
|
var public PlayerController linkOwner;
|
||||||
|
|
||||||
|
replication
|
||||||
|
{
|
||||||
|
reliable if (role == ROLE_Authority)
|
||||||
|
linkOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
remoteRole = ROLE_SimulatedProxy
|
||||||
|
netUpdateFrequency = 10
|
||||||
|
bAlwaysRelevant = true
|
||||||
|
bStatic = false
|
||||||
|
bNoDelete = false
|
||||||
|
bHidden = true
|
||||||
|
bOnlyDirtyReplication = true
|
||||||
|
bSkipActorPropertyReplication = true
|
||||||
|
}
|
||||||
294
kf_sources/AcediaCore/Classes/ActorService.uc
Normal file
294
kf_sources/AcediaCore/Classes/ActorService.uc
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* Service for safe `Actor` storage, aimed to resolve two issues that arise
|
||||||
|
* from storing references to `Actor`s inside non-`Actor` objects:
|
||||||
|
* 1. This can potentially prevent the whole level from being
|
||||||
|
* garbage-collected on level change, leading to excessive memory use
|
||||||
|
* and crashes;
|
||||||
|
* 2. This can lead to stored `Actor` temporarily being put into
|
||||||
|
* a "faulty state", where any sort of access to that `Actor`
|
||||||
|
* (except converting it into a `string` ¯\_(ツ)_/¯) will lead to
|
||||||
|
* server/game crashing.
|
||||||
|
* This `Service` resolves these issues by storing `Actor`s in such a way
|
||||||
|
* that they can be relatively quickly obtained via a simple struct value
|
||||||
|
* (`ActorReference`) that can be safely stored inside any `Object`.
|
||||||
|
* It is recommended to use `ActorRef` / `ActorBox` instead of accessing
|
||||||
|
* this `Service` directly.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ActorService extends Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service works as follows: it contains an array filled with `Actor`s
|
||||||
|
* that will dynamically grow in size whenever more space is needed for new
|
||||||
|
* `Actor`s. To refer to the stored `Actor` we put it's index in the storage
|
||||||
|
* array in `ActorReference` struct.
|
||||||
|
*
|
||||||
|
* The main problem with that approach is that once `Actor` is destroyed or
|
||||||
|
* removed- it leaves a "hole" inside our storage and if we keep simply adding
|
||||||
|
* new `Actor`s at the end of the storage array - we will eventually use
|
||||||
|
* too much space, even if we only store a limited amount of `Actor`s at any
|
||||||
|
* given point in time.
|
||||||
|
* To avoid this problem we also maintain an array of "empty indices" that
|
||||||
|
* remembers where the "holes" are located and can let us re-allocate them to
|
||||||
|
* place new `Actor`s in, without needlessly increasing storage's size.
|
||||||
|
*
|
||||||
|
* Another problem is that now two different `Actor`s can occupy the same
|
||||||
|
* spot at different points in time. To deal with that we will also record
|
||||||
|
* an `Actor`'s "version" for each storage index and increment it each time
|
||||||
|
* a new `Actor` is stored at that index. We will also store that "version"
|
||||||
|
* inside `ActorReference` to help us invalidate references to old `Actor`s
|
||||||
|
* that were stored there before.
|
||||||
|
*
|
||||||
|
* NOTE that this `Service` avoids doing any sort of manual cleaning,
|
||||||
|
* instead relying on the proper interface (`ActorRef` / `ActorBox`) to do it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the `Actor` storage inside this service.
|
||||||
|
* Reference is only considered invalid after an `Actor` it was referring
|
||||||
|
* to was removed from the storage (possibly by being replaced with
|
||||||
|
* another `Actor`).
|
||||||
|
*/
|
||||||
|
struct ActorReference
|
||||||
|
{
|
||||||
|
var private int index;
|
||||||
|
var private int version;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Arrays `actorRecords`, `actorVersions` and `indexAllocationFlag` could
|
||||||
|
* be represented as an array of a single struct with 3 element, but instead,
|
||||||
|
* for performance reasons, were separated into three distinct arrays - one for
|
||||||
|
* each variable.
|
||||||
|
* This means that they must at all times have the same length and
|
||||||
|
* elements with the same index should be considered to belong to
|
||||||
|
* the same record.
|
||||||
|
*/
|
||||||
|
// Storage for `Actor`s. It's size can grow, but it cannot shrink, instead
|
||||||
|
// marking some of array's indices as vacant
|
||||||
|
// (by putting them into `emptyIndices`).
|
||||||
|
var private array<Actor> actorRecords;
|
||||||
|
// Defines a "version" of a corresponding `Actor`.
|
||||||
|
var private array<int> actorVersions;
|
||||||
|
// Marks whether values at a particular index are currently used to
|
||||||
|
// store some `Actor` (`1`) or not (`0`).
|
||||||
|
// Without these flags we would have no way of knowing whether a spot in
|
||||||
|
// the array was already freed and recorded in `emptyIndices`, since stored
|
||||||
|
// `Actor`s can turn into `none` by themselves upon destruction.
|
||||||
|
var private array<byte> indexAllocationFlag;
|
||||||
|
|
||||||
|
// A set of empty indices - that once contained stored an `Actor`, but can
|
||||||
|
// now be reused.
|
||||||
|
// Used like a LIFO-queue to quickly find an empty spot in `actorRecords`.
|
||||||
|
var private array<int> emptyIndices;
|
||||||
|
|
||||||
|
// Finds an vacant index in our records (expands array if all indices are
|
||||||
|
// already taken) and puts `candidate` there.
|
||||||
|
// Does not do any checks for whether `candidate != none`.
|
||||||
|
private final function int InsertAtEmptyIndex(Actor candidate)
|
||||||
|
{
|
||||||
|
local int newIndex;
|
||||||
|
if (emptyIndices.length > 0)
|
||||||
|
{
|
||||||
|
newIndex = emptyIndices[emptyIndices.length - 1];
|
||||||
|
emptyIndices.length = emptyIndices.length - 1;
|
||||||
|
actorVersions[newIndex] += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newIndex = actorRecords.length;
|
||||||
|
actorVersions[newIndex] = 0;
|
||||||
|
}
|
||||||
|
actorRecords[newIndex] = candidate;
|
||||||
|
indexAllocationFlag[newIndex] = 1;
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forces `indexToFree` to become vacant, not storing any `Actor`.
|
||||||
|
private final function FreeIndex(int indexToFree)
|
||||||
|
{
|
||||||
|
if (indexAllocationFlag[indexToFree] > 0)
|
||||||
|
{
|
||||||
|
actorRecords[indexToFree] = none;
|
||||||
|
emptyIndices[emptyIndices.length] = indexToFree;
|
||||||
|
}
|
||||||
|
// No need to bump `actorVersions[indexToFree]` here, since any refences to
|
||||||
|
// this index will automatically be invalidated by this:
|
||||||
|
indexAllocationFlag[indexToFree] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new `Actor` to the storage.
|
||||||
|
*
|
||||||
|
* Any reference created with `AddActor()` must be release with
|
||||||
|
* `RemoveActor()`.
|
||||||
|
*
|
||||||
|
* This method does not attempt to check whether `newActor` is already stored
|
||||||
|
* in `ActorService`. Therefore it is possible for the same actor to be stored
|
||||||
|
* multiple times under different `ActorReference` records.
|
||||||
|
*
|
||||||
|
* Can only fail if provided `Actor` is either equal to `none`.
|
||||||
|
*
|
||||||
|
* Note that while this storage is supposed to be "safe", meaning that it
|
||||||
|
* aims to reduce probability of `Actor`-related crashes, it assumes that
|
||||||
|
* passed `Actor`s are not already "broken", as there is no way to check
|
||||||
|
* for that.
|
||||||
|
* `Actor`s that were not stored in a non-`Actor` object as a non-local
|
||||||
|
* variable should not be broken.
|
||||||
|
*
|
||||||
|
* @param newActor `Actor` to store. If it's equal to `none`, then
|
||||||
|
* this method will do nothing.
|
||||||
|
* @return `ActorReference` struct that can be used to retrieve stored
|
||||||
|
* `Actor` later. Returned reference will point at `none` once stored
|
||||||
|
* `Actor` gets destroyed or removed via the `RemoveActor()` call.
|
||||||
|
*/
|
||||||
|
public final function ActorReference AddActor(Actor newActor)
|
||||||
|
{
|
||||||
|
local int newIndex;
|
||||||
|
local ActorReference result;
|
||||||
|
// This will make the very first check inside `ValidateIndexVersion()` fail
|
||||||
|
result.index = -1;
|
||||||
|
if (newActor == none) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
newIndex = InsertAtEmptyIndex(newActor);
|
||||||
|
result.index = newIndex;
|
||||||
|
result.version = actorVersions[newIndex];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an `Actor` that provided `reference` points at.
|
||||||
|
*
|
||||||
|
* @param reference Reference to the `Actor` this method should return.
|
||||||
|
* @return `Actor`, stored in caller `ActorService` with `reference`.
|
||||||
|
* If stored `Actor` was already destroyed or removed, this method will
|
||||||
|
* return `none`.
|
||||||
|
*/
|
||||||
|
public final function Actor GetActor(ActorReference reference)
|
||||||
|
{
|
||||||
|
local int index;
|
||||||
|
local Actor result;
|
||||||
|
index = reference.index;
|
||||||
|
if (!ValidateIndexVersion(index, reference.version)) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
// While this can be considered a side-effect in a getter, removing `none`
|
||||||
|
// from the storage will not affect how `reference` behaves, since it can
|
||||||
|
// only refer to `none` from now on.
|
||||||
|
result = actorRecords[index];
|
||||||
|
if (result == none)
|
||||||
|
{
|
||||||
|
FreeIndex(index);
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces an `Actor`, that provided `reference` points at, with a `newActor`,
|
||||||
|
* returning a new `ActorReference` and invalidating the old one (`reference`),
|
||||||
|
* making it to refer to `none`.
|
||||||
|
*
|
||||||
|
* This method is guaranteed to be functionally identical (as far as public,
|
||||||
|
* not encapsulated, interface is concerned) to calling two methods:
|
||||||
|
* `RemoveActor(reference)` and `AddActor(newActor)`. But is expected to
|
||||||
|
* provide be more efficient.
|
||||||
|
*
|
||||||
|
* @param reference Reference to `Actor` that should be replaced.
|
||||||
|
* @param newActor New `Actor` to store.
|
||||||
|
* @return Reference to the `newActor` inside the storage.
|
||||||
|
*/
|
||||||
|
public final function ActorReference UpdateActor(
|
||||||
|
ActorReference reference,
|
||||||
|
Actor newActor)
|
||||||
|
{
|
||||||
|
local int index;
|
||||||
|
index = reference.index;
|
||||||
|
// Nothing to remove
|
||||||
|
if (!ValidateIndexVersion(index, reference.version)) {
|
||||||
|
return AddActor(newActor);
|
||||||
|
}
|
||||||
|
// Nothing to add
|
||||||
|
if (newActor == none) {
|
||||||
|
RemoveActor(reference);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If we need to both remove and add, we can just replace an `Actor`
|
||||||
|
// and bump the stored version to invalidate copies of the passed
|
||||||
|
// `reference`:
|
||||||
|
actorRecords[index] = newActor;
|
||||||
|
actorVersions[index] += 1;
|
||||||
|
reference.version += 1;
|
||||||
|
}
|
||||||
|
return reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unconditionally removes an `Actor` recorded with `reference` from storage.
|
||||||
|
*
|
||||||
|
* Cannot fail.
|
||||||
|
*
|
||||||
|
* If referred `Actor` is recorded in storage under several different
|
||||||
|
* references - only this reference will be affected. The rest are still going
|
||||||
|
* to refer to that `Actor`.
|
||||||
|
*
|
||||||
|
* @param reference Reference to `Actor` to be removed.
|
||||||
|
*/
|
||||||
|
public final function RemoveActor(ActorReference reference)
|
||||||
|
{
|
||||||
|
if (ValidateIndexVersion(reference.index, reference.version)) {
|
||||||
|
FreeIndex(reference.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns size of this storage.
|
||||||
|
*
|
||||||
|
* @param totalSize Default value (`false`) means that method should return
|
||||||
|
* amount of currently stored `Actor`s, while `true` means how much space
|
||||||
|
* this storage takes up (in the sense that the actual amount in bytes is
|
||||||
|
* `O(GetSize(true))` in big-O notation).
|
||||||
|
* @return Size of this storage.
|
||||||
|
*/
|
||||||
|
public final function int GetSize(optional bool totalSize)
|
||||||
|
{
|
||||||
|
if (totalSize) {
|
||||||
|
return actorRecords.length;
|
||||||
|
}
|
||||||
|
return actorRecords.length - emptyIndices.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates passed index and `Actor`'s version, taken from
|
||||||
|
// the `ActorReference`: returns `true` iff `Actor` referred to by them
|
||||||
|
// currently exists in this storage (even if it's now equal to `none`).
|
||||||
|
private final function bool ValidateIndexVersion(
|
||||||
|
int refIndex,
|
||||||
|
int refVersion)
|
||||||
|
{
|
||||||
|
if (refIndex < 0) return false;
|
||||||
|
if (refIndex >= actorRecords.length) return false;
|
||||||
|
if (actorVersions[refIndex] != refVersion) return false;
|
||||||
|
|
||||||
|
return (indexAllocationFlag[refIndex] > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
385
kf_sources/AcediaCore/Classes/AliasSource.uc
Normal file
385
kf_sources/AcediaCore/Classes/AliasSource.uc
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
/**
|
||||||
|
* This class implements an alias database that stores aliases inside
|
||||||
|
* standard config ini-files.
|
||||||
|
* Copyright 2020-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AliasSource extends BaseAliasSource
|
||||||
|
dependson(HashTable)
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
// Links alias to a value.
|
||||||
|
// An array of these structures (without duplicate `alias` records) defines
|
||||||
|
// a function from the space of aliases to the space of values.
|
||||||
|
struct AliasValuePair
|
||||||
|
{
|
||||||
|
var string alias;
|
||||||
|
var string value;
|
||||||
|
};
|
||||||
|
// Aliases data for saving and loading on a disk (ini-file).
|
||||||
|
// Name is chosen to make configurational files more readable.
|
||||||
|
var private config array<AliasValuePair> record;
|
||||||
|
|
||||||
|
// (Sub-)class of `Aliases` objects that this `AliasSource` uses to store
|
||||||
|
// aliases in per-object-config manner.
|
||||||
|
// Leaving this variable `none` will produce an `AliasSource` that can
|
||||||
|
// only store aliases in form of `record=(alias="...",value="...")`.
|
||||||
|
var public const class<AliasesStorage> aliasesClass;
|
||||||
|
// Storage for all objects of `aliasesClass` class in the config.
|
||||||
|
// Exists after `OnCreated()` event and is maintained up-to-date at all times.
|
||||||
|
var private array<AliasesStorage> loadedAliasObjects;
|
||||||
|
|
||||||
|
// Faster access to value by alias' name.
|
||||||
|
// It contains same records as `record` array + aliases from
|
||||||
|
// `loadedAliasObjects` objects when there are no duplicate aliases.
|
||||||
|
// Otherwise only stores first loaded alias.
|
||||||
|
var private HashTable aliasHash;
|
||||||
|
// Faster access to all aliases, corresponding to a certain value.
|
||||||
|
// This `HashTable` stores same data as `aliasHash`, but "in reverse":
|
||||||
|
// for each value as a "key" it stored `ArrayList` of corresponding aliases.
|
||||||
|
var private HashTable valueHash;
|
||||||
|
|
||||||
|
// `true` means that this `AliasSource` is awaiting saving into its config
|
||||||
|
var private bool pendingSaveToConfig;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition errIncorrectAliasPair, warnDuplicateAlias;
|
||||||
|
var private LoggerAPI.Definition warnInvalidAlias;
|
||||||
|
|
||||||
|
// Load and hash all the data `AliasSource` creation.
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
// If this check fails - caller alias source is fundamentally broken
|
||||||
|
// and requires mod to be fixed
|
||||||
|
if (!ASSERT_AliasesClassIsOwnedByThisSource()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Load and hash
|
||||||
|
loadedAliasObjects = aliasesClass.static.LoadAllObjects();
|
||||||
|
aliasHash = _.collections.EmptyHashTable();
|
||||||
|
valueHash = _.collections.EmptyHashTable();
|
||||||
|
HashValidAliasesFromRecord();
|
||||||
|
HashValidAliasesFromPerObjectConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
loadedAliasObjects.length = 0;
|
||||||
|
_.memory.Free(aliasHash);
|
||||||
|
aliasHash = none;
|
||||||
|
if (pendingSaveToConfig) {
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures that our `Aliases` class is properly linked with this
|
||||||
|
// source's class. Logs failure otherwise.
|
||||||
|
private function bool ASSERT_AliasesClassIsOwnedByThisSource()
|
||||||
|
{
|
||||||
|
if (aliasesClass == none) return true;
|
||||||
|
if (aliasesClass.default.sourceClass == class) return true;
|
||||||
|
|
||||||
|
_.logger.Auto(errIncorrectAliasPair).ArgClass(class);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load hashes from `AliasSource`'s config (`record` array)
|
||||||
|
private function HashValidAliasesFromRecord()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text aliasAsText, valueAsText;
|
||||||
|
|
||||||
|
for (i = 0; i < record.length; i += 1)
|
||||||
|
{
|
||||||
|
aliasAsText = _.text.FromString(record[i].alias);
|
||||||
|
valueAsText = _.text.FromString(record[i].value);
|
||||||
|
InsertAliasIntoHash(aliasAsText, valueAsText);
|
||||||
|
aliasAsText.FreeSelf();
|
||||||
|
valueAsText.FreeSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load hashes from `Aliases` objects' config
|
||||||
|
private function HashValidAliasesFromPerObjectConfig()
|
||||||
|
{
|
||||||
|
local int i, j;
|
||||||
|
local Text nextValue;
|
||||||
|
local array<Text> valueAliases;
|
||||||
|
|
||||||
|
for (i = 0; i < loadedAliasObjects.length; i += 1)
|
||||||
|
{
|
||||||
|
nextValue = loadedAliasObjects[i].GetValue();
|
||||||
|
valueAliases = loadedAliasObjects[i].GetAliases();
|
||||||
|
for (j = 0; j < valueAliases.length; j += 1) {
|
||||||
|
InsertAliasIntoHash(valueAliases[j], nextValue);
|
||||||
|
}
|
||||||
|
nextValue.FreeSelf();
|
||||||
|
_.memory.FreeMany(valueAliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function bool AreValuesCaseSensitive()
|
||||||
|
{
|
||||||
|
// Almost all built-in aliases are aliases to class names (or templates)
|
||||||
|
// and the rest are colors. Both are case-insensitive, so returning `false`
|
||||||
|
// is a good default implementation. Child classes can just change this
|
||||||
|
// value, if they need.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function array<Text> GetAllAliases()
|
||||||
|
{
|
||||||
|
return aliasHash.GetTextKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function array<Text> GetAliases(BaseText value)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text storedValue;
|
||||||
|
local ArrayList aliasesArray;
|
||||||
|
local array<Text> result;
|
||||||
|
|
||||||
|
storedValue = NormalizeValue(value);
|
||||||
|
aliasesArray = valueHash.GetArrayList(storedValue);
|
||||||
|
storedValue.FreeSelf();
|
||||||
|
if (aliasesArray == none) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (i = 0; i < aliasesArray.GetLength(); i += 1) {
|
||||||
|
result[result.length] = aliasesArray.GetText(i);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Normalizes" value:
|
||||||
|
// 1. Converts it into lower case if `AreValuesCaseSensitive()` returns
|
||||||
|
// `true`;
|
||||||
|
// 2. Converts in into `Text` in case passed value is `MutableText`, so
|
||||||
|
// that hash table is actually usable.
|
||||||
|
private function Text NormalizeValue(BaseText value)
|
||||||
|
{
|
||||||
|
if (value == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
if (AreValuesCaseSensitive()) {
|
||||||
|
return value.Copy();
|
||||||
|
}
|
||||||
|
return value.LowerCopy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserts alias into `aliasHash`, cleaning previous keys/values in case
|
||||||
|
// they already exist.
|
||||||
|
// Takes care of lower case conversion to store aliases in `aliasHash`
|
||||||
|
// in a case-insensitive way. Depending on `AreValuesCaseSensitive()`, can also
|
||||||
|
// convert values to lower case.
|
||||||
|
private function InsertAliasIntoHash(BaseText alias, BaseText value)
|
||||||
|
{
|
||||||
|
local Text storedAlias;
|
||||||
|
local Text storedValue;
|
||||||
|
local Text existingValue;
|
||||||
|
local ArrayList valueAliases;
|
||||||
|
|
||||||
|
if (alias == none) return;
|
||||||
|
if (value == none) return;
|
||||||
|
|
||||||
|
if (!alias.IsValidName())
|
||||||
|
{
|
||||||
|
_.logger.Auto(warnInvalidAlias)
|
||||||
|
.ArgClass(class)
|
||||||
|
.Arg(alias.Copy());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storedAlias = alias.LowerCopy();
|
||||||
|
existingValue = aliasHash.GetText(storedAlias);
|
||||||
|
if (aliasHash.HasKey(storedAlias))
|
||||||
|
{
|
||||||
|
_.logger.Auto(warnDuplicateAlias)
|
||||||
|
.ArgClass(class)
|
||||||
|
.Arg(alias.Copy())
|
||||||
|
.Arg(existingValue);
|
||||||
|
}
|
||||||
|
_.memory.Free(existingValue);
|
||||||
|
storedValue = NormalizeValue(value);
|
||||||
|
// Add to `aliasHash`: alias -> value
|
||||||
|
aliasHash.SetItem(storedAlias, storedValue);
|
||||||
|
// Add to `valueHash`: value -> alias
|
||||||
|
valueAliases = valueHash.GetArrayList(storedValue);
|
||||||
|
if (valueAliases == none) {
|
||||||
|
valueAliases = _.collections.EmptyArrayList();
|
||||||
|
}
|
||||||
|
valueAliases.AddItem(storedAlias);
|
||||||
|
valueHash.SetItem(storedValue, valueAliases);
|
||||||
|
// Clean up
|
||||||
|
storedAlias.FreeSelf();
|
||||||
|
storedValue.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bool HasAlias(BaseText alias)
|
||||||
|
{
|
||||||
|
local bool result;
|
||||||
|
local Text storedAlias;
|
||||||
|
|
||||||
|
if (alias == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
storedAlias = alias.LowerCopy();
|
||||||
|
result = aliasHash.HasKey(storedAlias);
|
||||||
|
storedAlias.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Text Resolve(
|
||||||
|
BaseText alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
local Text result;
|
||||||
|
local Text storedAlias;
|
||||||
|
|
||||||
|
if (alias == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
storedAlias = alias.LowerCopy();
|
||||||
|
result = aliasHash.GetText(storedAlias);
|
||||||
|
storedAlias.FreeSelf();
|
||||||
|
if (result != none) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (copyOnFailure) {
|
||||||
|
return alias.Copy();
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bool AddAlias(BaseText aliasToAdd, BaseText aliasValue)
|
||||||
|
{
|
||||||
|
local Text storedAlias;
|
||||||
|
|
||||||
|
if (aliasToAdd == none) return false;
|
||||||
|
if (aliasValue == none) return false;
|
||||||
|
if (!aliasToAdd.IsValidName()) return false;
|
||||||
|
|
||||||
|
// Check if alias already exists and if yes - remove it
|
||||||
|
storedAlias = aliasToAdd.LowerCopy();
|
||||||
|
if (aliasHash.HasKey(storedAlias)) {
|
||||||
|
RemoveAlias(aliasToAdd);
|
||||||
|
}
|
||||||
|
storedAlias.FreeSelf();
|
||||||
|
// Add alias-value pair
|
||||||
|
AddToConfigRecords(aliasToAdd.ToString(), aliasValue.ToString());
|
||||||
|
InsertAliasIntoHash(aliasToAdd, aliasValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bool RemoveAlias(BaseText aliasToRemove)
|
||||||
|
{
|
||||||
|
local Text storedAlias, storedValue;
|
||||||
|
local ArrayList valueAliases;
|
||||||
|
|
||||||
|
if (aliasToRemove == none) return false;
|
||||||
|
if (!aliasToRemove.IsValidName()) return false;
|
||||||
|
|
||||||
|
storedAlias = aliasToRemove.LowerCopy();
|
||||||
|
storedValue = aliasHash.GetText(storedAlias);
|
||||||
|
if (storedValue == none)
|
||||||
|
{
|
||||||
|
storedAlias.FreeSelf();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
aliasHash.RemoveItem(aliasToRemove);
|
||||||
|
// Since we've found `storedValue`, this couldn't possibly be `none` if
|
||||||
|
// "same data invariant" is preserved (see their declaration)
|
||||||
|
valueAliases = valueHash.GetArrayList(storedValue);
|
||||||
|
if (valueAliases != none) {
|
||||||
|
valueAliases.RemoveItem(storedAlias, true);
|
||||||
|
}
|
||||||
|
if (valueAliases != none && valueAliases.GetLength() <= 0)
|
||||||
|
{
|
||||||
|
valueHash.SetItem(storedValue, none);
|
||||||
|
valueAliases = none;
|
||||||
|
}
|
||||||
|
_.memory.Free(valueAliases);
|
||||||
|
RemoveFromConfigRecords(aliasToRemove.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes `string`s that represents alias to remove in proper case (lower for
|
||||||
|
// aliases and for values it depends on the caller source's settings):
|
||||||
|
// aliases are supposed to be ASCII, so `string` should handle it and its
|
||||||
|
// comparison just fine
|
||||||
|
private function AddToConfigRecords(string alias, string value)
|
||||||
|
{
|
||||||
|
local AliasValuePair newPair;
|
||||||
|
|
||||||
|
newPair.alias = alias;
|
||||||
|
newPair.value = value;
|
||||||
|
record[record.length] = newPair;
|
||||||
|
// Request saving
|
||||||
|
if (!pendingSaveToConfig)
|
||||||
|
{
|
||||||
|
pendingSaveToConfig = true;
|
||||||
|
_.scheduler.RequestDiskAccess(self).connect = SaveSelf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes `string` that represents alias to remove in lower case: aliases are
|
||||||
|
// supposed to be ASCII, so `string` should handle it and its comparison just
|
||||||
|
// fine
|
||||||
|
private function RemoveFromConfigRecords(string aliasToRemove)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local bool removedAliasFromRecord;
|
||||||
|
|
||||||
|
// Aliases are supposed to be ASCII, so `string` should handle it and its
|
||||||
|
// comparison just fine
|
||||||
|
while (i < record.length)
|
||||||
|
{
|
||||||
|
if (aliasToRemove ~= record[i].alias)
|
||||||
|
{
|
||||||
|
record.Remove(i, 1);
|
||||||
|
removedAliasFromRecord = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Since admins can fuck up and add duplicate aliases, we need to
|
||||||
|
// thoroughly check every alias object
|
||||||
|
for (i = 0; i < loadedAliasObjects.length; i += 1) {
|
||||||
|
loadedAliasObjects[i].RemoveAlias_S(aliasToRemove);
|
||||||
|
}
|
||||||
|
// Alias objects can request disk access themselves, so only record if
|
||||||
|
// needed for the record
|
||||||
|
if (removedAliasFromRecord && !pendingSaveToConfig)
|
||||||
|
{
|
||||||
|
pendingSaveToConfig = true;
|
||||||
|
_.scheduler.RequestDiskAccess(self).connect = SaveSelf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function SaveSelf()
|
||||||
|
{
|
||||||
|
pendingSaveToConfig = false;
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
// Source main parameters
|
||||||
|
aliasesClass = class'Aliases'
|
||||||
|
errIncorrectAliasPair = (l=LOG_Error,m="`AliasSource`-`Aliases` class pair is incorrectly setup for source `%1`. Omitting it.")
|
||||||
|
warnDuplicateAlias = (l=LOG_Warning,m="Alias source `%1` has duplicate record for alias \"%2\". This is likely due to an erroneous config. \"%3\" value will be used.")
|
||||||
|
warnInvalidAlias = (l=LOG_Warning,m="Alias source `%1` has record with invalid alias \"%2\". This is likely due to an erroneous config. This alias will be discarded.")
|
||||||
|
}
|
||||||
141
kf_sources/AcediaCore/Classes/Aliases.uc
Normal file
141
kf_sources/AcediaCore/Classes/Aliases.uc
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Config object for `Aliases_Feature`.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Aliases extends FeatureConfig
|
||||||
|
perobjectconfig
|
||||||
|
config(AcediaAliases);
|
||||||
|
|
||||||
|
struct CustomSourceRecord
|
||||||
|
{
|
||||||
|
var string name;
|
||||||
|
var class<AliasSource> source;
|
||||||
|
};
|
||||||
|
|
||||||
|
var public config class<AliasSource> weaponAliasSource;
|
||||||
|
var public config class<AliasSource> colorAliasSource;
|
||||||
|
var public config class<AliasSource> featureAliasSource;
|
||||||
|
var public config class<AliasSource> entityAliasSource;
|
||||||
|
var public config class<AliasSource> commandAliasSource;
|
||||||
|
var public config array<CustomSourceRecord> customSource;
|
||||||
|
|
||||||
|
protected function HashTable ToData()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text nextKey;
|
||||||
|
local HashTable data, otherSourcesData;
|
||||||
|
|
||||||
|
data = __().collections.EmptyHashTable();
|
||||||
|
// Add named aliases
|
||||||
|
data.SetString(P("weapon"), string(weaponAliasSource));
|
||||||
|
data.SetString(P("color"), string(colorAliasSource));
|
||||||
|
data.SetString(P("feature"), string(featureAliasSource));
|
||||||
|
data.SetString(P("entity"), string(entityAliasSource));
|
||||||
|
data.SetString(P("command"), string(commandAliasSource));
|
||||||
|
// Add the rest
|
||||||
|
otherSourcesData = __().collections.EmptyHashTable();
|
||||||
|
for (i = 0; i < customSource.length; i += 1)
|
||||||
|
{
|
||||||
|
nextKey = _.text.FromString(customSource[i].name);
|
||||||
|
otherSourcesData.SetString(nextKey, string(customSource[i].source));
|
||||||
|
nextKey.FreeSelf();
|
||||||
|
}
|
||||||
|
data.SetItem(P("other"), otherSourcesData);
|
||||||
|
otherSourcesData.FreeSelf();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function FromData(HashTable source)
|
||||||
|
{
|
||||||
|
local HashTable otherSourcesData;
|
||||||
|
|
||||||
|
if (source == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We cast `class` into `string`
|
||||||
|
// (e.g. `string(class'AcediaAliases_Weapons')`)
|
||||||
|
// instead of writing full name of the class so that code is independent
|
||||||
|
// from this package's name, making it easier to change later
|
||||||
|
weaponAliasSource = class<AliasSource>(_.memory.LoadClass_S(
|
||||||
|
source.GetString(P("weapon"), string(class'WeaponAliasSource'))));
|
||||||
|
colorAliasSource = class<AliasSource>(_.memory.LoadClass_S(
|
||||||
|
source.GetString(P("color"), string(class'ColorAliasSource'))));
|
||||||
|
featureAliasSource = class<AliasSource>(_.memory.LoadClass_S(
|
||||||
|
source.GetString(P("feature"), string(class'FeatureAliasSource'))));
|
||||||
|
entityAliasSource = class<AliasSource>(_.memory.LoadClass_S(
|
||||||
|
source.GetString(P("entity"), string(class'EntityAliasSource'))));
|
||||||
|
commandAliasSource = class<AliasSource>(_.memory.LoadClass_S(
|
||||||
|
source.GetString(P("command"), string(class'CommandAliasSource'))));
|
||||||
|
otherSourcesData = source.GetHashTable(P("other"));
|
||||||
|
if (otherSourcesData != none)
|
||||||
|
{
|
||||||
|
ReadOtherSources(otherSourcesData);
|
||||||
|
otherSourcesData.FreeSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doesn't check whether `otherSources` is `none`
|
||||||
|
protected function ReadOtherSources(HashTable otherSourcesData)
|
||||||
|
{
|
||||||
|
local CustomSourceRecord newRecord;
|
||||||
|
local BaseText keyAsText, valueAsText;
|
||||||
|
local AcediaObject key, value;
|
||||||
|
local HashTableIterator iter;
|
||||||
|
|
||||||
|
customSource.length = 0;
|
||||||
|
iter = HashTableIterator(otherSourcesData.Iterate());
|
||||||
|
iter.LeaveOnlyNotNone();
|
||||||
|
for (iter = iter; !iter.HasFinished(); iter.Next())
|
||||||
|
{
|
||||||
|
key = iter.GetKey();
|
||||||
|
value = iter.GetKey();
|
||||||
|
keyAsText = BaseText(key);
|
||||||
|
valueAsText = BaseText(value);
|
||||||
|
if (keyAsText != none && valueAsText != none)
|
||||||
|
{
|
||||||
|
newRecord.name = keyAsText.ToString();
|
||||||
|
newRecord.source = class<AliasSource>(
|
||||||
|
_.memory.LoadClass_S(valueAsText.ToString()));
|
||||||
|
if (newRecord.source != none) {
|
||||||
|
customSource[customSource.length] = newRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(key);
|
||||||
|
_.memory.Free(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function DefaultIt()
|
||||||
|
{
|
||||||
|
customSource.length = 0;
|
||||||
|
weaponAliasSource = class'WeaponAliasSource';
|
||||||
|
colorAliasSource = class'ColorAliasSource';
|
||||||
|
featureAliasSource = class'FeatureAliasSource';
|
||||||
|
entityAliasSource = class'EntityAliasSource';
|
||||||
|
commandAliasSource = class'CommandAliasSource';
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configName = "AcediaAliases"
|
||||||
|
weaponAliasSource = class'WeaponAliasSource'
|
||||||
|
colorAliasSource = class'ColorAliasSource'
|
||||||
|
featureAliasSource = class'FeatureAliasSource'
|
||||||
|
entityAliasSource = class'EntityAliasSource'
|
||||||
|
commandAliasSource = class'CommandAliasSource'
|
||||||
|
}
|
||||||
420
kf_sources/AcediaCore/Classes/AliasesAPI.uc
Normal file
420
kf_sources/AcediaCore/Classes/AliasesAPI.uc
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
/**
|
||||||
|
* Provides convenient access to Aliases-related functions.
|
||||||
|
* Copyright 2020-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AliasesAPI extends AcediaObject
|
||||||
|
dependson(LoggerAPI);
|
||||||
|
|
||||||
|
// To avoid bothering with fetching `Aliases_Feature` each time we need to
|
||||||
|
// access an alias source, we save all the basic ones separately and
|
||||||
|
// `Aliases_Feature` can simply trigger their updates whenever necessary via
|
||||||
|
// `AliasesAPI._reloadSources()` function.
|
||||||
|
var private BaseAliasSource weaponAliasSource;
|
||||||
|
var private BaseAliasSource colorAliasSource;
|
||||||
|
var private BaseAliasSource featureAliasSource;
|
||||||
|
var private BaseAliasSource entityAliasSource;
|
||||||
|
var private BaseAliasSource commandAliasSource;
|
||||||
|
|
||||||
|
public function _reloadSources()
|
||||||
|
{
|
||||||
|
local Aliases_Feature feature;
|
||||||
|
|
||||||
|
_.memory.Free(weaponAliasSource);
|
||||||
|
_.memory.Free(colorAliasSource);
|
||||||
|
_.memory.Free(featureAliasSource);
|
||||||
|
_.memory.Free(entityAliasSource);
|
||||||
|
_.memory.Free(commandAliasSource);
|
||||||
|
weaponAliasSource = none;
|
||||||
|
colorAliasSource = none;
|
||||||
|
featureAliasSource = none;
|
||||||
|
entityAliasSource = none;
|
||||||
|
commandAliasSource = none;
|
||||||
|
feature = Aliases_Feature(
|
||||||
|
class'Aliases_Feature'.static.GetEnabledInstance());
|
||||||
|
if (feature == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
weaponAliasSource = feature.GetWeaponSource();
|
||||||
|
colorAliasSource = feature.GetColorSource();
|
||||||
|
featureAliasSource = feature.GetFeatureSource();
|
||||||
|
entityAliasSource = feature.GetEntitySource();
|
||||||
|
commandAliasSource = feature.GetCommandSource();
|
||||||
|
_.memory.Free(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an easier access to the instance of the custom `BaseAliasSource`
|
||||||
|
* with a given name `sourceName`.
|
||||||
|
*
|
||||||
|
* Custom alias sources can be added manually by the admin through the
|
||||||
|
* `Aliases_Feature` config file.
|
||||||
|
*
|
||||||
|
* @param sourceName Name that alias source was added as to
|
||||||
|
* `Aliases_Feature`.
|
||||||
|
* @return Instance of the requested `BaseAliasSource`,
|
||||||
|
* `none` if `sourceName` is `none`, does not refer to any alias source
|
||||||
|
* or `Aliases_Feature` is disabled.
|
||||||
|
*/
|
||||||
|
public final function BaseAliasSource GetCustomSource(BaseText sourceName)
|
||||||
|
{
|
||||||
|
local Aliases_Feature feature;
|
||||||
|
|
||||||
|
if (sourceName == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
feature =
|
||||||
|
Aliases_Feature(class'Aliases_Feature'.static.GetEnabledInstance());
|
||||||
|
if (feature != none) {
|
||||||
|
return feature.GetCustomSource(sourceName);
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `BaseAliasSource` that is designated in configuration files as
|
||||||
|
* a source for weapon aliases.
|
||||||
|
*
|
||||||
|
* @return Reference to the `BaseAliasSource` that contains weapon aliases.
|
||||||
|
* Can return `none` if no source for weapons was configured or
|
||||||
|
* the configured source is incorrectly defined.
|
||||||
|
*/
|
||||||
|
public final function BaseAliasSource GetWeaponSource()
|
||||||
|
{
|
||||||
|
if (weaponAliasSource != none) {
|
||||||
|
weaponAliasSource.NewRef();
|
||||||
|
}
|
||||||
|
return weaponAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `BaseAliasSource` that is designated in configuration files as
|
||||||
|
* a source for color aliases.
|
||||||
|
*
|
||||||
|
* @return Reference to the `BaseAliasSource` that contains color aliases.
|
||||||
|
* Can return `none` if no source for colors was configured or
|
||||||
|
* the configured source is incorrectly defined.
|
||||||
|
*/
|
||||||
|
public final function BaseAliasSource GetColorSource()
|
||||||
|
{
|
||||||
|
if (colorAliasSource != none) {
|
||||||
|
colorAliasSource.NewRef();
|
||||||
|
}
|
||||||
|
return colorAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `BaseAliasSource` that is designated in configuration files as
|
||||||
|
* a source for feature aliases.
|
||||||
|
*
|
||||||
|
* @return Reference to the `BaseAliasSource` that contains feature aliases.
|
||||||
|
* Can return `none` if no source for features was configured or
|
||||||
|
* the configured source is incorrectly defined.
|
||||||
|
*/
|
||||||
|
public final function BaseAliasSource GetFeatureSource()
|
||||||
|
{
|
||||||
|
if (featureAliasSource != none) {
|
||||||
|
featureAliasSource.NewRef();
|
||||||
|
}
|
||||||
|
return featureAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `BaseAliasSource` that is designated in configuration files as
|
||||||
|
* a source for entity aliases.
|
||||||
|
*
|
||||||
|
* @return Reference to the `BaseAliasSource` that contains entity aliases.
|
||||||
|
* Can return `none` if no source for entities was configured or
|
||||||
|
* the configured source is incorrectly defined.
|
||||||
|
*/
|
||||||
|
public final function BaseAliasSource GetEntitySource()
|
||||||
|
{
|
||||||
|
if (entityAliasSource != none) {
|
||||||
|
entityAliasSource.NewRef();
|
||||||
|
}
|
||||||
|
return entityAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `BaseAliasSource` that is designated in configuration files as
|
||||||
|
* a source for command aliases.
|
||||||
|
*
|
||||||
|
* Command aliases values can have the format of "<command>" or
|
||||||
|
* "<command> <sub-command>".
|
||||||
|
*
|
||||||
|
* @return Reference to the `BaseAliasSource` that contains command aliases.
|
||||||
|
* Can return `none` if no source for command was configured or
|
||||||
|
* the configured source is incorrectly defined.
|
||||||
|
*/
|
||||||
|
public final function BaseAliasSource GetCommandSource()
|
||||||
|
{
|
||||||
|
if (commandAliasSource != none) {
|
||||||
|
commandAliasSource.NewRef();
|
||||||
|
}
|
||||||
|
return commandAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function Text ResolveWithSource(
|
||||||
|
BaseText alias,
|
||||||
|
BaseAliasSource source,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
local Text result;
|
||||||
|
local Text trimmedAlias;
|
||||||
|
|
||||||
|
if (alias == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
if (alias.StartsWith(P("$"))) {
|
||||||
|
trimmedAlias = alias.Copy(1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
trimmedAlias = alias.Copy();
|
||||||
|
}
|
||||||
|
if (source != none) {
|
||||||
|
result = source.Resolve(trimmedAlias, copyOnFailure);
|
||||||
|
}
|
||||||
|
else if (copyOnFailure) {
|
||||||
|
result = trimmedAlias.Copy();
|
||||||
|
}
|
||||||
|
trimmedAlias.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to look up a value stored for given alias in an `BaseAliasSource`
|
||||||
|
* configured to store weapon aliases.
|
||||||
|
*
|
||||||
|
* In Acedia aliases are typically prefixed with '$' to indicate that user
|
||||||
|
* means to enter alias. This method is able to handle both aliases with and
|
||||||
|
* without that prefix. This does not lead to conflicts, because '$' cannot
|
||||||
|
* be a valid part of any alias.
|
||||||
|
*
|
||||||
|
* Lookup of alias can fail if either alias does not exist in weapon alias
|
||||||
|
* source or weapon alias source itself does not exist
|
||||||
|
* (due to either faulty configuration or incorrect definition).
|
||||||
|
* To determine if weapon alias source exists you can check
|
||||||
|
* `_.alias.GetWeaponSource()` value.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to
|
||||||
|
* look up a value. Case-insensitive.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`. If `alias` was specified with '$' prefix -
|
||||||
|
* it will be discarded.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return `none`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias.Copy()`.
|
||||||
|
* If `alias == none` method always returns `none`.
|
||||||
|
*/
|
||||||
|
public final function Text ResolveWeapon(
|
||||||
|
BaseText alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
return ResolveWithSource(alias, weaponAliasSource, copyOnFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to look up a value stored for given alias in an `BaseAliasSource`
|
||||||
|
* configured to store color aliases.
|
||||||
|
*
|
||||||
|
* In Acedia aliases are typically prefixed with '$' to indicate that user
|
||||||
|
* means to enter alias. This method is able to handle both aliases with and
|
||||||
|
* without that prefix. This does not lead to conflicts, because '$' cannot
|
||||||
|
* be a valid part of any alias.
|
||||||
|
*
|
||||||
|
* Lookup of alias can fail if either alias does not exist in color alias
|
||||||
|
* source or color alias source itself does not exist
|
||||||
|
* (due to either faulty configuration or incorrect definition).
|
||||||
|
* To determine if color alias source exists you can check
|
||||||
|
* `_.alias.GetColorSource()` value.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to
|
||||||
|
* look up a value. Case-insensitive.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`. If `alias` was specified with '$' prefix -
|
||||||
|
* it will be discarded.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return `none`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias.Copy()`.
|
||||||
|
* If `alias == none` method always returns `none`.
|
||||||
|
*/
|
||||||
|
public final function Text ResolveColor(
|
||||||
|
BaseText alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
return ResolveWithSource(alias, colorAliasSource, copyOnFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to look up a value stored for given alias in an `BaseAliasSource`
|
||||||
|
* configured to store feature aliases.
|
||||||
|
*
|
||||||
|
* In Acedia aliases are typically prefixed with '$' to indicate that user
|
||||||
|
* means to enter alias. This method is able to handle both aliases with and
|
||||||
|
* without that prefix. This does not lead to conflicts, because '$' cannot
|
||||||
|
* be a valid part of any alias.
|
||||||
|
*
|
||||||
|
* Lookup of alias can fail if either alias does not exist in feature alias
|
||||||
|
* source or feature alias source itself does not exist
|
||||||
|
* (due to either faulty configuration or incorrect definition).
|
||||||
|
* To determine if feature alias source exists you can check
|
||||||
|
* `_.alias.GetFeatureSource()` value.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to
|
||||||
|
* look up a value. Case-insensitive.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`. If `alias` was specified with '$' prefix -
|
||||||
|
* it will be discarded.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return `none`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias.Copy()`.
|
||||||
|
* If `alias == none` method always returns `none`.
|
||||||
|
*/
|
||||||
|
public final function Text ResolveFeature(
|
||||||
|
BaseText alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
return ResolveWithSource(alias, featureAliasSource, copyOnFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to look up a value stored for given alias in an `BaseAliasSource`
|
||||||
|
* configured to store entity aliases.
|
||||||
|
*
|
||||||
|
* In Acedia aliases are typically prefixed with '$' to indicate that user
|
||||||
|
* means to enter alias. This method is able to handle both aliases with and
|
||||||
|
* without that prefix. This does not lead to conflicts, because '$' cannot
|
||||||
|
* be a valid part of any alias.
|
||||||
|
*
|
||||||
|
* Lookup of alias can fail if either alias does not exist in entity alias
|
||||||
|
* source or entity alias source itself does not exist
|
||||||
|
* (due to either faulty configuration or incorrect definition).
|
||||||
|
* To determine if entity alias source exists you can check
|
||||||
|
* `_.alias.GetEntitySource()` value.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to
|
||||||
|
* look up a value. Case-insensitive.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`. If `alias` was specified with '$' prefix -
|
||||||
|
* it will be discarded.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return `none`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias.Copy()`.
|
||||||
|
* If `alias == none` method always returns `none`.
|
||||||
|
*/
|
||||||
|
public final function Text ResolveEntity(
|
||||||
|
BaseText alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
return ResolveWithSource(alias, entityAliasSource, copyOnFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to look up a value stored for given alias in an `BaseAliasSource`
|
||||||
|
* configured to store command aliases.
|
||||||
|
*
|
||||||
|
* Command aliases values can have the format of "<command>" or
|
||||||
|
* "<command> <sub-command>".
|
||||||
|
*
|
||||||
|
* In Acedia aliases are typically prefixed with '$' to indicate that user
|
||||||
|
* means to enter alias. This method is able to handle both aliases with and
|
||||||
|
* without that prefix. This does not lead to conflicts, because '$' cannot
|
||||||
|
* be a valid part of any alias.
|
||||||
|
*
|
||||||
|
* Lookup of alias can fail if either alias does not exist in command alias
|
||||||
|
* source or command alias source itself does not exist
|
||||||
|
* (due to either faulty configuration or incorrect definition).
|
||||||
|
* To determine if command alias source exists you can check
|
||||||
|
* `_.alias.GetCommandSource()` value.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to
|
||||||
|
* look up a value. Case-insensitive.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`. If `alias` was specified with '$' prefix -
|
||||||
|
* it will be discarded.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return `none`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias.Copy()`.
|
||||||
|
* If `alias == none` method always returns `none`.
|
||||||
|
*/
|
||||||
|
public final function Text ResolveCommand(
|
||||||
|
BaseText alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
return ResolveWithSource(alias, commandAliasSource, copyOnFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to look up a value stored for given alias in a custom alias source
|
||||||
|
* with a given name `sourceName`.
|
||||||
|
*
|
||||||
|
* In Acedia aliases are typically prefixed with '$' to indicate that user
|
||||||
|
* means to enter alias. This method is able to handle both aliases with and
|
||||||
|
* without that prefix. This does not lead to conflicts, because '$' cannot
|
||||||
|
* be a valid part of any alias.
|
||||||
|
*
|
||||||
|
* Custom alias sources are any type of alias source that isn't built-in into
|
||||||
|
* Acedia. They can either be added manually by the admin through config file.
|
||||||
|
*
|
||||||
|
* Lookup of alias can fail if either alias does not exist in specified
|
||||||
|
* alias source or that alias source itself does not exist.
|
||||||
|
* To determine if specified alias source exists you can check
|
||||||
|
* `_.alias.GetCustomSource()` value.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to
|
||||||
|
* look up a value. Case-insensitive.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`. If `alias` was specified with '$' prefix -
|
||||||
|
* it will be discarded.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return `none`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias.Copy()`.
|
||||||
|
* If `alias == none` method always returns `none`.
|
||||||
|
*/
|
||||||
|
public final function Text ResolveCustom(
|
||||||
|
BaseText sourceName,
|
||||||
|
BaseText alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
local BaseAliasSource customSource;
|
||||||
|
|
||||||
|
customSource = GetCustomSource(sourceName);
|
||||||
|
if (customSource == none)
|
||||||
|
{
|
||||||
|
if (copyOnFailure && alias != none) {
|
||||||
|
return alias.Copy();
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
return ResolveWithSource(alias, customSource, copyOnFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
205
kf_sources/AcediaCore/Classes/AliasesStorage.uc
Normal file
205
kf_sources/AcediaCore/Classes/AliasesStorage.uc
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* This is a simple helper object for `AliasSource` that can store
|
||||||
|
* an array of aliases in config files in a per-object-config manner.
|
||||||
|
* One `AliasesStorage` object can store several aliases for a single
|
||||||
|
* value.
|
||||||
|
* It is recommended that you do not try to access these objects directly.
|
||||||
|
* `AliasesStorage` is abstract, so it can never be created, for any
|
||||||
|
* actual use create a child class. Suggested name scheme is
|
||||||
|
* "<something>Aliases", like "ColorAliases" or "WeaponAliases".
|
||||||
|
* It's only interesting function is storing '.'s as ':' in it's config,
|
||||||
|
* which is necessary to allow storing aliases for class names via
|
||||||
|
* these objects (since UnrealScript's cannot handle '.'s in object's names
|
||||||
|
* in it's configs).
|
||||||
|
* [INTERNAL] This is an internal object and should not be directly modified,
|
||||||
|
* its only allowed use is documented way to create new alias sources.
|
||||||
|
* Copyright 2019-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AliasesStorage extends AcediaObject
|
||||||
|
perObjectConfig
|
||||||
|
config(AcediaAliases)
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
// Aliases, recorded by this `AliasesStorage` object that all refer to
|
||||||
|
// the same value, defined by this object's name `string(self.name)`
|
||||||
|
var protected config array<string> alias;
|
||||||
|
|
||||||
|
// Name of the configurational file (without extension) where
|
||||||
|
// this `AliasSource`'s data will be stored
|
||||||
|
var protected const string configName;
|
||||||
|
|
||||||
|
// Link to the `AliasSource` that uses `AliasesStorage` objects of this class.
|
||||||
|
// To ensure that any `AliasesStorage` sub-class only belongs to one
|
||||||
|
// `AliasSource`.
|
||||||
|
var public const class<AliasSource> sourceClass;
|
||||||
|
|
||||||
|
// `true` means that this `AliasesStorage` object is awaiting saving into its
|
||||||
|
// config
|
||||||
|
var private bool pendingSaveToConfig;
|
||||||
|
|
||||||
|
// Since:
|
||||||
|
// 1. '.'s in values are converted into ':' for storage purposes;
|
||||||
|
// 2. We have to store values in `string` to make use of config files.
|
||||||
|
// we need methods to convert between "storage" (`string`)
|
||||||
|
// and "actual" (`Text`) value version.
|
||||||
|
// `ToStorageVersion()` and `ToActualVersion()` do that.
|
||||||
|
private final static function string ToStorageVersion(BaseText actualValue)
|
||||||
|
{
|
||||||
|
return Repl(actualValue.ToString(), ".", ":");
|
||||||
|
}
|
||||||
|
|
||||||
|
// See comment to `ToStorageVersion()`
|
||||||
|
private final static function Text ToActualVersion(string storageValue)
|
||||||
|
{
|
||||||
|
return __().text.FromString(Repl(storageValue, ":", "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all `AliasesStorage` objects from their config file
|
||||||
|
* (defined in paired `AliasSource` class).
|
||||||
|
*
|
||||||
|
* This is an internal method and returned `AliasesStorage` objects require
|
||||||
|
* a special treatment: they aren't meant to be used with Acedia's reference
|
||||||
|
* counting - never release their references.
|
||||||
|
*
|
||||||
|
* @return Array of all `Aliases` objects, loaded from their config file.
|
||||||
|
*/
|
||||||
|
public static final function array<AliasesStorage> LoadAllObjects()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<string> objectNames;
|
||||||
|
local array<AliasesStorage> loadedAliasObjects;
|
||||||
|
|
||||||
|
objectNames = GetPerObjectNames(default.configName,
|
||||||
|
string(default.class.name), MaxInt);
|
||||||
|
for (i = 0; i < objectNames.length; i += 1) {
|
||||||
|
loadedAliasObjects[i] = LoadObjectByName(objectNames[i]);
|
||||||
|
}
|
||||||
|
return loadedAliasObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads a new `AliasesStorage` object by it's given name (`objectName`).
|
||||||
|
private static final function AliasesStorage LoadObjectByName(
|
||||||
|
string objectName)
|
||||||
|
{
|
||||||
|
local AliasesStorage result;
|
||||||
|
|
||||||
|
// Since `MemoryAPI` for now does not support specifying names
|
||||||
|
// to created objects - do some manual dark magic and
|
||||||
|
// initialize this shit ourselves. Don't repeat this at home, kids.
|
||||||
|
result = new(none, objectName) default.class;
|
||||||
|
result._constructor();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a new `AliasesStorage` object based on the value (`aliasesValue`)
|
||||||
|
* of it's aliases.
|
||||||
|
*
|
||||||
|
* @param aliasesValue Value that aliases in this `Aliases` object will
|
||||||
|
* correspond to.
|
||||||
|
* @return Instance of `AliasesStorage` object with a given name.
|
||||||
|
*/
|
||||||
|
public static final function AliasesStorage LoadObject(BaseText aliasesValue)
|
||||||
|
{
|
||||||
|
if (aliasesValue != none) {
|
||||||
|
return LoadObjectByName(ToStorageVersion(aliasesValue));
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns value that caller's `Aliases` object's aliases point to.
|
||||||
|
*
|
||||||
|
* @return Value, stored by this object.
|
||||||
|
*/
|
||||||
|
public final function Text GetValue()
|
||||||
|
{
|
||||||
|
return ToActualVersion(string(self.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array of aliases that caller `AliasesStorage` tells us point to
|
||||||
|
* it's value.
|
||||||
|
*
|
||||||
|
* @return Array of all aliases, stored by caller `AliasesStorage` object.
|
||||||
|
*/
|
||||||
|
public final function array<Text> GetAliases()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<Text> textAliases;
|
||||||
|
|
||||||
|
for (i = 0; i < alias.length; i += 1) {
|
||||||
|
textAliases[i] = _.text.FromString(alias[i]);
|
||||||
|
}
|
||||||
|
return textAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [For inner use by `AliasSource`] Removes alias from this object.
|
||||||
|
*
|
||||||
|
* Does not update relevant `AliasHash`.
|
||||||
|
*
|
||||||
|
* Will prevent adding duplicate records inside it's own storage.
|
||||||
|
*
|
||||||
|
* @param aliasToRemove Alias to remove from caller `AliasesStorage` object.
|
||||||
|
* Expected to be in lower case.
|
||||||
|
*/
|
||||||
|
public final function RemoveAlias_S(string aliasToRemove)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local bool removedAlias;
|
||||||
|
|
||||||
|
while (i < alias.length)
|
||||||
|
{
|
||||||
|
if (aliasToRemove ~= alias[i])
|
||||||
|
{
|
||||||
|
alias.Remove(i, 1);
|
||||||
|
removedAlias = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!pendingSaveToConfig)
|
||||||
|
{
|
||||||
|
pendingSaveToConfig = true;
|
||||||
|
_.scheduler.RequestDiskAccess(self).connect = SaveOrClear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this object still has any alias records, - forces a rewrite of it's data
|
||||||
|
* into the config file, otherwise - removes it's record entirely.
|
||||||
|
*/
|
||||||
|
private final function SaveOrClear()
|
||||||
|
{
|
||||||
|
if (alias.length <= 0) {
|
||||||
|
ClearConfig();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SaveConfig();
|
||||||
|
}
|
||||||
|
pendingSaveToConfig = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
sourceClass = class'AliasSource'
|
||||||
|
configName = "AcediaAliases"
|
||||||
|
}
|
||||||
263
kf_sources/AcediaCore/Classes/Aliases_Feature.uc
Normal file
263
kf_sources/AcediaCore/Classes/Aliases_Feature.uc
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* This feature provides a mechanism to define commands that automatically
|
||||||
|
* parse their arguments into standard Acedia collection. It also allows to
|
||||||
|
* manage them (and specify limitation on how they can be called) in a
|
||||||
|
* centralized manner.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Aliases_Feature extends Feature
|
||||||
|
dependson(Aliases);
|
||||||
|
|
||||||
|
struct ClassSourcePair
|
||||||
|
{
|
||||||
|
var class<AliasSource> class;
|
||||||
|
var AliasSource source;
|
||||||
|
};
|
||||||
|
// We don't want to reload `AliasSource`s several times when this feature is
|
||||||
|
// disabled and re-enabled or its config is swapped, so we keep all loaded
|
||||||
|
// sources here at all times and look them up from here
|
||||||
|
var private array<ClassSourcePair> loadedSources;
|
||||||
|
|
||||||
|
// Loaded `AliasSource`s
|
||||||
|
var private AliasSource weaponAliasSource;
|
||||||
|
var private AliasSource colorAliasSource;
|
||||||
|
var private AliasSource featureAliasSource;
|
||||||
|
var private AliasSource entityAliasSource;
|
||||||
|
var private AliasSource commandAliasSource;
|
||||||
|
// Everything else
|
||||||
|
var private HashTable customSources;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition errCannotLoadAliasSource, errEmptyName;
|
||||||
|
var private LoggerAPI.Definition errDuplicateCustomSource;
|
||||||
|
|
||||||
|
protected function OnEnabled()
|
||||||
|
{
|
||||||
|
_.alias._reloadSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function OnDisabled()
|
||||||
|
{
|
||||||
|
DropSources();
|
||||||
|
_.alias._reloadSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function DropSources()
|
||||||
|
{
|
||||||
|
_.memory.Free(weaponAliasSource);
|
||||||
|
weaponAliasSource = none;
|
||||||
|
_.memory.Free(colorAliasSource);
|
||||||
|
colorAliasSource = none;
|
||||||
|
_.memory.Free(featureAliasSource);
|
||||||
|
featureAliasSource = none;
|
||||||
|
_.memory.Free(entityAliasSource);
|
||||||
|
entityAliasSource = none;
|
||||||
|
_.memory.Free(commandAliasSource);
|
||||||
|
commandAliasSource = none;
|
||||||
|
_.memory.Free(customSources);
|
||||||
|
customSources = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create each `AliasSource` instance only once to avoid any possible
|
||||||
|
// nonsense with loading named objects several times: alias sources don't use
|
||||||
|
// `AcediaConfig`s, so they don't automatically avoid named config reloading
|
||||||
|
private function AliasSource GetSource(class<AliasSource> sourceClass)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local AliasSource newSource;
|
||||||
|
local ClassSourcePair newPair;
|
||||||
|
|
||||||
|
if (sourceClass == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
for (i = 0; i < loadedSources.length; i += 1)
|
||||||
|
{
|
||||||
|
if (loadedSources[i].class == sourceClass) {
|
||||||
|
return loadedSources[i].source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newSource = AliasSource(_.memory.Allocate(sourceClass));
|
||||||
|
if (newSource != none)
|
||||||
|
{
|
||||||
|
newPair.class = sourceClass;
|
||||||
|
newPair.source = newSource;
|
||||||
|
// One reference we store, one we return
|
||||||
|
newSource.NewRef();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_.logger.Auto(errCannotLoadAliasSource).ArgClass(sourceClass);
|
||||||
|
}
|
||||||
|
return newSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function SwapConfig(FeatureConfig config)
|
||||||
|
{
|
||||||
|
local Aliases newConfig;
|
||||||
|
|
||||||
|
newConfig = Aliases(config);
|
||||||
|
if (newConfig == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
weaponAliasSource = GetSource(newConfig.weaponAliasSource);
|
||||||
|
colorAliasSource = GetSource(newConfig.colorAliasSource);
|
||||||
|
featureAliasSource = GetSource(newConfig.featureAliasSource);
|
||||||
|
entityAliasSource = GetSource(newConfig.entityAliasSource);
|
||||||
|
commandAliasSource = GetSource(newConfig.commandAliasSource);
|
||||||
|
LoadCustomSources(newConfig.customSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LoadCustomSources(
|
||||||
|
array<Aliases.CustomSourceRecord> configCustomSources)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local bool reportedEmptyName;
|
||||||
|
local Text nextKey;
|
||||||
|
local AliasSource nextSource, conflictingSource;
|
||||||
|
|
||||||
|
_.memory.Free(customSources);
|
||||||
|
customSources = _.collections.EmptyHashTable();
|
||||||
|
for (i = 0; i < configCustomSources.length; i += 1)
|
||||||
|
{
|
||||||
|
if (configCustomSources[i].name == "" && !reportedEmptyName)
|
||||||
|
{
|
||||||
|
reportedEmptyName = true;
|
||||||
|
_.logger.Auto(errEmptyName);
|
||||||
|
}
|
||||||
|
nextKey = _.text.FromString(configCustomSources[i].name);
|
||||||
|
// We only store `AliasSource`s
|
||||||
|
conflictingSource = AliasSource(customSources.GetItem(nextKey));
|
||||||
|
if (conflictingSource != none)
|
||||||
|
{
|
||||||
|
_.logger.Auto(errDuplicateCustomSource)
|
||||||
|
.ArgClass(conflictingSource.class)
|
||||||
|
.Arg(nextKey) // Releases `nextKey`
|
||||||
|
.ArgClass(configCustomSources[i].source);
|
||||||
|
conflictingSource.FreeSelf();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextSource = GetSource(configCustomSources[i].source);
|
||||||
|
if (nextSource != none)
|
||||||
|
{
|
||||||
|
customSources.SetItem(nextKey, nextSource);
|
||||||
|
nextSource.FreeSelf();
|
||||||
|
}
|
||||||
|
nextKey.FreeSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `AliasSource` for weapon aliases.
|
||||||
|
*
|
||||||
|
* @return `AliasSource`, configured to store weapon aliases.
|
||||||
|
*/
|
||||||
|
public function AliasSource GetWeaponSource()
|
||||||
|
{
|
||||||
|
if (weaponAliasSource != none) {
|
||||||
|
weaponAliasSource.Newref();
|
||||||
|
}
|
||||||
|
return weaponAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `AliasSource` for color aliases.
|
||||||
|
*
|
||||||
|
* @return `AliasSource`, configured to store color aliases.
|
||||||
|
*/
|
||||||
|
public function AliasSource GetColorSource()
|
||||||
|
{
|
||||||
|
if (colorAliasSource != none) {
|
||||||
|
colorAliasSource.Newref();
|
||||||
|
}
|
||||||
|
return colorAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `AliasSource` for feature aliases.
|
||||||
|
*
|
||||||
|
* @return `AliasSource`, configured to store feature aliases.
|
||||||
|
*/
|
||||||
|
public function AliasSource GetFeatureSource()
|
||||||
|
{
|
||||||
|
if (featureAliasSource != none) {
|
||||||
|
featureAliasSource.Newref();
|
||||||
|
}
|
||||||
|
return featureAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `AliasSource` for entity aliases.
|
||||||
|
*
|
||||||
|
* @return `AliasSource`, configured to store entity aliases.
|
||||||
|
*/
|
||||||
|
public function AliasSource GetEntitySource()
|
||||||
|
{
|
||||||
|
if (entityAliasSource != none) {
|
||||||
|
entityAliasSource.Newref();
|
||||||
|
}
|
||||||
|
return entityAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `AliasSource` for command aliases.
|
||||||
|
*
|
||||||
|
* @return `AliasSource`, configured to store command aliases.
|
||||||
|
*/
|
||||||
|
public function AliasSource GetCommandSource()
|
||||||
|
{
|
||||||
|
if (commandAliasSource != none) {
|
||||||
|
commandAliasSource.Newref();
|
||||||
|
}
|
||||||
|
return commandAliasSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns custom `AliasSource` with a given name.
|
||||||
|
*
|
||||||
|
* @return Custom `AliasSource`, configured with a given name `sourceName`.
|
||||||
|
*/
|
||||||
|
public function AliasSource GetCustomSource(BaseText sourceName)
|
||||||
|
{
|
||||||
|
if (sourceName == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
// We only store `AliasSource`s
|
||||||
|
return AliasSource(customSources.GetItem(sourceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns custom `AliasSource` with a given name `sourceName.
|
||||||
|
*
|
||||||
|
* @return Custom `AliasSource`, configured with a given name `sourceName`.
|
||||||
|
*/
|
||||||
|
public function AliasSource GetCustomSource_S(string sourceName)
|
||||||
|
{
|
||||||
|
local Text wrapper;
|
||||||
|
local AliasSource result;
|
||||||
|
|
||||||
|
wrapper = _.text.FromString(sourceName);
|
||||||
|
result = GetCustomSource(wrapper);
|
||||||
|
wrapper.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configClass = class'Aliases'
|
||||||
|
errEmptyName = (l=LOG_Error,m="Empty name provided for the custom alias source. This is likely due to an erroneous config.")
|
||||||
|
errCannotLoadAliasSource = (l=LOG_Error,m="Failed to load alias source class `%1`.")
|
||||||
|
errDuplicateCustomSource = (l=LOG_Error,m="Custom alias source `%1` is already registered with name '%2'. Alias source `%3` with the same name will be ignored.")
|
||||||
|
}
|
||||||
1245
kf_sources/AcediaCore/Classes/ArrayList.uc
Normal file
1245
kf_sources/AcediaCore/Classes/ArrayList.uc
Normal file
File diff suppressed because it is too large
Load Diff
86
kf_sources/AcediaCore/Classes/ArrayListIterator.uc
Normal file
86
kf_sources/AcediaCore/Classes/ArrayListIterator.uc
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Iterator for iterating over `ArrayList`'s items.
|
||||||
|
* Copyright 2022-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ArrayListIterator extends CollectionIterator
|
||||||
|
dependson(ArrayList);
|
||||||
|
|
||||||
|
var private ArrayList relevantCollection;
|
||||||
|
var private int currentIndex;
|
||||||
|
|
||||||
|
var private bool skipNoneReferences;
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
relevantCollection = none;
|
||||||
|
skipNoneReferences = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bool Initialize(Collection relevantArray)
|
||||||
|
{
|
||||||
|
currentIndex = 0;
|
||||||
|
relevantCollection = ArrayList(relevantArray);
|
||||||
|
if (relevantCollection == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function LeaveOnlyNotNone()
|
||||||
|
{
|
||||||
|
skipNoneReferences = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bool Next()
|
||||||
|
{
|
||||||
|
local int collectionLength;
|
||||||
|
|
||||||
|
if (!skipNoneReferences)
|
||||||
|
{
|
||||||
|
currentIndex += 1;
|
||||||
|
return HasFinished();
|
||||||
|
}
|
||||||
|
collectionLength = relevantCollection.GetLength();
|
||||||
|
while (currentIndex < collectionLength)
|
||||||
|
{
|
||||||
|
currentIndex += 1;
|
||||||
|
if (!relevantCollection.IsNone(currentIndex)) {
|
||||||
|
return HasFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HasFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function AcediaObject Get()
|
||||||
|
{
|
||||||
|
return relevantCollection.GetItem(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function AcediaObject GetKey()
|
||||||
|
{
|
||||||
|
return _.box.int(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bool HasFinished()
|
||||||
|
{
|
||||||
|
return currentIndex >= relevantCollection.GetLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
110
kf_sources/AcediaCore/Classes/Avarice.uc
Normal file
110
kf_sources/AcediaCore/Classes/Avarice.uc
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Config object for `Avarice_Feature`.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Avarice extends FeatureConfig
|
||||||
|
perobjectconfig
|
||||||
|
config(AcediaAvarice);
|
||||||
|
|
||||||
|
struct AvariceLinkRecord
|
||||||
|
{
|
||||||
|
var string name;
|
||||||
|
var string address;
|
||||||
|
};
|
||||||
|
// List all the names (and addresses) of all Avarice instances Acedia must
|
||||||
|
// connect to.
|
||||||
|
// `name` parameter is a useful (case-insensitive) identifier that
|
||||||
|
// can be used in other configs to point at each link.
|
||||||
|
// `address` must have form "host:port", where "host" is either ip or
|
||||||
|
// domain name and "port" is a numerical port value.
|
||||||
|
var public config array<AvariceLinkRecord> link;
|
||||||
|
|
||||||
|
// In case Avarice utility is launched after link started trying open
|
||||||
|
// the connection - that connection attempt will fail. To fix that link must
|
||||||
|
// try connecting again.
|
||||||
|
// This variable sets the time (in seconds), after which link will
|
||||||
|
// re-attempt opening connection. Setting value too low can prevent any
|
||||||
|
// connection from opening, setting it too high might make you wait for
|
||||||
|
// connection too long.
|
||||||
|
var public config float reconnectTime;
|
||||||
|
|
||||||
|
protected function HashTable ToData()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local HashTable data;
|
||||||
|
local HashTable linkData;
|
||||||
|
local ArrayList linksArray;
|
||||||
|
data = __().collections.EmptyHashTable();
|
||||||
|
data.SetFloat(P("reconnectTime"), reconnectTime, true);
|
||||||
|
linksArray = __().collections.EmptyArrayList();
|
||||||
|
data.SetItem(P("link"), linksArray);
|
||||||
|
for (i = 0; i < link.length; i += 1)
|
||||||
|
{
|
||||||
|
linkData = __().collections.EmptyHashTable();
|
||||||
|
linkData.SetString(P("name"), link[i].name);
|
||||||
|
linkData.SetString(P("address"), link[i].address);
|
||||||
|
linksArray.AddItem(linkData);
|
||||||
|
linkData.FreeSelf();
|
||||||
|
}
|
||||||
|
linksArray.FreeSelf();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function FromData(HashTable source)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local ArrayList linksArray;
|
||||||
|
local HashTable nextLink;
|
||||||
|
local AvariceLinkRecord nextRecord;
|
||||||
|
if (source == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reconnectTime = source.GetFloat(P("reconnectTime"));
|
||||||
|
link.length = 0;
|
||||||
|
linksArray = source.GetArrayList(P("link"));
|
||||||
|
if (linksArray == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i = 0; i < linksArray.GetLength(); i += 1)
|
||||||
|
{
|
||||||
|
nextLink = linksArray.GetHashTable(i);
|
||||||
|
if (nextLink == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextRecord.name = nextLink.GetString(P("name"));
|
||||||
|
nextRecord.address = nextLink.GetString(P("address"));
|
||||||
|
link[i] = nextRecord;
|
||||||
|
_.memory.Free(nextLink);
|
||||||
|
}
|
||||||
|
_.memory.Free(linksArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function DefaultIt()
|
||||||
|
{
|
||||||
|
local AvariceLinkRecord defaultRecord;
|
||||||
|
reconnectTime = 10.0;
|
||||||
|
link.length = 0;
|
||||||
|
defaultRecord.name = "avarice";
|
||||||
|
defaultRecord.address = "127.0.0.1:1234";
|
||||||
|
link[0] = defaultRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configName = "AcediaAvarice"
|
||||||
|
}
|
||||||
77
kf_sources/AcediaCore/Classes/AvariceAPI.uc
Normal file
77
kf_sources/AcediaCore/Classes/AvariceAPI.uc
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* API for Avarice functionality of Acedia.
|
||||||
|
* Copyright 2020 - 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AvariceAPI extends AcediaObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns all the `AvariceLink` created by `Avarice` feature.
|
||||||
|
*
|
||||||
|
* @return Array of links created by this feature.
|
||||||
|
* Guaranteed to not contain `none` values.
|
||||||
|
* Empty if `Avarice` feature is currently disabled.
|
||||||
|
*/
|
||||||
|
public final function array<AvariceLink> GetAllLinks()
|
||||||
|
{
|
||||||
|
local Avarice_Feature avariceFeature;
|
||||||
|
local array<AvariceLink> emptyResult;
|
||||||
|
avariceFeature =
|
||||||
|
Avarice_Feature(class'Avarice_Feature'.static.GetEnabledInstance());
|
||||||
|
if (avariceFeature != none) {
|
||||||
|
return avariceFeature.GetAllLinks();
|
||||||
|
}
|
||||||
|
return emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and returns `AvariceLink` by its name, specified in "AcediaAvarice"
|
||||||
|
* config, if it exists.
|
||||||
|
*
|
||||||
|
* @param linkName Name of the `AvariceLink` to find.
|
||||||
|
* @return `AvariceLink` corresponding to name `linkName`.
|
||||||
|
* If `linkName == none` or `AvariceLink` with such name does not exist -
|
||||||
|
* returns `none`.
|
||||||
|
*/
|
||||||
|
public final function AvariceLink GetLink(BaseText linkName)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text nextName;
|
||||||
|
local array<AvariceLink> allLinks;
|
||||||
|
if (linkName == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
allLinks = GetAllLinks();
|
||||||
|
for (i = 0; i < allLinks.length; i += 1)
|
||||||
|
{
|
||||||
|
if (allLinks[i] == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextName = allLinks[i].GetName();
|
||||||
|
if (linkName.Compare(nextName, SCASE_INSENSITIVE))
|
||||||
|
{
|
||||||
|
_.memory.Free(nextName);
|
||||||
|
return allLinks[i];
|
||||||
|
}
|
||||||
|
_.memory.Free(nextName);
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
443
kf_sources/AcediaCore/Classes/AvariceLink.uc
Normal file
443
kf_sources/AcediaCore/Classes/AvariceLink.uc
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
/**
|
||||||
|
* Provide interface for the connection to Avarice application.
|
||||||
|
* It's parameters are defined in Acedia's config.
|
||||||
|
* Class provides methods to obtain its configuration information
|
||||||
|
* (name, address, port), methods to check and change the status of connection,
|
||||||
|
* signals to handle arriving messages and ways to send messages back.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AvariceLink extends AcediaObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Objects of this class are supposed to be obtained via the
|
||||||
|
* `AvariceAPI.GetLink()` method. Available links are automatically initialized
|
||||||
|
* based on the configs and their parameters cannot be changed.
|
||||||
|
* It is also possible to spawn a link of your own by creating an object of
|
||||||
|
* this class (`AvariceLink`) and calling `Initialize()` method with
|
||||||
|
* appropriate parameters. To start the link then simply call `StartUp()`.
|
||||||
|
* But such links will not appear in the list of available links in
|
||||||
|
* `AvariceAPI`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Actual work of dealing with network input/output is done in
|
||||||
|
// the `AvariceTcpStream` `Actor` class that is stored inside this reference
|
||||||
|
var private NativeActorRef tcpStream;
|
||||||
|
|
||||||
|
// `tcpStream` communicates with this class by informing it about specific
|
||||||
|
// events. This enum describes all of their types.
|
||||||
|
enum AvariceNetworkMessage
|
||||||
|
{
|
||||||
|
// Connection with Avarice established - can happen several times in case
|
||||||
|
// connection is interrupted
|
||||||
|
ANM_Connected,
|
||||||
|
// We have lost connection with Avarice, but normally will attempt to
|
||||||
|
// reconnect back
|
||||||
|
ANM_Disconnected,
|
||||||
|
// JSON message received
|
||||||
|
ANM_Message,
|
||||||
|
// Connection died: either was manually closed, host address could not
|
||||||
|
// be resolved or invalid data was received from Avarice
|
||||||
|
ANM_Death
|
||||||
|
};
|
||||||
|
|
||||||
|
// Name of this link, specified in the config
|
||||||
|
var private Text linkName;
|
||||||
|
// Host of the Avarice instance we are connecting to
|
||||||
|
var private Text linkHost;
|
||||||
|
// Port used by the Avarice instance we are connecting to
|
||||||
|
var private int linkPort;
|
||||||
|
|
||||||
|
var private SimpleSignal onConnectedSignal;
|
||||||
|
var private SimpleSignal onDisconnectedSignal;
|
||||||
|
var private SimpleSignal onDeathSignal;
|
||||||
|
// We want to have a separate signal for each message "service", since most
|
||||||
|
// users of `AvariceLink` would only care about one particular service.
|
||||||
|
// To achieve that we use this array as a "service name" <-> "signal" map.
|
||||||
|
var private HashTable serviceSignalMap;
|
||||||
|
|
||||||
|
var private const int TSERVICE_PREFIX, TTYPE_PREFIX;
|
||||||
|
var private const int TPARAMS_PREFIX, TMESSAGE_SUFFIX;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition fatalCannotSpawn;
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
onConnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
|
||||||
|
onDisconnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
|
||||||
|
onDeathSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
|
||||||
|
serviceSignalMap = _.collections.EmptyHashTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
local Actor storedStream;
|
||||||
|
_.memory.Free(onConnectedSignal);
|
||||||
|
_.memory.Free(onDisconnectedSignal);
|
||||||
|
_.memory.Free(onDeathSignal);
|
||||||
|
_.memory.Free(serviceSignalMap);
|
||||||
|
_.memory.Free(linkName);
|
||||||
|
_.memory.Free(linkHost);
|
||||||
|
onConnectedSignal = none;
|
||||||
|
onDisconnectedSignal = none;
|
||||||
|
onDeathSignal = none;
|
||||||
|
serviceSignalMap = none;
|
||||||
|
linkName = none;
|
||||||
|
linkHost = none;
|
||||||
|
linkPort = 0;
|
||||||
|
if (tcpStream == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storedStream = tcpStream.Get();
|
||||||
|
if (storedStream != none) {
|
||||||
|
storedStream.Destroy();
|
||||||
|
}
|
||||||
|
tcpStream.FreeSelf();
|
||||||
|
tcpStream = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes this caller `AvariceLink` with config data.
|
||||||
|
*
|
||||||
|
* Can only successfully (for that `name` and `host` must not be `none`)
|
||||||
|
* be called once.
|
||||||
|
*
|
||||||
|
* @param name Alias (case-insensitive) of caller `AvariceLink`.
|
||||||
|
* Must not be `none`.
|
||||||
|
* @param host Host of the Avarice instance that caller `AvariceLink` is
|
||||||
|
* connecting to. Must not be `none`.
|
||||||
|
* @param name Port used by the Avarice instance that caller `AvariceLink`
|
||||||
|
* is connecting to.
|
||||||
|
*/
|
||||||
|
public final function Initialize(BaseText name, BaseText host, int port)
|
||||||
|
{
|
||||||
|
if (tcpStream != none) return;
|
||||||
|
if (name == none) return;
|
||||||
|
if (host == none) return;
|
||||||
|
|
||||||
|
linkName = name.Copy();
|
||||||
|
linkHost = host.Copy();
|
||||||
|
linkPort = port;
|
||||||
|
tcpStream = _server.unreal.ActorRef(none);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that will be emitted whenever caller `AvariceLink` connects to
|
||||||
|
* Avarice. This event can be emitted multiple times if case link temporarily
|
||||||
|
* loses it's TCP connection or if connection is killed off due to errors
|
||||||
|
* (or manually).
|
||||||
|
*
|
||||||
|
* [Signature]
|
||||||
|
* void <slot>()
|
||||||
|
*/
|
||||||
|
/* SIGNAL */
|
||||||
|
public final function SimpleSlot OnConnected(AcediaObject receiver)
|
||||||
|
{
|
||||||
|
return SimpleSlot(onConnectedSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that will be emitted whenever caller `AvariceLink` disconnects from
|
||||||
|
* Avarice. Disconnects can temporarily be cause by network issue and
|
||||||
|
* `AvariceLink` will attempt to restore it's connection. To detect when
|
||||||
|
* connection was permanently severed use `OnDeath` signal instead.
|
||||||
|
*
|
||||||
|
* [Signature]
|
||||||
|
* void <slot>()
|
||||||
|
*/
|
||||||
|
/* SIGNAL */
|
||||||
|
public final function SimpleSlot OnDisconnected(AcediaObject receiver)
|
||||||
|
{
|
||||||
|
return SimpleSlot(onDisconnectedSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that will be emitted whenever connection is closed and dropped:
|
||||||
|
* either due to bein unable to resolve host's address, receiving incorrect
|
||||||
|
* input from Avarice or someone manually closing it.
|
||||||
|
*
|
||||||
|
* [Signature]
|
||||||
|
* void <slot>()
|
||||||
|
*/
|
||||||
|
/* SIGNAL */
|
||||||
|
public final function SimpleSlot OnDeath(AcediaObject receiver)
|
||||||
|
{
|
||||||
|
return SimpleSlot(onDeathSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that will be emitted whenever caller `AvariceLink` disconnects from
|
||||||
|
* Avarice. Disconnects can temporarily be cause by network issue and
|
||||||
|
* `AvariceLink` will attempt to restore it's connection. To detect when
|
||||||
|
* connection was permanently severed use `OnDeath` signal instead.
|
||||||
|
*
|
||||||
|
* @param service Name of the service, whos messages one wants to receive.
|
||||||
|
* `none` will be treated as an empty `Text`.
|
||||||
|
*
|
||||||
|
* [Signature]
|
||||||
|
* void <slot>(AvariceLink link, AvariceMessage message)
|
||||||
|
* @param link Link that has received message.
|
||||||
|
* @param message Received message.
|
||||||
|
* Can be any JSON-compatible value (see `JSONAPI.IsCompatible()`
|
||||||
|
* for more information).
|
||||||
|
*/
|
||||||
|
/* SIGNAL */
|
||||||
|
public final function Avarice_OnMessage_Slot OnMessage(
|
||||||
|
AcediaObject receiver,
|
||||||
|
BaseText service)
|
||||||
|
{
|
||||||
|
return Avarice_OnMessage_Slot(GetServiceSignal(service).NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function Avarice_OnMessage_Signal GetServiceSignal(
|
||||||
|
BaseText service)
|
||||||
|
{
|
||||||
|
local Avarice_OnMessage_Signal result;
|
||||||
|
if (service != none) {
|
||||||
|
service = service.Copy();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
service = Text(_.memory.Allocate(class'Text'));
|
||||||
|
}
|
||||||
|
result = Avarice_OnMessage_Signal(serviceSignalMap.GetItem(service));
|
||||||
|
if (result == none)
|
||||||
|
{
|
||||||
|
result = Avarice_OnMessage_Signal(
|
||||||
|
_.memory.Allocate(class'Avarice_OnMessage_Signal'));
|
||||||
|
serviceSignalMap.SetItem(service, result);
|
||||||
|
_.memory.Free(result);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
service.FreeSelf();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts caller `AvariceLink`, making it attempt to connect to the Avarice
|
||||||
|
* with parameters that should be first specified by the `Initialize()` call.
|
||||||
|
*
|
||||||
|
* Does nothing if the caller `AvariceLink` is either not initialized or
|
||||||
|
* is already active (`IsActive() == true`).
|
||||||
|
*/
|
||||||
|
public final function StartUp()
|
||||||
|
{
|
||||||
|
local AvariceTcpStream newStream;
|
||||||
|
local LevelCore core;
|
||||||
|
|
||||||
|
if (tcpStream == none) return;
|
||||||
|
if (tcpStream.Get() != none) return;
|
||||||
|
core = __level().GetLevelCore();
|
||||||
|
if (core == none) return;
|
||||||
|
|
||||||
|
newStream = AvariceTcpStream(core.Allocate(class'AvariceTcpStream'));
|
||||||
|
if (newStream == none)
|
||||||
|
{
|
||||||
|
// `linkName` has to be defined if `tcpStream` is defined
|
||||||
|
_.logger.Auto(fatalCannotSpawn).Arg(linkName.Copy());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tcpStream.Set(newStream);
|
||||||
|
newStream.StartUp(self, class'Avarice_Feature'.static.GetReconnectTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down any connections related to the caller `AvariceLink`.
|
||||||
|
*
|
||||||
|
* Does nothing if the caller `AvariceLink` is either not initialized or
|
||||||
|
* is already inactive (`IsActive() == false`).
|
||||||
|
*/
|
||||||
|
public final function ShutDown()
|
||||||
|
{
|
||||||
|
local Actor storedStream;
|
||||||
|
if (tcpStream == none) return;
|
||||||
|
storedStream = tcpStream.Get();
|
||||||
|
if (storedStream == none) return;
|
||||||
|
|
||||||
|
storedStream.Destroy();
|
||||||
|
tcpStream.Set(none);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether caller `AvariceLink` is currently active: either connected or
|
||||||
|
* currently attempts to connect to Avarice.
|
||||||
|
*
|
||||||
|
* See also `IsConnected()`.
|
||||||
|
*
|
||||||
|
* @return `true` if caller `AvariceLink` is either connected or currently
|
||||||
|
* attempting to connect to Avarice. `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool IsActive()
|
||||||
|
{
|
||||||
|
if (tcpStream == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return tcpStream.Get() != none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether caller `AvariceLink` is currently connected or to Avarice.
|
||||||
|
*
|
||||||
|
* See also `IsActive()`.
|
||||||
|
*
|
||||||
|
* @return `true` iff caller `AvariceLink` is currently connected to Avarice.
|
||||||
|
*/
|
||||||
|
public final function bool IsConnected()
|
||||||
|
{
|
||||||
|
local AvariceTcpStream storedStream;
|
||||||
|
if (tcpStream == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
storedStream = AvariceTcpStream(tcpStream.Get());
|
||||||
|
return storedStream.linkState == STATE_Connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns name caller `AvariceLink` was initialized with.
|
||||||
|
* Defined through the config files.
|
||||||
|
*
|
||||||
|
* @return Name of the caller `AvariceLink`.
|
||||||
|
* `none` iff caller link was not yet initialized.
|
||||||
|
*/
|
||||||
|
public final function Text GetName()
|
||||||
|
{
|
||||||
|
if (linkName != none) {
|
||||||
|
return linkName.Copy();
|
||||||
|
}
|
||||||
|
// `linkName` cannot be `none` after `Initialize()` call
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns host name (without port number) caller `AvariceLink` was
|
||||||
|
* initialized with. Defined through the config files.
|
||||||
|
*
|
||||||
|
* See `GetPort()` method for port number.
|
||||||
|
*
|
||||||
|
* @return Host name of the caller `AvariceLink`.
|
||||||
|
* `none` iff caller link was not yet initialized.
|
||||||
|
*/
|
||||||
|
public final function Text GetHost()
|
||||||
|
{
|
||||||
|
if (linkHost != none) {
|
||||||
|
return linkHost.Copy();
|
||||||
|
}
|
||||||
|
// `linkName` cannot be `none` after `Initialize()` call
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns port number caller `AvariceLink` was initialized with.
|
||||||
|
* Defined through the config files.
|
||||||
|
*
|
||||||
|
* @return Host name of the caller `AvariceLink`.
|
||||||
|
* If caller link was not yet initialized, method makes no guarantees
|
||||||
|
* about returned number.
|
||||||
|
*/
|
||||||
|
public final function int GetPort()
|
||||||
|
{
|
||||||
|
return linkPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the Avarice that caller `AvariceLink` is connected to.
|
||||||
|
*
|
||||||
|
* Message can only be set if caller `AvariceLink` was initialized and is
|
||||||
|
* currently connected (see `IsConnected()`) to Avarice.
|
||||||
|
*
|
||||||
|
* @param service Name of the service this message is addressed.
|
||||||
|
* As an example, to address a database one would specify its name,
|
||||||
|
* like "db". Cannot be `none`.
|
||||||
|
* @param type Name of this message. As an example, to address
|
||||||
|
* a database, one would specify here WHAT that database must do,
|
||||||
|
* like "get" to fetch some data. Cannot be `none`.
|
||||||
|
* @param parameters Parameters of the command. Can be any value that is
|
||||||
|
* JSON-compatible (see `JSONAPI.IsCompatible()` for details).
|
||||||
|
* @return `true` if message was successfully sent and `false` otherwise.
|
||||||
|
* Note that this method returning `true` does not necessarily mean that
|
||||||
|
* message has arrived (which is impossible to know at this moment),
|
||||||
|
* instead simply saying that network call to send data was successful.
|
||||||
|
* Avarice does not provide any mechanism to verify message arrival, so if
|
||||||
|
* you need that confirmation - it is necessary that service you are
|
||||||
|
* addressing make a reply.
|
||||||
|
*/
|
||||||
|
public final function bool SendMessage(
|
||||||
|
BaseText service,
|
||||||
|
BaseText type,
|
||||||
|
AcediaObject parameters)
|
||||||
|
{
|
||||||
|
local Mutabletext parametesAsJSON;
|
||||||
|
local MutableText message;
|
||||||
|
local AvariceTcpStream storedStream;
|
||||||
|
if (tcpStream == none) return false;
|
||||||
|
if (service == none) return false;
|
||||||
|
if (type == none) return false;
|
||||||
|
storedStream = AvariceTcpStream(tcpStream.Get());
|
||||||
|
if (storedStream == none) return false;
|
||||||
|
if (storedStream.linkState != STATE_Connected) return false;
|
||||||
|
parametesAsJSON = _.json.Print(parameters);
|
||||||
|
if (parametesAsJSON == none) return false;
|
||||||
|
|
||||||
|
message = _.text.Empty();
|
||||||
|
message.Append(T(TSERVICE_PREFIX))
|
||||||
|
.Append(_.json.Print(service))
|
||||||
|
.Append(T(TTYPE_PREFIX))
|
||||||
|
.Append(_.json.Print(type))
|
||||||
|
.Append(T(TPARAMS_PREFIX))
|
||||||
|
.Append(parametesAsJSON)
|
||||||
|
.Append(T(TMESSAGE_SUFFIX));
|
||||||
|
storedStream.SendMessage(message);
|
||||||
|
message.FreeSelf();
|
||||||
|
parametesAsJSON.FreeSelf();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a public method, but it is not a part of
|
||||||
|
// `AvariceLink` interface.
|
||||||
|
// It is used as a communication channel with `AvariceTcpStream` and
|
||||||
|
// should not be called outside of that class.
|
||||||
|
public final function ReceiveNetworkMessage(
|
||||||
|
AvariceNetworkMessage message,
|
||||||
|
optional AvariceMessage avariceMessage)
|
||||||
|
{
|
||||||
|
if (message == ANM_Connected) {
|
||||||
|
onConnectedSignal.Emit();
|
||||||
|
}
|
||||||
|
else if (message == ANM_Disconnected) {
|
||||||
|
onDisconnectedSignal.Emit();
|
||||||
|
}
|
||||||
|
else if (message == ANM_Message && avariceMessage != none) {
|
||||||
|
GetServiceSignal(avariceMessage.service).Emit(self, avariceMessage);
|
||||||
|
}
|
||||||
|
else if (message == ANM_Death) {
|
||||||
|
onDeathSignal.Emit();
|
||||||
|
tcpStream.Set(none);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
TSERVICE_PREFIX = 0
|
||||||
|
stringConstants(0) = "&{\"s\":"
|
||||||
|
TTYPE_PREFIX = 1
|
||||||
|
stringConstants(1) = ",\"t\":"
|
||||||
|
TPARAMS_PREFIX = 2
|
||||||
|
stringConstants(2) = ",\"p\":"
|
||||||
|
TMESSAGE_SUFFIX = 3
|
||||||
|
stringConstants(3) = "&}"
|
||||||
|
fatalCannotSpawn = (l=LOG_Error,m="Cannot spawn new actor of class `AvariceTcpStream`, avarice link \"%1\" will not be created")
|
||||||
|
}
|
||||||
97
kf_sources/AcediaCore/Classes/AvariceMessage.uc
Normal file
97
kf_sources/AcediaCore/Classes/AvariceMessage.uc
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Object that represents a message received from Avarice.
|
||||||
|
* For performance's sake it does not provide a getter/setter interface and
|
||||||
|
* exposes public fields instead. However, for Acedia to correctly function
|
||||||
|
* you are not supposed modify those fields in any way, only using them to
|
||||||
|
* read necessary data.
|
||||||
|
* All `AvariceMessage`'s fields will be automatically deallocated, so if
|
||||||
|
* you need their data - you have to make a copy, instead of simply storing
|
||||||
|
* a reference to them.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AvariceMessage extends AcediaObject;
|
||||||
|
|
||||||
|
// Every message from Avarice has following structure:
|
||||||
|
// { "s": "<service_name>", "t": "<command_type>", "p": <any_json_value> }
|
||||||
|
// Value of the "s" field
|
||||||
|
var public Text service;
|
||||||
|
// Value of the "t" field
|
||||||
|
var public Text type;
|
||||||
|
// Value of the "p" field
|
||||||
|
var public AcediaObject parameters;
|
||||||
|
|
||||||
|
var private HashTable messageTemplate;
|
||||||
|
|
||||||
|
var private const int TS, TT, TP;
|
||||||
|
|
||||||
|
public static function StaticConstructor()
|
||||||
|
{
|
||||||
|
if (StaticConstructorGuard()) return;
|
||||||
|
super.StaticConstructor();
|
||||||
|
|
||||||
|
default.messageTemplate = __().collections.EmptyHashTable();
|
||||||
|
ResetTemplate(default.messageTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
__().memory.Free(type);
|
||||||
|
__().memory.Free(service);
|
||||||
|
__().memory.Free(parameters);
|
||||||
|
type = none;
|
||||||
|
service = none;
|
||||||
|
parameters = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final function ResetTemplate(HashTable template)
|
||||||
|
{
|
||||||
|
if (template == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
template.SetItem(T(default.TS), none);
|
||||||
|
template.SetItem(T(default.TT), none);
|
||||||
|
template.SetItem(T(default.TP), none);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function MutableText ToText()
|
||||||
|
{
|
||||||
|
local MutableText result;
|
||||||
|
local HashTable template;
|
||||||
|
if (type == none) return none;
|
||||||
|
if (service == none) return none;
|
||||||
|
|
||||||
|
template = default.messageTemplate;
|
||||||
|
ResetTemplate(template);
|
||||||
|
template.SetItem(T(TT), type);
|
||||||
|
template.SetItem(T(TS), service);
|
||||||
|
if (parameters != none) {
|
||||||
|
template.SetItem(T(TP), parameters);
|
||||||
|
}
|
||||||
|
result = _.json.Print(template);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
TS = 0
|
||||||
|
stringConstants(0) = "s"
|
||||||
|
TT = 1
|
||||||
|
stringConstants(1) = "t"
|
||||||
|
TP = 2
|
||||||
|
stringConstants(2) = "p"
|
||||||
|
}
|
||||||
149
kf_sources/AcediaCore/Classes/AvariceStreamReader.uc
Normal file
149
kf_sources/AcediaCore/Classes/AvariceStreamReader.uc
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* Helper class meant for reading byte stream sent to us by the Avarice
|
||||||
|
* application.
|
||||||
|
* Avarice sends us utf8-encoded JSONs one-by-one, prepending each of them
|
||||||
|
* with 4 bytes (big endian) that encode the length of the following message.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AvariceStreamReader extends AcediaObject;
|
||||||
|
|
||||||
|
// Are we currently reading length of the message (`true`) or
|
||||||
|
// the message itself (`false`)?
|
||||||
|
var private bool readingLength;
|
||||||
|
// How many byte we have read so far.
|
||||||
|
// Resets to zero when we finish reading either length of the message or
|
||||||
|
// the message itself.
|
||||||
|
var private int readBytes;
|
||||||
|
// Expected length of the next message
|
||||||
|
var private int nextMessageLength;
|
||||||
|
// Message read so far
|
||||||
|
var private ByteArrayRef nextMessage;
|
||||||
|
// All the messages we have fully read, but did not yet return
|
||||||
|
var private array<MutableText> outputQueue;
|
||||||
|
// For converting read messages into `MutableText`
|
||||||
|
var private Utf8Decoder decoder;
|
||||||
|
// Set to `true` if Avarice input was somehow unacceptable.
|
||||||
|
// Cannot be recovered from.
|
||||||
|
var private bool hasFailed;
|
||||||
|
|
||||||
|
// Maximum allowed size of JSON message sent from avarice;
|
||||||
|
// Anything more than that is treated as a mistake.
|
||||||
|
// TODO: make this configurable
|
||||||
|
var private const int MAX_MESSAGE_LENGTH;
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
readingLength = true;
|
||||||
|
nextMessage = ByteArrayRef(_.memory.Allocate(class'ByteArrayRef'));
|
||||||
|
decoder = Utf8Decoder(_.memory.Allocate(class'Utf8Decoder'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
_.memory.FreeMany(outputQueue);
|
||||||
|
_.memory.Free(nextMessage);
|
||||||
|
_.memory.Free(decoder);
|
||||||
|
outputQueue.length = 0;
|
||||||
|
nextMessage = none;
|
||||||
|
decoder = none;
|
||||||
|
hasFailed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds next `byte` from the input Avarice stream to the reader.
|
||||||
|
*
|
||||||
|
* If input stream signals that message we have to read is too long
|
||||||
|
* (longer then `MAX_MESSAGE_LENGTH`) - enters a failed state and will
|
||||||
|
* no longer accept any input. Failed status can be checked with
|
||||||
|
* `Failed()` method.
|
||||||
|
* Otherwise cannot fail.
|
||||||
|
*
|
||||||
|
* @param nextByte Next byte from the Avarice input stream.
|
||||||
|
* @return `false` if caller `AvariceStreamReader` is in a failed state
|
||||||
|
* (including if it entered one after pushing this byte)
|
||||||
|
* and `true` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool PushByte(byte nextByte)
|
||||||
|
{
|
||||||
|
if (hasFailed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (readingLength)
|
||||||
|
{
|
||||||
|
// Make space for the next 8 bits by shifting previously recorded ones
|
||||||
|
nextMessageLength = nextMessageLength << 8;
|
||||||
|
nextMessageLength += nextByte;
|
||||||
|
readBytes += 1;
|
||||||
|
if (readBytes >= 4)
|
||||||
|
{
|
||||||
|
readingLength = false;
|
||||||
|
readBytes = 0;
|
||||||
|
}
|
||||||
|
// Message either too long or so long it overfilled `MaxInt`
|
||||||
|
if ( nextMessageLength > MAX_MESSAGE_LENGTH
|
||||||
|
|| nextMessageLength < 0)
|
||||||
|
{
|
||||||
|
hasFailed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
nextMessage.AddItem(nextByte);
|
||||||
|
readBytes += 1;
|
||||||
|
if (readBytes >= nextMessageLength)
|
||||||
|
{
|
||||||
|
outputQueue[outputQueue.length] = decoder.Decode(nextMessage);
|
||||||
|
nextMessage.Empty();
|
||||||
|
readingLength = true;
|
||||||
|
readBytes = 0;
|
||||||
|
nextMessageLength = 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all complete messages read so far.
|
||||||
|
*
|
||||||
|
* Even if caller `AvariceStreamReader` entered a failed state - this method
|
||||||
|
* will return all the messages read before it has failed.
|
||||||
|
*
|
||||||
|
* @return aAl complete messages read from Avarice stream so far
|
||||||
|
*/
|
||||||
|
public final function array<MutableText> PopMessages()
|
||||||
|
{
|
||||||
|
local array<MutableText> result;
|
||||||
|
result = outputQueue;
|
||||||
|
outputQueue.length = 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is caller `AvariceStreamReader` in a failed state?
|
||||||
|
* See `PushByte()` method for details.
|
||||||
|
*
|
||||||
|
* @return `true` iff caller `AvariceStreamReader` has failed.
|
||||||
|
*/
|
||||||
|
public final function bool Failed()
|
||||||
|
{
|
||||||
|
return hasFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
MAX_MESSAGE_LENGTH = 26214400 // 25 * 1024 * 1024 = 25MB
|
||||||
|
}
|
||||||
387
kf_sources/AcediaCore/Classes/AvariceTcpStream.uc
Normal file
387
kf_sources/AcediaCore/Classes/AvariceTcpStream.uc
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
/**
|
||||||
|
* Acedia's `TcpLink` class for connecting to Avarice.
|
||||||
|
* This class should be considered an internal implementation detail and not
|
||||||
|
* accessed directly.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class AvariceTcpStream extends TcpLink
|
||||||
|
dependson(LoggerAPI);
|
||||||
|
|
||||||
|
var private Global _;
|
||||||
|
|
||||||
|
// Reference to the link that has spawned us, to pass on messages.
|
||||||
|
var private AvariceLink ownerLink;
|
||||||
|
|
||||||
|
// Information needed for connection
|
||||||
|
var private string linkHost;
|
||||||
|
var private int linkPort;
|
||||||
|
var private IpAddr remoteAddress;
|
||||||
|
|
||||||
|
// If `OpenNoSteam()` (inside `OpenAddress()`) call is made before Avarice
|
||||||
|
// application started - connection will not succeed. Because of that
|
||||||
|
// `AvariceTcpStream` will keep restarting its attempt to connect if it waited
|
||||||
|
// for connection to go through for long enough.
|
||||||
|
// This variable is set when `AvariceTcpStream` is initialized and it
|
||||||
|
// defines how long we have to wait before reconnection attempt.
|
||||||
|
var private float reconnectInterval;
|
||||||
|
// This variable track how much time has passed since `OpenNoSteam()` call.
|
||||||
|
var private float timeSpentConnecting;
|
||||||
|
// For reconnections we need to remember whether we have already bound our
|
||||||
|
// port, otherwise it will lead to errors in logs.
|
||||||
|
var private bool portBound;
|
||||||
|
|
||||||
|
// Array used to read to and write from TCP connection.
|
||||||
|
// All the native methods use it, so avoid creating it locally in our methods.
|
||||||
|
var private byte buffer[255];
|
||||||
|
|
||||||
|
// Used to convert our messages in a way appropriate for the network
|
||||||
|
var private Utf8Encoder encoder;
|
||||||
|
// Used to read and correctly interpret byte stream from Avarice
|
||||||
|
var private AvariceStreamReader avariceReader;
|
||||||
|
|
||||||
|
// Arbitrary value indicating that next byte sequence from us reports amount of
|
||||||
|
// bytes received so far
|
||||||
|
var private byte HEAD_BYTES_RECEIVED;
|
||||||
|
// Arbitrary value indicating that next byte sequence from us contains
|
||||||
|
// JSON message (prepended by it's length)
|
||||||
|
var private byte HEAD_MESSAGE;
|
||||||
|
|
||||||
|
// Byte mask to extract lowest byte from the `int`s:
|
||||||
|
// 00000000 00000000 00000000 11111111
|
||||||
|
var private int byteMask;
|
||||||
|
|
||||||
|
// `Text` values to be used as keys for getting information from
|
||||||
|
// received messages
|
||||||
|
var private Text keyS, keyT, keyP;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition infoConnected;
|
||||||
|
var private LoggerAPI.Definition infoDisconnected;
|
||||||
|
var private LoggerAPI.Definition fatalNoLink;
|
||||||
|
var private LoggerAPI.Definition fatalBadPort;
|
||||||
|
var private LoggerAPI.Definition fatalCannotBindPort;
|
||||||
|
var private LoggerAPI.Definition fatalCannotResolveHost;
|
||||||
|
var private LoggerAPI.Definition fatalCannotConnect;
|
||||||
|
var private LoggerAPI.Definition fatalInvaliddUTF8;
|
||||||
|
var private LoggerAPI.Definition fatalInvalidMessage;
|
||||||
|
|
||||||
|
// Starts this link, to stop it - simply destroy it
|
||||||
|
public final function StartUp(AvariceLink link, float reconnectTime)
|
||||||
|
{
|
||||||
|
if (link == none)
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ownerLink = link;
|
||||||
|
// Apparently `TcpLink` ignores default values for these variables,
|
||||||
|
// so we set them here
|
||||||
|
linkMode = MODE_Binary;
|
||||||
|
receiveMode = RMODE_Manual;
|
||||||
|
// This actor does not have `AcediaActor` for a parent, so manually
|
||||||
|
// define `_` for convenience
|
||||||
|
_ = class'Global'.static.GetInstance();
|
||||||
|
// Necessary constants
|
||||||
|
keyS = _.text.FromString("s");
|
||||||
|
keyT = _.text.FromString("t");
|
||||||
|
keyP = _.text.FromString("p");
|
||||||
|
// For decoding input and encoding output
|
||||||
|
encoder = Utf8Encoder(_.memory.Allocate(class'Utf8Encoder'));
|
||||||
|
avariceReader =
|
||||||
|
AvariceStreamReader(_.memory.Allocate(class'AvariceStreamReader'));
|
||||||
|
linkHost = _.text.IntoString(link.GetHost());
|
||||||
|
linkPort = link.GetPort();
|
||||||
|
reconnectInterval = reconnectTime;
|
||||||
|
TryConnecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function TryConnecting()
|
||||||
|
{
|
||||||
|
if (linkPort <= 0)
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatalBadPort)
|
||||||
|
.ArgInt(linkPort)
|
||||||
|
.Arg(ownerLink.GetName());
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// `linkPort` is a port we are connecting to, which is different
|
||||||
|
// from the port we have to bind to use `OpenNoSteam()` method
|
||||||
|
if (!portBound && BindPort(, true) <= 0)
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatalCannotBindPort)
|
||||||
|
.Arg(ownerLink.GetName());
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
portBound = true;
|
||||||
|
// Try to read `linkHost` as an IP address first, in case of failure -
|
||||||
|
// try to resolve it from the host name
|
||||||
|
StringToIpAddr(linkHost, remoteAddress);
|
||||||
|
remoteAddress.port = linkPort;
|
||||||
|
if (remoteAddress.addr == 0) {
|
||||||
|
Resolve(linkHost);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
OpenAddress();
|
||||||
|
}
|
||||||
|
timeSpentConnecting = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
event Resolved(IpAddr resolvedAddress)
|
||||||
|
{
|
||||||
|
remoteAddress.addr = resolvedAddress.addr;
|
||||||
|
OpenAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
event ResolveFailed()
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatalCannotResolveHost).Arg(_.text.FromString(linkHost));
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function OpenAddress()
|
||||||
|
{
|
||||||
|
if (!OpenNoSteam(remoteAddress))
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatalCannotConnect).Arg(ownerLink.GetName());
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event Opened()
|
||||||
|
{
|
||||||
|
_.logger.Auto(infoConnected).Arg(ownerLink.GetName());
|
||||||
|
if (ownerLink != none) {
|
||||||
|
ownerLink.ReceiveNetworkMessage(ANM_Connected);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event Closed()
|
||||||
|
{
|
||||||
|
_.logger.Auto(infoDisconnected).Arg(ownerLink.GetName());
|
||||||
|
if (ownerLink != none) {
|
||||||
|
ownerLink.ReceiveNetworkMessage(ANM_Connected);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName());
|
||||||
|
}
|
||||||
|
portBound = false;
|
||||||
|
timeSpentConnecting = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
event Destroyed()
|
||||||
|
{
|
||||||
|
if (_ != none)
|
||||||
|
{
|
||||||
|
_.memory.Free(avariceReader);
|
||||||
|
_.memory.Free(encoder);
|
||||||
|
_.memory.Free(keyS);
|
||||||
|
_.memory.Free(keyT);
|
||||||
|
_.memory.Free(keyP);
|
||||||
|
if (ownerLink != none) {
|
||||||
|
ownerLink.ReceiveNetworkMessage(ANM_Death);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event Tick(float delta)
|
||||||
|
{
|
||||||
|
if (linkState == STATE_Connected) {
|
||||||
|
HandleIncomingData();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
timeSpentConnecting += delta;
|
||||||
|
if (timeSpentConnecting >= reconnectInterval)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
TryConnecting();
|
||||||
|
timeSpentConnecting = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function HandleIncomingData()
|
||||||
|
{
|
||||||
|
local int i, totalReceivedBytes;
|
||||||
|
local AvariceMessage nextMessage;
|
||||||
|
local array<MutableText> receivedMessages;
|
||||||
|
local int bytesRead;
|
||||||
|
bytesRead = ReadBinary(255, buffer);
|
||||||
|
while (bytesRead > 0)
|
||||||
|
{
|
||||||
|
totalReceivedBytes += bytesRead;
|
||||||
|
for (i = 0; i < bytesRead; i += 1) {
|
||||||
|
avariceReader.PushByte(buffer[i]);
|
||||||
|
}
|
||||||
|
bytesRead = ReadBinary(255, buffer);
|
||||||
|
}
|
||||||
|
if (avariceReader.Failed())
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatalInvaliddUTF8).Arg(ownerLink.GetName());
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Tell Avarice how many bytes we have received, so it can send
|
||||||
|
// more information
|
||||||
|
if (totalReceivedBytes > 0) {
|
||||||
|
SendReceived(totalReceivedBytes);
|
||||||
|
}
|
||||||
|
receivedMessages = avariceReader.PopMessages();
|
||||||
|
for (i = 0; i < receivedMessages.length; i += 1)
|
||||||
|
{
|
||||||
|
nextMessage = MessageFromText(receivedMessages[i]);
|
||||||
|
// This means received message is invalid,
|
||||||
|
// which means whatever we are connected to is feeding us invalid data,
|
||||||
|
// which means connection should be cut immediately
|
||||||
|
if (nextMessage == none)
|
||||||
|
{
|
||||||
|
_.logger.Auto(fatalInvalidMessage).Arg(ownerLink.GetName());
|
||||||
|
_.memory.Free(nextMessage);
|
||||||
|
_.memory.FreeMany(receivedMessages);
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ownerLink.ReceiveNetworkMessage(ANM_Message, nextMessage);
|
||||||
|
_.memory.Free(nextMessage);
|
||||||
|
}
|
||||||
|
_.memory.FreeMany(receivedMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function SendMessage(BaseText textMessage)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local int nextByte;
|
||||||
|
local ByteArrayRef message;
|
||||||
|
local int messageLength;
|
||||||
|
if (textMessage == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message = encoder.Encode(textMessage);
|
||||||
|
messageLength = message.GetLength();
|
||||||
|
// Signal that we are sending next message
|
||||||
|
buffer[0] = HEAD_MESSAGE;
|
||||||
|
// Next four bytes (with indices 1, 2, 3 and 4) must contain length of
|
||||||
|
// the message's contents as a 4-byte unsigned integer in big endian.
|
||||||
|
// UnrealScript does not actually have an unsigned integer type, but
|
||||||
|
// even `int`'s positive value range is enough, since avarice server
|
||||||
|
// will not accept message length that is even close to `MaxInt`.
|
||||||
|
buffer[4] = messageLength & byteMask;
|
||||||
|
messageLength -= buffer[4];
|
||||||
|
messageLength = messageLength >> 8;
|
||||||
|
buffer[3] = messageLength & byteMask;
|
||||||
|
messageLength -= buffer[3];
|
||||||
|
messageLength = messageLength >> 8;
|
||||||
|
buffer[2] = messageLength & byteMask;
|
||||||
|
messageLength -= buffer[2];
|
||||||
|
messageLength = messageLength >> 8;
|
||||||
|
buffer[1] = messageLength;
|
||||||
|
// Record the rest of the message in chunks of `255`, since `SendBinary()`
|
||||||
|
// can only send this much at once
|
||||||
|
nextByte = 5; // We have already added 5 bytes in the code above
|
||||||
|
messageLength = message.GetLength();
|
||||||
|
for (i = 0; i < messageLength; i += 1)
|
||||||
|
{
|
||||||
|
buffer[nextByte] = message.GetItem(i);
|
||||||
|
nextByte += 1;
|
||||||
|
if (nextByte >= 255)
|
||||||
|
{
|
||||||
|
nextByte = 0;
|
||||||
|
SendBinary(255, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cycle above only sent full chunks of `255`, so send the remainder now
|
||||||
|
if (nextByte > 0) {
|
||||||
|
SendBinary(nextByte, buffer);
|
||||||
|
}
|
||||||
|
message.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function SendReceived(int received)
|
||||||
|
{
|
||||||
|
// Signal that we are sending amount of bytes received this tick
|
||||||
|
buffer[0] = HEAD_BYTES_RECEIVED;
|
||||||
|
// Next four bytes (with indices 1 and 2) must contain amount of bytes
|
||||||
|
// received as a 2-byte unsigned integer in big endian
|
||||||
|
buffer[2] = received & byteMask;
|
||||||
|
received -= buffer[2];
|
||||||
|
received = received >> 8;
|
||||||
|
buffer[1] = received;
|
||||||
|
SendBinary(3, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function AvariceMessage MessageFromText(BaseText message)
|
||||||
|
{
|
||||||
|
local Parser parser;
|
||||||
|
local AvariceMessage result;
|
||||||
|
local HashTable parsedMessage;
|
||||||
|
local AcediaObject item;
|
||||||
|
if (message == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
parser = _.text.Parse(message);
|
||||||
|
parsedMessage = _.json.ParseHashTableWith(parser);
|
||||||
|
parser.FreeSelf();
|
||||||
|
if (parsedMessage == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
result = AvariceMessage(_.memory.Allocate(class'AvariceMessage'));
|
||||||
|
item = parsedMessage.TakeItem(keyS);
|
||||||
|
if (item == none || item.class != class'Text')
|
||||||
|
{
|
||||||
|
_.memory.Free(item);
|
||||||
|
_.memory.Free(parsedMessage);
|
||||||
|
_.memory.Free(result);
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
result.service = Text(item);
|
||||||
|
item = parsedMessage.TakeItem(keyT);
|
||||||
|
if (item == none || item.class != class'Text')
|
||||||
|
{
|
||||||
|
_.memory.Free(item);
|
||||||
|
_.memory.Free(parsedMessage);
|
||||||
|
_.memory.Free(result);
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
result.type = Text(item);
|
||||||
|
result.parameters = parsedMessage.TakeItem(keyP);
|
||||||
|
_.memory.Free(parsedMessage);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
HEAD_BYTES_RECEIVED = 85
|
||||||
|
HEAD_MESSAGE = 42
|
||||||
|
byteMask = 255 // Only lowest 8 bits are `1`
|
||||||
|
infoConnected = (l=LOG_Info,m="Avarice link \"%1\" connected")
|
||||||
|
infoDisconnected = (l=LOG_Info,m="Avarice link \"%1\" disconnected")
|
||||||
|
fatalNoLink = (l=LOG_Fatal,m="Unexpected internal `none` value for Avarice link \"%1\"")
|
||||||
|
fatalBadPort = (l=LOG_Fatal,m="Bad port \"%1\" specified for Avarice link \"%2\"")
|
||||||
|
fatalCannotBindPort = (l=LOG_Fatal,m="Cannot bind port for Avarice link \"%1\"")
|
||||||
|
fatalCannotResolveHost = (l=LOG_Fatal,m="Cannot resolve host \"%1\" for Avarice link \"%2\"")
|
||||||
|
fatalCannotConnect = (l=LOG_Fatal,m="Connection for Avarice link \"%1\" was rejected")
|
||||||
|
fatalInvaliddUTF8 = (l=LOG_Fatal,m="Avarice link \"%1\" has received invalid UTF8, aborting connection")
|
||||||
|
fatalInvalidMessage = (l=LOG_Fatal,m="Avarice link \"%1\" has received invalid message, aborting connection")
|
||||||
|
}
|
||||||
168
kf_sources/AcediaCore/Classes/Avarice_Feature.uc
Normal file
168
kf_sources/AcediaCore/Classes/Avarice_Feature.uc
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* This feature makes it possible to use TCP connection to exchange
|
||||||
|
* messages (represented by JSON objects) with external applications.
|
||||||
|
* There are some peculiarities to UnrealEngine's `TCPLink`, so to simplify
|
||||||
|
* communication process for external applications, they are expected to
|
||||||
|
* connect to the server through the "Avarice" utility that can accept a stream
|
||||||
|
* of utf8-encoded JSON messageand feed them to our `TCPLink` (we use child
|
||||||
|
* class `AvariceTcpStream`) in a way it can receive them.
|
||||||
|
* Every message sent to us must have the following structure:
|
||||||
|
* { "s": "<service_name>", "t": "<command_type>", "p": <any_json_value> }
|
||||||
|
* where
|
||||||
|
* * <service_name> describes a particular source of messages
|
||||||
|
* (it can be a name of the database or an alias for
|
||||||
|
* a connected application);
|
||||||
|
* * <command_type> simply states the name of a command, for a database it
|
||||||
|
* can be "get", "set", "delete", etc..
|
||||||
|
* * <any_json_value> can be an arbitrary json value and can be used to
|
||||||
|
* pass any additional information along with the message.
|
||||||
|
* Acedia provides a special treatment for any messages that have their
|
||||||
|
* service set to "echo" - it always returns them back as-is, except for the
|
||||||
|
* message type that gets set to "end".
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Avarice_Feature extends Feature
|
||||||
|
dependson(Avarice);
|
||||||
|
|
||||||
|
var private /*config*/ array<Avarice.AvariceLinkRecord> link;
|
||||||
|
var private /*config*/ float reconnectTime;
|
||||||
|
|
||||||
|
// The feature itself is dead simple - it simply creates list of
|
||||||
|
// `AvariceLink` objects, according to its settings, and stores them
|
||||||
|
var private array<AvariceLink> createdLinks;
|
||||||
|
|
||||||
|
var private const int TECHO, TEND, TCOLON;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition errorBadAddress;
|
||||||
|
|
||||||
|
protected function OnEnabled()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text name;
|
||||||
|
local MutableText host;
|
||||||
|
local int port;
|
||||||
|
local AvariceLink nextLink;
|
||||||
|
for (i = 0; i < link.length; i += 1)
|
||||||
|
{
|
||||||
|
name = _.text.FromString(link[i].name);
|
||||||
|
if (ParseAddress(link[i].address, host, port))
|
||||||
|
{
|
||||||
|
nextLink = AvariceLink(_.memory.Allocate(class'AvariceLink'));
|
||||||
|
nextLink.Initialize(name, host, port);
|
||||||
|
nextLink.StartUp();
|
||||||
|
nextLink.OnMessage(self, T(TECHO)).connect = EchoHandler;
|
||||||
|
createdLinks[createdLinks.length] = nextLink;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_.logger.Auto(errorBadAddress)
|
||||||
|
.Arg(_.text.FromString(link[i].address))
|
||||||
|
.Arg(_.text.FromString(link[i].name));
|
||||||
|
}
|
||||||
|
_.memory.Free(name);
|
||||||
|
_.memory.Free(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function OnDisabled()
|
||||||
|
{
|
||||||
|
_.memory.FreeMany(createdLinks);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function SwapConfig(FeatureConfig config)
|
||||||
|
{
|
||||||
|
local Avarice newConfig;
|
||||||
|
newConfig = Avarice(config);
|
||||||
|
if (newConfig == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link = newConfig.link;
|
||||||
|
reconnectTime = newConfig.reconnectTime;
|
||||||
|
// For static `GetReconnectTime()` method
|
||||||
|
default.reconnectTime = reconnectTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply back any messages from "echo" service
|
||||||
|
private function EchoHandler(AvariceLink link, AvariceMessage message)
|
||||||
|
{
|
||||||
|
link.SendMessage(T(TECHO), T(TEND), message.parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool ParseAddress(
|
||||||
|
string address,
|
||||||
|
out MutableText host,
|
||||||
|
out int port)
|
||||||
|
{
|
||||||
|
local bool success;
|
||||||
|
local Parser parser;
|
||||||
|
parser = _.text.ParseString(address);
|
||||||
|
parser.Skip()
|
||||||
|
.MUntil(host, T(TCOLON).GetCharacter(0))
|
||||||
|
.Match(T(TCOLON))
|
||||||
|
.MUnsignedInteger(port)
|
||||||
|
.Skip();
|
||||||
|
success = parser.Ok() && parser.GetRemainingLength() == 0;
|
||||||
|
parser.FreeSelf();
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns all the `AvariceLink` created by this feature.
|
||||||
|
*
|
||||||
|
* @return Array of links created by this feature.
|
||||||
|
* Guaranteed to not contain `none` values.
|
||||||
|
*/
|
||||||
|
public final function array<AvariceLink> GetAllLinks()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
while (i < createdLinks.length)
|
||||||
|
{
|
||||||
|
if (createdLinks[i] == none) {
|
||||||
|
createdLinks.Remove(i, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return createdLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns its current `reconnectTime` setting that describes amount of time
|
||||||
|
* between connection attempts.
|
||||||
|
*
|
||||||
|
* @return Value of `reconnectTime` config variable.
|
||||||
|
*/
|
||||||
|
public final static function float GetReconnectTime()
|
||||||
|
{
|
||||||
|
return default.reconnectTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configClass = class'Avarice'
|
||||||
|
// `Text` constants
|
||||||
|
TECHO = 0
|
||||||
|
stringConstants(0) = "echo"
|
||||||
|
TEND = 1
|
||||||
|
stringConstants(1) = "end"
|
||||||
|
TCOLON = 2
|
||||||
|
stringConstants(2) = ":"
|
||||||
|
// Log messages
|
||||||
|
errorBadAddress = (l=LOG_Error,m="Cannot parse address \"%1\" for \"%2\"")
|
||||||
|
}
|
||||||
38
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Signal.uc
Normal file
38
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Signal.uc
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Signal class implementation for `Avarice`'s `OnMessage` signal.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Avarice_OnMessage_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function Emit(AvariceLink link, AvariceMessage message)
|
||||||
|
{
|
||||||
|
local Slot nextSlot;
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none)
|
||||||
|
{
|
||||||
|
Avarice_OnMessage_Slot(nextSlot).connect(link, message);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
relatedSlotClass = class'Avarice_OnMessage_Slot'
|
||||||
|
}
|
||||||
40
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Slot.uc
Normal file
40
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Slot.uc
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Slot class implementation for `Avarice`'s `OnMessage` signal.
|
||||||
|
* Copyright 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Avarice_OnMessage_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate connect(AvariceLink link, AvariceMessage message)
|
||||||
|
{
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
311
kf_sources/AcediaCore/Classes/BaseAliasSource.uc
Normal file
311
kf_sources/AcediaCore/Classes/BaseAliasSource.uc
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
/**
|
||||||
|
* Aliases allow users to define human-readable and easier to use
|
||||||
|
* "synonyms" to some symbol sequences (mainly names of UnrealScript classes).
|
||||||
|
* This is an interface class that can be implemented in various different
|
||||||
|
* ways.
|
||||||
|
* Several `AliasSource`s are supposed to exist separately, each storing
|
||||||
|
* aliases of particular kind: for weapon, zeds, colors, etc..
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class BaseAliasSource extends AcediaObject
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # `AliasSource`
|
||||||
|
*
|
||||||
|
* Interface for any alias source instance that includes basic methods for
|
||||||
|
* alias lookup and adding/removal.
|
||||||
|
*
|
||||||
|
* ## Aliases
|
||||||
|
*
|
||||||
|
* Aliases in Acedia are usually defined as either `$<alias_name>`.
|
||||||
|
* `<alias_name>` can only contain ASCII latin letters and digits.
|
||||||
|
* `<alias_name>` is meant to define a human-readable name for something
|
||||||
|
* (e.g. "m14" for `KFMod.M14EBRBattleRifle`).
|
||||||
|
* Aliases are *case-insensitive*.
|
||||||
|
* '$' prefix is used to emphasize that what user is specifying is,
|
||||||
|
* in fact, an alias and it *is not* actually important for aliases feature
|
||||||
|
* and API: only `<alias_name>` is used. However, for convenience's sake,
|
||||||
|
* `AliasesAPI` usually recognizes aliases both with and without '$' prefix.
|
||||||
|
* Alias sources (classes derived from `BaseAliasSource`), however should only
|
||||||
|
* handle resolving `<alias_name>`, treating '$' prefix as a mistake in alias
|
||||||
|
* parameter.
|
||||||
|
*
|
||||||
|
* ## Implementation
|
||||||
|
*
|
||||||
|
* As far as implementation goes, it's up to you how your own child alias
|
||||||
|
* source class is configured to obtain its aliases, but `Aliases_Feature`
|
||||||
|
* should only create a single instance for every source class (although
|
||||||
|
* nothing can prevent other mods from creating more instances, so we cannot
|
||||||
|
* give any guarantees).
|
||||||
|
* Methods that add or remove aliases are allowed to fail for whatever
|
||||||
|
* reason is valid for your source's case (it might forbid adding aliases
|
||||||
|
* at all), as long as they return `false`.
|
||||||
|
* Although all built-in aliases are storing case-insensitive values,
|
||||||
|
* `BaseAliasSource` does not demand that and allows to configure this behavior
|
||||||
|
* via `AreValuesCaseSensitive()`. This is important for `GetAliases()` method
|
||||||
|
* that returns all aliases referring to a given value.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether caller alias source class stores case-sensitive values.
|
||||||
|
*
|
||||||
|
* This information is necessary for organizing lookup of aliases by
|
||||||
|
* a given value. Aliases themselves are always case-insensitive.
|
||||||
|
*
|
||||||
|
* This method should not change returned value for any fixed class.
|
||||||
|
*
|
||||||
|
* @return `true` if stored values are case-sensitive and `false` if they are
|
||||||
|
* case-insensitive.
|
||||||
|
*/
|
||||||
|
public static function bool AreValuesCaseSensitive();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all aliases that are stored in the caller source/
|
||||||
|
*
|
||||||
|
* @return Array of all aliases inside the caller alias source. All `Text`
|
||||||
|
* references are guaranteed to not be `none` or duplicated.
|
||||||
|
*/
|
||||||
|
public function array<Text> GetAllAliases();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all aliases that are stored in the caller source/
|
||||||
|
*
|
||||||
|
* @return Array of all aliases inside the caller alias source. All `Text`
|
||||||
|
* references are guaranteed to not be `none` or duplicated.
|
||||||
|
*/
|
||||||
|
public function array<string> GetAllAliases_S()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<Text> resultWithTexts;
|
||||||
|
local array<string> result;
|
||||||
|
|
||||||
|
resultWithTexts = GetAllAliases();
|
||||||
|
for (i = 0; i < resultWithTexts.length; i += 1) {
|
||||||
|
result[result.length] = resultWithTexts[i].ToString();
|
||||||
|
}
|
||||||
|
_.memory.FreeMany(resultWithTexts);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all aliases that represent given value `value`.
|
||||||
|
*
|
||||||
|
* @param value Value for which to return all its aliases.
|
||||||
|
* Whether it is treated as case-sensitive is decided by
|
||||||
|
* `AreValuesCaseSensitive()` method, but all default alias sources are
|
||||||
|
* case-insensitive.
|
||||||
|
* @return Array of all aliases that refer to the given `value` inside
|
||||||
|
* the caller alias source. All `Text` references are guaranteed to not be
|
||||||
|
* `none` or duplicated.
|
||||||
|
*/
|
||||||
|
public function array<Text> GetAliases(BaseText value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all aliases that represent given value `value`.
|
||||||
|
*
|
||||||
|
* @param value Value for which to return all its aliases.
|
||||||
|
* Whether it is treated as case-sensitive is decided by
|
||||||
|
* `AreValuesCaseSensitive()` method, but all default alias sources are
|
||||||
|
* case-insensitive.
|
||||||
|
* @return Array of all aliases that refer to the given `value` inside
|
||||||
|
* the caller alias source. All `string` references are guaranteed to not
|
||||||
|
* be duplicated.
|
||||||
|
*/
|
||||||
|
public function array<string> GetAliases_S(string value)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Text valueAsText;
|
||||||
|
local array<Text> resultWithTexts;
|
||||||
|
local array<string> result;
|
||||||
|
|
||||||
|
valueAsText = _.text.FromString(value);
|
||||||
|
resultWithTexts = GetAliases(valueAsText);
|
||||||
|
_.memory.Free(valueAsText);
|
||||||
|
for (i = 0; i < resultWithTexts.length; i += 1) {
|
||||||
|
result[result.length] = resultWithTexts[i].ToString();
|
||||||
|
}
|
||||||
|
_.memory.FreeMany(resultWithTexts);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given alias is present in caller `AliasSource`.
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param alias Alias to check, case-insensitive.
|
||||||
|
* @return `true` if present, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public function bool HasAlias(BaseText alias);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given alias is present in caller `AliasSource`.
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param alias Alias to check, case-insensitive.
|
||||||
|
* @return `true` if present, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public function bool HasAlias_S(string alias)
|
||||||
|
{
|
||||||
|
local bool result;
|
||||||
|
local Text aliasAsText;
|
||||||
|
|
||||||
|
aliasAsText = _.text.FromString(alias);
|
||||||
|
result = HasAlias(aliasAsText);
|
||||||
|
_.memory.Free(aliasAsText);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns value stored for the given alias in caller `AliasSource`
|
||||||
|
* (as well as it's `Aliases` objects).
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to return
|
||||||
|
* a value. Case-insensitive. If given `alias` starts with "$" character -
|
||||||
|
* that character will be removed before resolving that alias.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return `none`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias.Copy()`.
|
||||||
|
* If `alias == none` method always returns `none`.
|
||||||
|
*/
|
||||||
|
public function Text Resolve(BaseText alias, optional bool copyOnFailure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns value stored for the given alias in caller `AliasSource`
|
||||||
|
* (as well as it's `Aliases` objects).
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param alias Alias, for which method will attempt to return
|
||||||
|
* a value. Case-insensitive. If given `alias` starts with "$" character -
|
||||||
|
* that character will be removed before resolving that alias.
|
||||||
|
* @param copyOnFailure Whether method should return copy of original
|
||||||
|
* `alias` value in case caller source did not have any records
|
||||||
|
* corresponding to `alias`.
|
||||||
|
* @return If look up was successful - value, associated with the given
|
||||||
|
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
|
||||||
|
* flag: `copyOnFailure == false` means method will return empty `string`
|
||||||
|
* and `copyOnFailure == true` means method will return `alias`.
|
||||||
|
*/
|
||||||
|
public function string Resolve_S(
|
||||||
|
string alias,
|
||||||
|
optional bool copyOnFailure)
|
||||||
|
{
|
||||||
|
local Text resultAsText;
|
||||||
|
local Text aliasAsText;
|
||||||
|
|
||||||
|
aliasAsText = _.text.FromString(alias);
|
||||||
|
resultAsText = Resolve(aliasAsText, copyOnFailure);
|
||||||
|
return _.text.IntoString(resultAsText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds another alias to the caller `AliasSource`.
|
||||||
|
* If alias with the same name as `aliasToAdd` already exists - method
|
||||||
|
* overwrites it.
|
||||||
|
*
|
||||||
|
* Can fail iff `aliasToAdd` is an invalid alias or `aliasValue == none`.
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param aliasToAdd Alias that you want to add to caller source.
|
||||||
|
* Alias names are case-insensitive.
|
||||||
|
* @param aliasValue Intended value of this alias.
|
||||||
|
* @return `true` if alias was added and `false` otherwise (alias was invalid).
|
||||||
|
*/
|
||||||
|
public function bool AddAlias(BaseText aliasToAdd, BaseText aliasValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds another alias to the caller `AliasSource`.
|
||||||
|
* If alias with the same name as `aliasToAdd` already exists, -
|
||||||
|
* method overwrites it.
|
||||||
|
*
|
||||||
|
* Can fail iff `aliasToAdd` is an invalid alias.
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param aliasToAdd Alias that you want to add to caller source.
|
||||||
|
* Alias names are case-insensitive.
|
||||||
|
* @param aliasValue Intended value of this alias.
|
||||||
|
* @return `true` if alias was added and `false` otherwise (alias was invalid).
|
||||||
|
*/
|
||||||
|
public function bool AddAlias_S(string aliasToAdd, string aliasValue)
|
||||||
|
{
|
||||||
|
local bool result;
|
||||||
|
local Text aliasAsText, valueAsText;
|
||||||
|
|
||||||
|
aliasAsText = _.text.FromString(aliasToAdd);
|
||||||
|
valueAsText = _.text.FromString(aliasValue);
|
||||||
|
result = AddAlias(aliasAsText, valueAsText);
|
||||||
|
_.memory.Free(aliasAsText);
|
||||||
|
_.memory.Free(valueAsText);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes alias (all records with it, in case of duplicates) from
|
||||||
|
* the caller `AliasSource`.
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param aliasToRemove Alias that you want to remove from caller source.
|
||||||
|
* @return `true` if an alias was present in the source and was deleted and
|
||||||
|
* `false` if there was no specified alias in the first place.
|
||||||
|
*/
|
||||||
|
public function bool RemoveAlias(BaseText aliasToRemove);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes alias (all records with it, in case of duplicates) from
|
||||||
|
* the caller `AliasSource`.
|
||||||
|
*
|
||||||
|
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param aliasToRemove Alias that you want to remove from caller source.
|
||||||
|
* @return `true` if an alias was present in the source and was deleted and
|
||||||
|
* `false` if there was no specified alias in the first place.
|
||||||
|
*/
|
||||||
|
public function bool RemoveAlias_S(string aliasToRemove)
|
||||||
|
{
|
||||||
|
local bool result;
|
||||||
|
local Text aliasAsText;
|
||||||
|
|
||||||
|
aliasAsText = _.text.FromString(aliasToRemove);
|
||||||
|
result = RemoveAlias(aliasAsText);
|
||||||
|
_.memory.Free(aliasAsText);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
654
kf_sources/AcediaCore/Classes/BigInt.uc
Normal file
654
kf_sources/AcediaCore/Classes/BigInt.uc
Normal file
@ -0,0 +1,654 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2022-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class BigInt extends AcediaObject
|
||||||
|
dependson(MathApi);
|
||||||
|
|
||||||
|
/// A simple big integer implementation.
|
||||||
|
///
|
||||||
|
/// [`BigInt`]'s main purpose is to allow Acedia's databases to store integers of arbitrary size.
|
||||||
|
/// It can be used for long arithmetic computations, but it was mainly meant as a players'
|
||||||
|
/// statistics counter and, therefore, not optimized for performing large amount of operations.
|
||||||
|
|
||||||
|
/// [`BigInt`] data as a struct - meant to be used to store [`BigInt`]'s values inside
|
||||||
|
/// the local databases.
|
||||||
|
struct BigIntData {
|
||||||
|
var bool negative;
|
||||||
|
var array<byte> digits;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Result of comparison for [`BigInt`]s with each other.
|
||||||
|
enum BigIntCompareResult
|
||||||
|
{
|
||||||
|
BICR_Less,
|
||||||
|
BICR_Equal,
|
||||||
|
BICR_Greater
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Does stored [`BigInt`] have a negative sign?
|
||||||
|
var private bool negative;
|
||||||
|
/// Digits array, from least to most significant. For example, for 13524:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// `digits[0] = 4`
|
||||||
|
/// `digits[1] = 2`
|
||||||
|
/// `digits[2] = 5`
|
||||||
|
/// `digits[3] = 3`
|
||||||
|
/// `digits[4] = 1`
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Valid [`BigInt`] should not have this array empty: zero should be represented by an array with
|
||||||
|
/// a single `0`-element.
|
||||||
|
/// This isn't a most efficient representation for [`BigInt`], but it's easy to convert to and from
|
||||||
|
/// decimal representation.
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
///
|
||||||
|
/// This array must not have leading (in the sense of significance) zeroes.
|
||||||
|
/// That is, last element of the array should not be a `0`.
|
||||||
|
/// The only exception if if stored value is `0`, then `digits` must consist of
|
||||||
|
/// a single `0` element.
|
||||||
|
var private array<byte> digits;
|
||||||
|
|
||||||
|
/// Constants useful for converting [`BigInt`] back to [`int`], while avoiding overflow.
|
||||||
|
/// We can add less digits than that without any fear of overflow.
|
||||||
|
const DIGITS_IN_MAX_INT = 10;
|
||||||
|
/// Maximum [`int`] value is `2147483647`, so in case most significant digit is 10th and is `2`
|
||||||
|
/// (so number has a form of `2xxxxxxxxx`), to check for overflow we only need to compare
|
||||||
|
/// combination of the rest of the digits with this constant.
|
||||||
|
const ALMOST_MAX_INT = 147483647;
|
||||||
|
/// To add last digit we add/subtract that digit multiplied by this value.
|
||||||
|
const LAST_DIGIT_ORDER = 1000000000;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
SetZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
negative = false;
|
||||||
|
digits.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auxiliary method to set current value to zero
|
||||||
|
private function SetZero() {
|
||||||
|
negative = false;
|
||||||
|
digits.length = 1;
|
||||||
|
digits[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal [`int`] value `-2,147,483,648` is somewhat of a pain to handle, so just use this
|
||||||
|
// auxiliary pre-made constructor for it
|
||||||
|
private function SetMinimalNegative() {
|
||||||
|
negative = true;
|
||||||
|
digits.length = 10;
|
||||||
|
digits[0] = 8;
|
||||||
|
digits[1] = 4;
|
||||||
|
digits[2] = 6;
|
||||||
|
digits[3] = 3;
|
||||||
|
digits[4] = 8;
|
||||||
|
digits[5] = 4;
|
||||||
|
digits[6] = 7;
|
||||||
|
digits[7] = 4;
|
||||||
|
digits[8] = 1;
|
||||||
|
digits[9] = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes unnecessary zeroes from leading digit positions `digits`.
|
||||||
|
// Does not change contained value.
|
||||||
|
private final function TrimLeadingZeroes() {
|
||||||
|
local int i, zeroesToRemove;
|
||||||
|
|
||||||
|
// Finds how many leading zeroes there is.
|
||||||
|
// Since `digits` stores digits from least to most significant, we need to check from the end of
|
||||||
|
// `digits` array.
|
||||||
|
for (i = digits.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (digits[i] != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
zeroesToRemove += 1;
|
||||||
|
}
|
||||||
|
// `digits` must not be empty, enforce `0` value in that case
|
||||||
|
if (zeroesToRemove >= digits.length) {
|
||||||
|
SetZero();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
digits.length = digits.length - zeroesToRemove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes current value of [`BigInt`] to given value.
|
||||||
|
public final function Set(BigInt value)
|
||||||
|
{
|
||||||
|
if (value != none) {
|
||||||
|
value.TrimLeadingZeroes();
|
||||||
|
digits = value.digits;
|
||||||
|
negative = value.negative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes current value of [`BigInt`] to given value.
|
||||||
|
public final function SetInt(int value) {
|
||||||
|
local MathApi.IntegerDivisionResult divisionResult;
|
||||||
|
|
||||||
|
negative = false;
|
||||||
|
digits.length = 0;
|
||||||
|
if (value < 0) {
|
||||||
|
// Treat special case of minimal [`int`] value `-2,147,483,648` that
|
||||||
|
// won't fit into positive [`int`] as special and use pre-made
|
||||||
|
// specialized constructor `CreateMinimalNegative()`
|
||||||
|
if (value < -maxInt) {
|
||||||
|
SetMinimalNegative();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
negative = true;
|
||||||
|
value *= -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value == 0) {
|
||||||
|
digits[0] = 0;
|
||||||
|
} else {
|
||||||
|
while (value > 0) {
|
||||||
|
divisionResult = __().math.IntegerDivision(value, 10);
|
||||||
|
value = divisionResult.quotient;
|
||||||
|
digits[digits.length] = divisionResult.remainder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrimLeadingZeroes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes current value of [`BigInt`] to the value, given by decimal representation.
|
||||||
|
///
|
||||||
|
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given
|
||||||
|
/// as an argument, caller's value won't change.
|
||||||
|
/// Returns `true` in case of success and `false` otherwise.
|
||||||
|
public final function bool SetDecimal(BaseText value) {
|
||||||
|
local int i;
|
||||||
|
local byte nextDigit;
|
||||||
|
local bool newNegative;
|
||||||
|
local array<byte> newDigits;
|
||||||
|
local Parser parser;
|
||||||
|
local Basetext.Character nextCharacter;
|
||||||
|
|
||||||
|
if (value == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
parser = value.Parse();
|
||||||
|
newNegative = ParseSign(parser);
|
||||||
|
// Reset to valid state whether sign was consumed or not
|
||||||
|
parser.Confirm();
|
||||||
|
parser.R();
|
||||||
|
newDigits.length = parser.GetRemainingLength();
|
||||||
|
// Parse new one
|
||||||
|
i = newDigits.length - 1;
|
||||||
|
while (!parser.HasFinished()){
|
||||||
|
// This should not happen, but just in case
|
||||||
|
if (i < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parser.MCharacter(nextCharacter);
|
||||||
|
nextDigit = __().text.CharacterToInt(nextCharacter, 10);
|
||||||
|
if (nextDigit < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
newDigits[i] = nextDigit;
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
parser.FreeSelf();
|
||||||
|
digits = newDigits;
|
||||||
|
negative = newNegative;
|
||||||
|
TrimLeadingZeroes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to parse either `+` or `-` and returns `true` iff it parsed `-`.
|
||||||
|
// If neither got parsed, `parser` will enter failed state.
|
||||||
|
// Assumes `parser` isn't `none`.
|
||||||
|
private final function bool ParseSign(Parser parser) {
|
||||||
|
parser.Match(P("-"));
|
||||||
|
negative = parser.Ok();
|
||||||
|
if (parser.Ok()) {
|
||||||
|
negative = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parser.R();
|
||||||
|
parser.Match(P("+"));
|
||||||
|
}
|
||||||
|
return negative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes current value of [`BigInt`] to the value, given by decimal representation.
|
||||||
|
///
|
||||||
|
/// If invalid decimal representation (digits only, possibly with leading sign) is given as
|
||||||
|
/// an argument, caller's value won't change.
|
||||||
|
/// Returns `true` in case of success and `false` otherwise.
|
||||||
|
public final function bool SetDecimal_S(string value) {
|
||||||
|
local bool result;
|
||||||
|
local MutableText wrapper;
|
||||||
|
|
||||||
|
wrapper = __().text.FromStringM(value);
|
||||||
|
result = SetDecimal(wrapper);
|
||||||
|
wrapper.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auxiliary method for comparing two [`BigInt`]s by their absolute value.
|
||||||
|
private function BigIntCompareResult _compareAbsolute(BigInt other) {
|
||||||
|
local int i;
|
||||||
|
local array<byte> otherDigits;
|
||||||
|
|
||||||
|
otherDigits = other.digits;
|
||||||
|
if (digits.length == otherDigits.length)
|
||||||
|
{
|
||||||
|
for (i = digits.length - 1; i >= 0; i -= 1)
|
||||||
|
{
|
||||||
|
if (digits[i] < otherDigits[i]) {
|
||||||
|
return BICR_Less;
|
||||||
|
}
|
||||||
|
if (digits[i] > otherDigits[i]) {
|
||||||
|
return BICR_Greater;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BICR_Equal;
|
||||||
|
}
|
||||||
|
if (digits.length < otherDigits.length) {
|
||||||
|
return BICR_Less;
|
||||||
|
}
|
||||||
|
return BICR_Greater;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares caller [`BigInt`] to [`other`].
|
||||||
|
///
|
||||||
|
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
|
||||||
|
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
|
||||||
|
/// it means that caller [`BigInt`] is smaller that `other`.
|
||||||
|
/// If argument is `none`, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
|
||||||
|
public function BigIntCompareResult Compare(BigInt other) {
|
||||||
|
local BigIntCompareResult resultForModulus;
|
||||||
|
|
||||||
|
if (other == none) return BICR_Less;
|
||||||
|
if (negative && !other.negative) return BICR_Less;
|
||||||
|
if (!negative && other.negative) return BICR_Greater;
|
||||||
|
resultForModulus = _compareAbsolute(other);
|
||||||
|
if (resultForModulus == BICR_Equal) return BICR_Equal;
|
||||||
|
if (negative && (resultForModulus == BICR_Greater)) return BICR_Less;
|
||||||
|
if (!negative && (resultForModulus == BICR_Less)) return BICR_Less;
|
||||||
|
|
||||||
|
return BICR_Greater;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares caller [`BigInt`] to [`other`].
|
||||||
|
///
|
||||||
|
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
|
||||||
|
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
|
||||||
|
/// it means that caller [`BigInt`] is smaller that `other`.
|
||||||
|
public function BigIntCompareResult CompareInt(int other) {
|
||||||
|
local BigInt wrapper;
|
||||||
|
local BigIntCompareResult result;
|
||||||
|
|
||||||
|
wrapper = _.math.ToBigInt(other);
|
||||||
|
result = Compare(wrapper);
|
||||||
|
wrapper.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares caller [`BigInt`] to a decimal representation of a number.
|
||||||
|
///
|
||||||
|
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
|
||||||
|
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
|
||||||
|
/// it means that caller [`BigInt`] is smaller that `other`.
|
||||||
|
/// If argument is `none` or is an invalid decimal representation (digits only, possibly with
|
||||||
|
/// leading sign, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
|
||||||
|
public function BigIntCompareResult CompareDecimal(BaseText other) {
|
||||||
|
local BigInt wrapper;
|
||||||
|
local BigIntCompareResult result;
|
||||||
|
|
||||||
|
wrapper = _.math.MakeBigInt(other);
|
||||||
|
result = Compare(wrapper);
|
||||||
|
_.memory.Free(wrapper);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares caller [`BigInt`] to a decimal representation of a number.
|
||||||
|
///
|
||||||
|
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
|
||||||
|
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
|
||||||
|
/// it means that caller [`BigInt`] is smaller that `other`.
|
||||||
|
/// If argument is an invalid decimal representation (digits only, possibly with leading sign, then
|
||||||
|
/// it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
|
||||||
|
public function BigIntCompareResult CompareDecimal_S(string other) {
|
||||||
|
local BigInt wrapper;
|
||||||
|
local BigIntCompareResult result;
|
||||||
|
|
||||||
|
wrapper = _.math.MakeBigInt_S(other);
|
||||||
|
result = Compare(wrapper);
|
||||||
|
wrapper.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds absolute values of caller [`BigInt`] and [`other`] with no changes to the sign.
|
||||||
|
private function _add(BigInt other) {
|
||||||
|
local int i;
|
||||||
|
local byte carry, digitSum;
|
||||||
|
local array<byte> otherDigits;
|
||||||
|
|
||||||
|
if (other == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
otherDigits = other.digits;
|
||||||
|
if (digits.length < otherDigits.length) {
|
||||||
|
digits.length = otherDigits.length;
|
||||||
|
} else {
|
||||||
|
otherDigits.length = digits.length;
|
||||||
|
}
|
||||||
|
carry = 0;
|
||||||
|
for (i = 0; i < digits.length; i += 1) {
|
||||||
|
digitSum = digits[i] + otherDigits[i] + carry;
|
||||||
|
digits[i] = _.math.Remainder(digitSum, 10);
|
||||||
|
carry = (digitSum - digits[i]) / 10;
|
||||||
|
}
|
||||||
|
if (carry > 0) {
|
||||||
|
digits[digits.length] = carry;
|
||||||
|
}
|
||||||
|
// No leading zeroes can be created here, so no need to trim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtracts absolute value of [`other`] from the caller [`BigInt`], flipping caller's sign in case
|
||||||
|
// `other`'s absolute value is bigger.
|
||||||
|
private function _sub(BigInt other) {
|
||||||
|
local int i;
|
||||||
|
local int carry, nextDigit;
|
||||||
|
local array<byte> minuendDigits, subtrahendDigits;
|
||||||
|
local BigIntCompareResult resultForModulus;
|
||||||
|
|
||||||
|
if (other == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resultForModulus = _compareAbsolute(other);
|
||||||
|
if (resultForModulus == BICR_Equal) {
|
||||||
|
SetZero();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resultForModulus == BICR_Less) {
|
||||||
|
negative = !negative;
|
||||||
|
minuendDigits = other.digits;
|
||||||
|
subtrahendDigits = digits;
|
||||||
|
} else {
|
||||||
|
minuendDigits = digits;
|
||||||
|
subtrahendDigits = other.digits;
|
||||||
|
}
|
||||||
|
digits.length = minuendDigits.length;
|
||||||
|
subtrahendDigits.length = minuendDigits.length;
|
||||||
|
carry = 0;
|
||||||
|
for (i = 0; i < digits.length; i += 1) {
|
||||||
|
nextDigit = int(minuendDigits[i]) - int(subtrahendDigits[i]) + carry;
|
||||||
|
if (nextDigit < 0) {
|
||||||
|
nextDigit += 10;
|
||||||
|
carry = -1;
|
||||||
|
} else {
|
||||||
|
carry = 0;
|
||||||
|
}
|
||||||
|
digits[i] = nextDigit;
|
||||||
|
}
|
||||||
|
TrimLeadingZeroes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds another value to the caller [`BigInt`].
|
||||||
|
///
|
||||||
|
/// If argument is `none`, then given method does nothing.
|
||||||
|
public function Add(BigInt other) {
|
||||||
|
if (other == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (negative == other.negative) {
|
||||||
|
_add(other);
|
||||||
|
} else {
|
||||||
|
_sub(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds another value to the caller [`BigInt`].
|
||||||
|
public function AddInt(int other) {
|
||||||
|
local BigInt otherObject;
|
||||||
|
|
||||||
|
otherObject = _.math.ToBigInt(other);
|
||||||
|
Add(otherObject);
|
||||||
|
_.memory.Free(otherObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds decimal representation of the number to the caller [`BigInt`].
|
||||||
|
///
|
||||||
|
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given -
|
||||||
|
/// does nothing.
|
||||||
|
public function AddDecimal(BaseText other) {
|
||||||
|
local BigInt otherObject;
|
||||||
|
|
||||||
|
if (other == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
otherObject = _.math.MakeBigInt(other);
|
||||||
|
Add(otherObject);
|
||||||
|
_.memory.Free(otherObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds decimal representation of the number to the caller [`BigInt`].
|
||||||
|
///
|
||||||
|
/// If invalid decimal representation (digits only, possibly with leading sign) is given -
|
||||||
|
/// does nothing.
|
||||||
|
public function AddDecimal_S(string other) {
|
||||||
|
local BigInt otherObject;
|
||||||
|
|
||||||
|
otherObject = _.math.MakeBigInt_S(other);
|
||||||
|
Add(otherObject);
|
||||||
|
_.memory.Free(otherObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtracts another value to the caller [`BigInt`].
|
||||||
|
///
|
||||||
|
/// If argument is `none`, then given method does nothing.
|
||||||
|
public function Subtract(BigInt other) {
|
||||||
|
if (negative != other.negative) {
|
||||||
|
_add(other);
|
||||||
|
} else {
|
||||||
|
_sub(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds another value to the caller [`BigInt`].
|
||||||
|
public function SubtractInt(int other) {
|
||||||
|
local BigInt otherObject;
|
||||||
|
|
||||||
|
otherObject = _.math.ToBigInt(other);
|
||||||
|
Subtract(otherObject);
|
||||||
|
_.memory.Free(otherObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtracts decimal representation of the number to the caller [`BigInt`].
|
||||||
|
///
|
||||||
|
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given -
|
||||||
|
/// does nothing.
|
||||||
|
public function SubtractDecimal(BaseText other) {
|
||||||
|
local BigInt otherObject;
|
||||||
|
|
||||||
|
if (other == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
otherObject = _.math.MakeBigInt(other);
|
||||||
|
Subtract(otherObject);
|
||||||
|
_.memory.Free(otherObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds decimal representation of the number to the caller [`BigInt`].
|
||||||
|
///
|
||||||
|
/// If invalid decimal representation (digits only, possibly with leading sign) is given -
|
||||||
|
/// does nothing.
|
||||||
|
public function SubtractDecimal_S(string other) {
|
||||||
|
local BigInt otherObject;
|
||||||
|
|
||||||
|
otherObject = _.math.MakeBigInt_S(other);
|
||||||
|
Subtract(otherObject);
|
||||||
|
_.memory.Free(otherObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if caller [`BigInt`] is negative.
|
||||||
|
///
|
||||||
|
/// Returns if stored value is negative and `false` otherwise.
|
||||||
|
/// Zero is not considered negative number.
|
||||||
|
public function bool IsNegative() {
|
||||||
|
// Handle special case of zero first (it ignores `negative` flag)
|
||||||
|
if (digits.length == 1 && digits[0] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return negative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts caller [`BigInt`] into [`int`] representation.
|
||||||
|
///
|
||||||
|
/// In case stored value is outside `int`'s value range
|
||||||
|
/// (`[-maxInt-1, maxInt] == [-2147483648; 2147483647]`), method returns either maximal or minimal
|
||||||
|
// possible value, depending on the [`BigInt`]'s sign.
|
||||||
|
public function int ToInt() {
|
||||||
|
local int i;
|
||||||
|
local int accumulator;
|
||||||
|
local int safeDigitsAmount;
|
||||||
|
|
||||||
|
if (digits.length <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (digits.length > DIGITS_IN_MAX_INT) {
|
||||||
|
if (negative) {
|
||||||
|
return (-maxInt - 1);
|
||||||
|
} else {
|
||||||
|
return maxInt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At most `DIGITS_IN_MAX_INT - 1` iterations
|
||||||
|
safeDigitsAmount = Min(DIGITS_IN_MAX_INT - 1, digits.length);
|
||||||
|
for (i = safeDigitsAmount - 1; i >= 0; i -= 1) {
|
||||||
|
accumulator *= 10;
|
||||||
|
accumulator += digits[i];
|
||||||
|
}
|
||||||
|
if (negative) {
|
||||||
|
accumulator *= -1;
|
||||||
|
}
|
||||||
|
accumulator = AddUnsafeDigitToInt(accumulator);
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adding `DIGITS_IN_MAX_INT - 1` will never lead to an overflow, but adding the next digit can,
|
||||||
|
/// so we need to handle it differently and more carefully.
|
||||||
|
/// Assumes `digits.length <= DIGITS_IN_MAX_INT`.
|
||||||
|
private function int AddUnsafeDigitToInt(int accumulator) {
|
||||||
|
local int unsafeDigit;
|
||||||
|
local bool noOverflow;
|
||||||
|
|
||||||
|
if (digits.length < DIGITS_IN_MAX_INT) {
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
unsafeDigit = digits[DIGITS_IN_MAX_INT - 1];
|
||||||
|
// `maxInt` stats with `2`, so if last/unsafe digit is either `0` or `1`, there is no overflow,
|
||||||
|
// otherwise - check rest of the digits
|
||||||
|
noOverflow = (unsafeDigit < 2);
|
||||||
|
if (unsafeDigit == 2) {
|
||||||
|
// Include `maxInt` and `-maxInt-1` (minimal possible value) into an overflow too - this way
|
||||||
|
// we still give a correct result, but do not have to worry about `int`-arithmetic error
|
||||||
|
noOverflow = noOverflow
|
||||||
|
|| (negative && (accumulator > -ALMOST_MAX_INT - 1))
|
||||||
|
|| (!negative && (accumulator < ALMOST_MAX_INT));
|
||||||
|
}
|
||||||
|
if (noOverflow) {
|
||||||
|
if (negative) {
|
||||||
|
accumulator -= unsafeDigit * LAST_DIGIT_ORDER;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
accumulator += unsafeDigit * LAST_DIGIT_ORDER;
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
// Handle overflow
|
||||||
|
if (negative) {
|
||||||
|
return (-maxInt - 1);
|
||||||
|
}
|
||||||
|
return maxInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts caller [`BigInt`] into [`Text`] representation.
|
||||||
|
public function Text ToText() {
|
||||||
|
return ToText_M().IntoText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts caller [`BigInt`] into [`MutableText`] representation.
|
||||||
|
public function MutableText ToText_M() {
|
||||||
|
local int i;
|
||||||
|
local MutableText result;
|
||||||
|
|
||||||
|
result = _.text.Empty();
|
||||||
|
if (negative) {
|
||||||
|
result.AppendCharacter(_.text.GetCharacter("-"));
|
||||||
|
}
|
||||||
|
for (i = digits.length - 1; i >= 0; i -= 1) {
|
||||||
|
result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts caller [`BigInt`] into [`string`] representation.
|
||||||
|
public function string ToString() {
|
||||||
|
local int i;
|
||||||
|
local string result;
|
||||||
|
|
||||||
|
if (negative) {
|
||||||
|
result = "-";
|
||||||
|
}
|
||||||
|
for (i = digits.length - 1; i >= 0; i -= 1) {
|
||||||
|
result = result $ digits[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores [`BigInt`] from the [`BigIntData`] value.
|
||||||
|
///
|
||||||
|
/// This method is created to make an efficient way to store [`BigInt`].
|
||||||
|
public function FromData(BigIntData data) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
negative = data.negative;
|
||||||
|
digits = data.digits;
|
||||||
|
// Deal with possibly erroneous data
|
||||||
|
for (i = 0; i < digits.length; i += 1) {
|
||||||
|
if (digits[i] > 9) {
|
||||||
|
digits[i] = 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts caller [`BigInt`]'s value into [`BigIntData`].
|
||||||
|
///
|
||||||
|
/// This method is created to make an efficient way to store [`BigInt`].
|
||||||
|
public function BigIntData ToData() {
|
||||||
|
local BigIntData result;
|
||||||
|
|
||||||
|
result.negative = negative;
|
||||||
|
result.digits = digits;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
415
kf_sources/AcediaCore/Classes/ChatAPI.uc
Normal file
415
kf_sources/AcediaCore/Classes/ChatAPI.uc
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2022-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ChatApi extends AcediaObject;
|
||||||
|
|
||||||
|
///! API for accessing chat-related events.
|
||||||
|
///!
|
||||||
|
///! # Implementation
|
||||||
|
///!
|
||||||
|
///! Signal functions that track text chat messages `OnMessage()` and `OnMessageFor()` simply
|
||||||
|
///! hook into [`BroadcastApi`] the first time such signal is requested.
|
||||||
|
///!
|
||||||
|
///! Signal function [`OnVoiceMessage()`] for tracking voice replaces a function in
|
||||||
|
///! [`KFPlayerController`] to track when they are replicated.
|
||||||
|
///! Then replaced function informs [`ChatApi`] about new voice message transmissions via
|
||||||
|
///! internal [`_EmitOnVoiceMessage()`] method.
|
||||||
|
|
||||||
|
/// Lists voice messages built-in in the game.
|
||||||
|
enum BuiltInVoiceMessage {
|
||||||
|
// Support
|
||||||
|
BIVM_SupportMedic,
|
||||||
|
BIVM_SupportHelp,
|
||||||
|
BIVM_SupportAskForMoney,
|
||||||
|
BIVM_SupportAskForWeapon,
|
||||||
|
// Acknowledgements
|
||||||
|
BIVM_AckYes,
|
||||||
|
BIVM_AckNo,
|
||||||
|
BIVM_AckThanks,
|
||||||
|
BIVM_AckSorry,
|
||||||
|
// Alert
|
||||||
|
BIVM_AlretLookOut,
|
||||||
|
BIVM_AlretRun,
|
||||||
|
BIVM_AlretWaitForMe,
|
||||||
|
BIVM_AlretWeldTheDoors,
|
||||||
|
BIVM_AlretLetsHoleUpHere,
|
||||||
|
BIVM_AlretFollowMe,
|
||||||
|
// Direction
|
||||||
|
BIVM_DirectionGetToTheTrader,
|
||||||
|
BIVM_DirectionGoUpstairs,
|
||||||
|
BIVM_DirectionGoDownstairs,
|
||||||
|
BIVM_DirectionGetOutside,
|
||||||
|
BIVM_DirectionGetInside,
|
||||||
|
// Insult
|
||||||
|
BIVM_InsultSpecimens,
|
||||||
|
BIVM_InsultPlayers,
|
||||||
|
// Trader
|
||||||
|
BIVM_TraderCheckWhereTheShopIs,
|
||||||
|
BIVM_TraderGetClose,
|
||||||
|
BIVM_TraderShopOpen,
|
||||||
|
BIVM_TraderShopOpenFinal,
|
||||||
|
BIVM_Trader30SecondsUntilShopCloses,
|
||||||
|
BIVM_TraderShopClosed,
|
||||||
|
BIVM_TraderCompliment,
|
||||||
|
BIVM_TraderNotEnoughMoney,
|
||||||
|
BIVM_TraderCannotCarry,
|
||||||
|
BIVM_TraderHurryUp1,
|
||||||
|
BIVM_TraderHurryUp2,
|
||||||
|
// Auto
|
||||||
|
BIVM_AutoWelding,
|
||||||
|
BIVM_AutoUnwelding,
|
||||||
|
BIVM_AutoReload,
|
||||||
|
BIVM_AutoOutOfAmmo,
|
||||||
|
BIVM_AutoDosh,
|
||||||
|
BIVM_AutoStandStillTryingToHealYou,
|
||||||
|
BIVM_AutoLowOnHealth,
|
||||||
|
BIVM_AutoBloatAcid,
|
||||||
|
BIVM_AutoPatriarchCloack,
|
||||||
|
BIVM_AutoPatriarchMinigun,
|
||||||
|
BIVM_AutoPatriarchRocketLauncher,
|
||||||
|
BIVM_AutoGrabbedByClot,
|
||||||
|
BIVM_AutoFleshpoundSpotted,
|
||||||
|
BIVM_AutoGorefastSpotted,
|
||||||
|
BIVM_AutoScrakeSpotted,
|
||||||
|
BIVM_AutoSirenSpotten,
|
||||||
|
BIVM_AutoSirenScream,
|
||||||
|
BIVM_AutoStalkerSpotted,
|
||||||
|
BIVM_AutoCrawlertSpotted,
|
||||||
|
BIVM_AutoMeleeKilledAStalker,
|
||||||
|
BIVM_AutoUsingFlamethrower,
|
||||||
|
BIVM_AutoEquipHuntingShotgun,
|
||||||
|
BIVM_AutoEquipHandcannons,
|
||||||
|
BIVM_AutoEquipLAW,
|
||||||
|
BIVM_AutoEquipFireaxe,
|
||||||
|
// Fallback
|
||||||
|
BIVM_Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Killing Floor's native voice message is defined by `name` and `byte` pair.
|
||||||
|
/// This struct is added to allow returning them as a pair.
|
||||||
|
struct NativeVoiceMessage {
|
||||||
|
var name type;
|
||||||
|
var byte index;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Tracks whether we've already connected to broadcast signals.
|
||||||
|
var protected bool connectedToBroadcastAPI;
|
||||||
|
/// Tracks whether we've already replaced a function that allows us to catch voice messages.
|
||||||
|
var private bool replacedSendVoiceMessage;
|
||||||
|
|
||||||
|
/// Auxiliary constants that store amount of values in [`BuiltInVoiceMessage`] before
|
||||||
|
/// a certain group.
|
||||||
|
/// Used for conversion between native voice messages and [`BuiltInVoiceMessage`]
|
||||||
|
var private const int VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS;
|
||||||
|
var private const int VOICE_MESSAGES_BEFORE_ALERTS;
|
||||||
|
var private const int VOICE_MESSAGES_BEFORE_DIRECTIONS;
|
||||||
|
var private const int VOICE_MESSAGES_BEFORE_INSULTS;
|
||||||
|
var private const int VOICE_MESSAGES_BEFORE_TRADER;
|
||||||
|
var private const int VOICE_MESSAGES_BEFORE_AUTO;
|
||||||
|
var private const int VOICE_MESSAGES_TOTAL;
|
||||||
|
|
||||||
|
var protected ChatAPI_OnMessage_Signal onMessageSignal;
|
||||||
|
var protected ChatAPI_OnMessageFor_Signal onMessageForSignal;
|
||||||
|
var protected ChatAPI_OnVoiceMessage_Signal onVoiceMessageSignal;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
onMessageSignal = ChatAPI_OnMessage_Signal(_.memory.Allocate(class'ChatAPI_OnMessage_Signal'));
|
||||||
|
onMessageForSignal =
|
||||||
|
ChatAPI_OnMessageFor_Signal(_.memory.Allocate(class'ChatAPI_OnMessageFor_Signal'));
|
||||||
|
onVoiceMessageSignal =
|
||||||
|
ChatAPI_OnVoiceMessage_Signal(_.memory.Allocate(class'ChatAPI_OnVoiceMessage_Signal'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
_.memory.Free(onMessageSignal);
|
||||||
|
_.memory.Free(onMessageForSignal);
|
||||||
|
_.memory.Free(onVoiceMessageSignal);
|
||||||
|
onMessageSignal = none;
|
||||||
|
onMessageForSignal = none;
|
||||||
|
onVoiceMessageSignal = none;
|
||||||
|
_server.unreal.broadcasts.OnHandleText(self).Disconnect();
|
||||||
|
_server.unreal.broadcasts.OnHandleTextFor(self).Disconnect();
|
||||||
|
connectedToBroadcastAPI = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal that will be emitted when a player sends a message into the chat.
|
||||||
|
///
|
||||||
|
/// Allows to modify message before sending it, as well as prevent it from being sent at all.
|
||||||
|
///
|
||||||
|
/// Return `false` to prevent message from being sent.
|
||||||
|
/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted.
|
||||||
|
///
|
||||||
|
/// # Slot description
|
||||||
|
///
|
||||||
|
/// bool <slot>(EPlayer sender, MutableText message, bool teamMessage)
|
||||||
|
///
|
||||||
|
/// ## Parameters
|
||||||
|
///
|
||||||
|
/// * [`sender`]: `EPlayer` that has sent the message.
|
||||||
|
/// * [`message`]: Message that `sender` has sent.
|
||||||
|
/// This is a mutable variable and can be modified from message will be sent.
|
||||||
|
/// * [`teamMessage`]: Is this a team message (to be sent only to players on the same team)?
|
||||||
|
///
|
||||||
|
/// ## Returns
|
||||||
|
///
|
||||||
|
/// Return `false` to prevent this message from being sent at all and `true` otherwise.
|
||||||
|
/// Message will be sent only if all handlers will return `true`.
|
||||||
|
public /*signal*/ function ChatAPI_OnMessage_Slot OnMessage(AcediaObject receiver) {
|
||||||
|
TryConnectingBroadcastSignals();
|
||||||
|
return ChatAPI_OnMessage_Slot(onMessageSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal that will be emitted when a player sends a message into the chat.
|
||||||
|
///
|
||||||
|
/// Allows to modify message before sending it, as well as prevent it from being sent at all.
|
||||||
|
///
|
||||||
|
/// Return `false` to prevent message from being sent to a specific player.
|
||||||
|
/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted.
|
||||||
|
///
|
||||||
|
/// # Slot description
|
||||||
|
///
|
||||||
|
/// bool <slot>(EPlayer receiver, EPlayer sender, BaseText message)
|
||||||
|
///
|
||||||
|
/// ## Parameters
|
||||||
|
///
|
||||||
|
/// * [`receiver`]: `EPlayer` that will receive the message.
|
||||||
|
/// * [`sender`]: `EPlayer` that has sent the message.
|
||||||
|
/// * [`message`]: Message that `sender` has sent. This is an immutable variable and cannot
|
||||||
|
/// be changed at this point. Use `OnMessage()` signal function for that.
|
||||||
|
///
|
||||||
|
/// ## Returns
|
||||||
|
///
|
||||||
|
/// Return `false` to prevent this message from being sent to a particular player and
|
||||||
|
/// `true` otherwise.
|
||||||
|
/// Message will be sent only if all handlers will return `true`.
|
||||||
|
/// However decision whether to send message or not is made for every player separately.
|
||||||
|
public /*signal*/ function ChatAPI_OnMessageFor_Slot OnMessageFor(AcediaObject receiver) {
|
||||||
|
TryConnectingBroadcastSignals();
|
||||||
|
return ChatAPI_OnMessageFor_Slot(onMessageForSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal that will be emitted when a player sends a voice message.
|
||||||
|
///
|
||||||
|
/// # Slot description
|
||||||
|
///
|
||||||
|
/// void <slot>(EPlayer sender, ChatApi.BuiltInVoiceMessage message)
|
||||||
|
///
|
||||||
|
/// ## Parameters
|
||||||
|
///
|
||||||
|
/// * [`sender`]: `EPlayer` that has sent the voice message.
|
||||||
|
/// * [`message`]: Message that `sender` has sent.
|
||||||
|
public /*signal*/ function ChatAPI_OnVoiceMessage_Slot OnVoiceMessage(AcediaObject receiver) {
|
||||||
|
if (!replacedSendVoiceMessage) {
|
||||||
|
_.unflect.ReplaceFunction_S(
|
||||||
|
"KFMod.KFPlayerController.SendVoiceMessage",
|
||||||
|
"AcediaCore.Unflect_ChatApi_Controller.SendVoiceMessage",
|
||||||
|
"`ChatApi` was required to catch voice messages");
|
||||||
|
replacedSendVoiceMessage = true;
|
||||||
|
}
|
||||||
|
return ChatAPI_OnVoiceMessage_Slot(onVoiceMessageSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
public /*internal*/ function NativeVoiceMessage _enumIntoNativeVoiceMessage(
|
||||||
|
BuiltInVoiceMessage voiceMessage
|
||||||
|
) {
|
||||||
|
local int enumValue;
|
||||||
|
local NativeVoiceMessage result;
|
||||||
|
|
||||||
|
enumValue = int(voiceMessage);
|
||||||
|
if (enumValue < VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS) {
|
||||||
|
result.type = 'SUPPORT';
|
||||||
|
result.index = enumValue;
|
||||||
|
} else if (enumValue < VOICE_MESSAGES_BEFORE_ALERTS) {
|
||||||
|
result.type = 'ACK';
|
||||||
|
result.index = enumValue - VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS;
|
||||||
|
} else if (enumValue < VOICE_MESSAGES_BEFORE_DIRECTIONS) {
|
||||||
|
result.type = 'ALERT';
|
||||||
|
result.index = enumValue - VOICE_MESSAGES_BEFORE_ALERTS;
|
||||||
|
} else if (enumValue < VOICE_MESSAGES_BEFORE_INSULTS) {
|
||||||
|
result.type = 'DIRECTION';
|
||||||
|
result.index = enumValue - VOICE_MESSAGES_BEFORE_DIRECTIONS;
|
||||||
|
} else if (enumValue < VOICE_MESSAGES_BEFORE_TRADER) {
|
||||||
|
result.type = 'INSULT';
|
||||||
|
result.index = enumValue - VOICE_MESSAGES_BEFORE_INSULTS;
|
||||||
|
} else if (enumValue < VOICE_MESSAGES_BEFORE_AUTO) {
|
||||||
|
result.type = 'TRADER';
|
||||||
|
result.index = enumValue - VOICE_MESSAGES_BEFORE_TRADER;
|
||||||
|
if (result.index >= 5) {
|
||||||
|
result.index += 1;
|
||||||
|
}
|
||||||
|
} else if (enumValue < VOICE_MESSAGES_TOTAL) {
|
||||||
|
result.type = 'AUTO';
|
||||||
|
result.index = enumValue - VOICE_MESSAGES_BEFORE_AUTO;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public /*internal*/ function BuiltInVoiceMessage _nativeVoiceMessageIntoEnum(
|
||||||
|
NativeVoiceMessage voiceMessage
|
||||||
|
) {
|
||||||
|
switch (voiceMessage.type) {
|
||||||
|
case 'SUPPORT':
|
||||||
|
if (voiceMessage.index < 4) {
|
||||||
|
return BuiltInVoiceMessage(voiceMessage.index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ACK':
|
||||||
|
if (voiceMessage.index < 4) {
|
||||||
|
return BuiltInVoiceMessage(
|
||||||
|
VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS + voiceMessage.index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ALERT':
|
||||||
|
if (voiceMessage.index < 6) {
|
||||||
|
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_ALERTS + voiceMessage.index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'DIRECTION':
|
||||||
|
if (voiceMessage.index < 5) {
|
||||||
|
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_DIRECTIONS + voiceMessage.index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'INSULT':
|
||||||
|
if (voiceMessage.index < 2) {
|
||||||
|
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_INSULTS + voiceMessage.index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'TRADER':
|
||||||
|
if (voiceMessage.index < 12) {
|
||||||
|
if (voiceMessage.index < 5) {
|
||||||
|
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index);
|
||||||
|
} else if (voiceMessage.index > 5) {
|
||||||
|
return BuiltInVoiceMessage(
|
||||||
|
VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'AUTO':
|
||||||
|
if (voiceMessage.index < 25) {
|
||||||
|
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_AUTO + voiceMessage.index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return BIVM_Unknown;
|
||||||
|
}
|
||||||
|
return BIVM_Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final /*internal*/ /*native*/ function _EmitOnVoiceMessage(
|
||||||
|
PlayerController sender,
|
||||||
|
name messageType,
|
||||||
|
byte messageID
|
||||||
|
) {
|
||||||
|
local EPlayer wrapper;
|
||||||
|
local NativeVoiceMessage nativeVoiceMessage;
|
||||||
|
local BuiltInVoiceMessage builtInVoiceMessage;
|
||||||
|
|
||||||
|
if (sender == none) return;
|
||||||
|
wrapper = _.players.FromController(sender);
|
||||||
|
if (wrapper == none) return;
|
||||||
|
|
||||||
|
nativeVoiceMessage.type = messageType;
|
||||||
|
nativeVoiceMessage.index = messageID;
|
||||||
|
builtInVoiceMessage = _nativeVoiceMessageIntoEnum(nativeVoiceMessage);
|
||||||
|
if (builtInVoiceMessage != BIVM_Unknown) {
|
||||||
|
onVoiceMessageSignal.Emit(wrapper, builtInVoiceMessage);
|
||||||
|
}
|
||||||
|
_.memory.Free(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function TryConnectingBroadcastSignals() {
|
||||||
|
if (connectedToBroadcastAPI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectedToBroadcastAPI = true;
|
||||||
|
_server.unreal.broadcasts.OnHandleText(self).connect = HandleText;
|
||||||
|
_server.unreal.broadcasts.OnHandleTextFor(self).connect = HandleTextFor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bool HandleText(
|
||||||
|
Actor sender,
|
||||||
|
out string message,
|
||||||
|
name messageType,
|
||||||
|
bool teamMessage
|
||||||
|
) {
|
||||||
|
local bool result;
|
||||||
|
local MutableText messageAsText;
|
||||||
|
local EPlayer senderPlayer;
|
||||||
|
|
||||||
|
// We only want to catch chat messages from a player
|
||||||
|
if (messageType != 'Say' && messageType != 'TeamSay') return true;
|
||||||
|
senderPlayer = _.players.FromController(PlayerController(sender));
|
||||||
|
if (senderPlayer == none) return true;
|
||||||
|
|
||||||
|
messageAsText = __().text.FromColoredStringM(message);
|
||||||
|
result = onMessageSignal.Emit(senderPlayer, messageAsText, teamMessage);
|
||||||
|
message = messageAsText.ToColoredString();
|
||||||
|
// To correctly display chat messages we want to drop default color tag
|
||||||
|
// at the beginning (the one `ToColoredString()` adds if first character
|
||||||
|
// has no defined color).
|
||||||
|
// This is a compatibility consideration with vanilla UI that expects
|
||||||
|
// uncolored text. Not removing initial color tag will make chat text
|
||||||
|
// appear black.
|
||||||
|
if (!messageAsText.GetFormatting(0).isColored) {
|
||||||
|
message = Mid(message, 4);
|
||||||
|
}
|
||||||
|
_.memory.Free(messageAsText);
|
||||||
|
_.memory.Free(senderPlayer);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bool HandleTextFor(
|
||||||
|
PlayerController receiver,
|
||||||
|
Actor sender,
|
||||||
|
out string message,
|
||||||
|
name messageType
|
||||||
|
) {
|
||||||
|
local bool result;
|
||||||
|
local Text messageAsText;
|
||||||
|
local EPlayer senderPlayer, receiverPlayer;
|
||||||
|
|
||||||
|
// We only want to catch chat messages from another player
|
||||||
|
if (messageType != 'Say' && messageType != 'TeamSay') return true;
|
||||||
|
senderPlayer = _.players.FromController(PlayerController(sender));
|
||||||
|
if (senderPlayer == none) return true;
|
||||||
|
|
||||||
|
receiverPlayer = _.players.FromController(receiver);
|
||||||
|
if (receiverPlayer == none) {
|
||||||
|
_.memory.Free(senderPlayer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
messageAsText = __().text.FromColoredString(message);
|
||||||
|
result = onMessageForSignal.Emit(receiverPlayer, senderPlayer, messageAsText);
|
||||||
|
_.memory.Free(messageAsText);
|
||||||
|
_.memory.Free(senderPlayer);
|
||||||
|
_.memory.Free(receiverPlayer);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS = 4
|
||||||
|
VOICE_MESSAGES_BEFORE_ALERTS = 8
|
||||||
|
VOICE_MESSAGES_BEFORE_DIRECTIONS = 14
|
||||||
|
VOICE_MESSAGES_BEFORE_INSULTS = 19
|
||||||
|
VOICE_MESSAGES_BEFORE_TRADER = 21
|
||||||
|
VOICE_MESSAGES_BEFORE_AUTO = 32
|
||||||
|
VOICE_MESSAGES_TOTAL = 57
|
||||||
|
}
|
||||||
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Signal.uc
Normal file
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Signal.uc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Signal class implementation for `ChatAPI`'s `OnMessageFor` signal.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ChatAPI_OnMessageFor_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function bool Emit(
|
||||||
|
EPlayer receiver,
|
||||||
|
EPlayer sender,
|
||||||
|
BaseText message)
|
||||||
|
{
|
||||||
|
local Slot nextSlot;
|
||||||
|
local bool nextReply;
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none)
|
||||||
|
{
|
||||||
|
nextReply = ChatAPI_OnMessageFor_Slot(nextSlot)
|
||||||
|
.connect(receiver, sender, message);
|
||||||
|
if (!nextReply && !nextSlot.IsEmpty())
|
||||||
|
{
|
||||||
|
CleanEmptySlots();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
relatedSlotClass = class'ChatAPI_OnMessageFor_Slot'
|
||||||
|
}
|
||||||
41
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Slot.uc
Normal file
41
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Slot.uc
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Slot class implementation for `CharAPI`'s `OnMessageFor` signal.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ChatAPI_OnMessageFor_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate bool connect(EPlayer receiver, EPlayer sender, BaseText message)
|
||||||
|
{
|
||||||
|
DummyCall();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Signal.uc
Normal file
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Signal.uc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Signal class implementation for `ChatAPI`'s `OnMessage` signal.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ChatAPI_OnMessage_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function bool Emit(
|
||||||
|
EPlayer sender,
|
||||||
|
MutableText message,
|
||||||
|
bool teamMessage)
|
||||||
|
{
|
||||||
|
local Slot nextSlot;
|
||||||
|
local bool nextReply;
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none)
|
||||||
|
{
|
||||||
|
nextReply = ChatAPI_OnMessage_Slot(nextSlot)
|
||||||
|
.connect(sender, message, teamMessage);
|
||||||
|
if (!nextReply && !nextSlot.IsEmpty())
|
||||||
|
{
|
||||||
|
CleanEmptySlots();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
relatedSlotClass = class'ChatAPI_OnMessage_Slot'
|
||||||
|
}
|
||||||
44
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Slot.uc
Normal file
44
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Slot.uc
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Slot class implementation for `ChatAPI`'s `OnMessage` signal.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ChatAPI_OnMessage_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate bool connect(
|
||||||
|
EPlayer sender,
|
||||||
|
MutableText message,
|
||||||
|
bool teamMessage)
|
||||||
|
{
|
||||||
|
DummyCall();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ChatAPI_OnVoiceMessage_Signal extends Signal
|
||||||
|
dependsOn(ChatApi);
|
||||||
|
|
||||||
|
public final function Emit(EPlayer sender, ChatApi.BuiltInVoiceMessage message) {
|
||||||
|
local Slot nextSlot;
|
||||||
|
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none) {
|
||||||
|
ChatAPI_OnVoiceMessage_Slot(nextSlot).connect(sender, message);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
relatedSlotClass = class'ChatAPI_OnVoiceMessage_Slot'
|
||||||
|
}
|
||||||
41
kf_sources/AcediaCore/Classes/ChatAPI_OnVoiceMessage_Slot.uc
Normal file
41
kf_sources/AcediaCore/Classes/ChatAPI_OnVoiceMessage_Slot.uc
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ChatAPI_OnVoiceMessage_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
EPlayer sender,
|
||||||
|
ChatApi.BuiltInVoiceMessage message
|
||||||
|
) {
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
306
kf_sources/AcediaCore/Classes/CmdItemsTool.uc
Normal file
306
kf_sources/AcediaCore/Classes/CmdItemsTool.uc
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CmdItemsTool extends AcediaObject
|
||||||
|
dependson(CommandAPI)
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
//! This is a base class for auxiliary objects that will be used for storing
|
||||||
|
//! named [`Command`] instances and [`Voting`] classes: they both have in common
|
||||||
|
//! the need to remember who was authorized to use them (i.e. which user group)
|
||||||
|
//! and with what permissions (i.e. name of the config that contains appropriate
|
||||||
|
//! permissions).
|
||||||
|
//!
|
||||||
|
//! Aside from trivial accessors to its data, it also provides a way to resolve
|
||||||
|
//! the best permissions available to the user by finding the most priviledged
|
||||||
|
//! group he belongs to.
|
||||||
|
//!
|
||||||
|
//! NOTE: child classes must implement `MakeCard()` method and can override
|
||||||
|
//! `DiscardCard()` method to catch events of removing items from storage.
|
||||||
|
|
||||||
|
/// Allows to specify a base class requirement for this tool - only classes
|
||||||
|
/// that were derived from it can be stored inside.
|
||||||
|
var protected const class<AcediaObject> ruleBaseClass;
|
||||||
|
|
||||||
|
/// Names of user groups that can decide permissions for items,
|
||||||
|
/// in order of importance: from most significant to the least significant.
|
||||||
|
/// This is used for resolving the best permissions for each user.
|
||||||
|
var private array<Text> permissionGroupOrder;
|
||||||
|
|
||||||
|
/// Maps item names to their [`ItemCards`] with information about which groups
|
||||||
|
/// are authorized to use this particular item.
|
||||||
|
var private HashTable registeredCards;
|
||||||
|
|
||||||
|
var LoggerAPI.Definition errItemInvalidName;
|
||||||
|
var LoggerAPI.Definition errItemDuplicate;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
registeredCards = _.collections.EmptyHashTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
_.memory.Free(registeredCards);
|
||||||
|
_.memory.FreeMany(permissionGroupOrder);
|
||||||
|
registeredCards = none;
|
||||||
|
permissionGroupOrder.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers given item class under the specified (case-insensitive) name.
|
||||||
|
///
|
||||||
|
/// If name parameter is omitted (specified as `none`) or is an invalid name
|
||||||
|
/// (according to [`BaseText::IsValidName()`] method), then item class will not
|
||||||
|
/// be registered.
|
||||||
|
///
|
||||||
|
/// Returns `true` if item was successfully registered and `false` otherwise`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If provided name that is invalid or already taken by a different item -
|
||||||
|
/// a warning will be logged and item class won't be registered.
|
||||||
|
public function bool AddItemClass(class<AcediaObject> itemClass, BaseText itemName) {
|
||||||
|
local Text itemKey;
|
||||||
|
local ItemCard newCard, existingCard;
|
||||||
|
|
||||||
|
if (itemClass == none) return false;
|
||||||
|
if (itemName == none) return false;
|
||||||
|
if (registeredCards == none) return false;
|
||||||
|
|
||||||
|
if (ruleBaseClass == none || !ClassIsChildOf(itemClass, ruleBaseClass)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The item name is transformed into lowercase, immutable value.
|
||||||
|
// This facilitates the use of item names as keys in a [`HashTable`],
|
||||||
|
// enabling case-insensitive matching.
|
||||||
|
itemKey = itemName.LowerCopy();
|
||||||
|
if (itemKey == none || !itemKey.IsValidName()) {
|
||||||
|
_.logger.Auto(errItemInvalidName).ArgClass(itemClass).Arg(itemKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Guaranteed to only store cards
|
||||||
|
existingCard = ItemCard(registeredCards.GetItem(itemName));
|
||||||
|
if (existingCard != none) {
|
||||||
|
_.logger.Auto(errItemDuplicate)
|
||||||
|
.ArgClass(existingCard.GetItemClass())
|
||||||
|
.Arg(itemKey)
|
||||||
|
.ArgClass(itemClass);
|
||||||
|
_.memory.Free(existingCard);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
newCard = MakeCard(itemClass, itemName);
|
||||||
|
registeredCards.SetItem(itemKey, newCard);
|
||||||
|
_.memory.Free2(itemKey, newCard);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes item of given class from the list of registered items.
|
||||||
|
///
|
||||||
|
/// Removing once registered item is not an action that is expected to
|
||||||
|
/// be performed under normal circumstances and does not have an efficient
|
||||||
|
/// implementation (it is linear on the current amount of items).
|
||||||
|
///
|
||||||
|
/// Returns `true` if successfully removed registered item class and
|
||||||
|
/// `false` otherwise (either item wasn't registered or caller tool
|
||||||
|
/// initialized).
|
||||||
|
public function bool RemoveItemClass(class<AcediaObject> itemClass) {
|
||||||
|
local int i;
|
||||||
|
local CollectionIterator iter;
|
||||||
|
local ItemCard nextCard;
|
||||||
|
local array<Text> keysToRemove;
|
||||||
|
|
||||||
|
if (itemClass == none) return false;
|
||||||
|
if (registeredCards == none) return false;
|
||||||
|
|
||||||
|
// Removing items during iterator breaks an iterator, so first we find
|
||||||
|
// all the keys to remove
|
||||||
|
iter = registeredCards.Iterate();
|
||||||
|
iter.LeaveOnlyNotNone();
|
||||||
|
while (!iter.HasFinished()) {
|
||||||
|
// Guaranteed to only be `ItemCard`
|
||||||
|
nextCard = ItemCard(iter.Get());
|
||||||
|
if (nextCard.GetItemClass() == itemClass) {
|
||||||
|
keysToRemove[keysToRemove.length] = Text(iter.GetKey());
|
||||||
|
DiscardCard(nextCard);
|
||||||
|
}
|
||||||
|
_.memory.Free(nextCard);
|
||||||
|
iter.Next();
|
||||||
|
}
|
||||||
|
iter.FreeSelf();
|
||||||
|
// Actual clean up everything in `keysToRemove`
|
||||||
|
for (i = 0; i < keysToRemove.length; i += 1) {
|
||||||
|
registeredCards.RemoveItem(keysToRemove[i]);
|
||||||
|
}
|
||||||
|
_.memory.FreeMany(keysToRemove);
|
||||||
|
return (keysToRemove.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows to specify the order of the user group in terms of privilege for
|
||||||
|
/// accessing stored items. Only specified groups will be used when resolving
|
||||||
|
/// appropriate permissions config name for a user.
|
||||||
|
public final function SetPermissionGroupOrder(array<Text> groupOrder) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
_.memory.FreeMany(permissionGroupOrder);
|
||||||
|
permissionGroupOrder.length = 0;
|
||||||
|
for (i = 0; i < groupOrder.length; i += 1) {
|
||||||
|
if (groupOrder[i] != none) {
|
||||||
|
permissionGroupOrder[permissionGroupOrder.length] = groupOrder[i].Copy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specifies what permissions (given by the config name) given user group has
|
||||||
|
/// when using an item with a specified name.
|
||||||
|
///
|
||||||
|
/// Method must be called after item with a given name is added.
|
||||||
|
///
|
||||||
|
/// If this config name is specified as `none`, then "default" will be
|
||||||
|
/// used instead. For non-`none` values, only an invalid name (according to
|
||||||
|
/// [`BaseText::IsValidName()`] method) will prevent the group from being
|
||||||
|
/// registered.
|
||||||
|
///
|
||||||
|
/// Method will return `true` if group was successfully authorized and `false`
|
||||||
|
/// otherwise (either group already authorized or no item with specified name
|
||||||
|
/// was added in the caller tool so far).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If specified group was already authorized to use card's item, then it
|
||||||
|
/// will log a warning message about it.
|
||||||
|
public function bool AuthorizeUsage(BaseText itemName, BaseText groupName, BaseText configName) {
|
||||||
|
local bool result;
|
||||||
|
local ItemCard relevantCard;
|
||||||
|
|
||||||
|
if (configName != none && !configName.IsValidName()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
relevantCard = GetCard(itemName);
|
||||||
|
if (relevantCard != none) {
|
||||||
|
result = relevantCard.AuthorizeGroupWithConfig(groupName, configName);
|
||||||
|
_.memory.Free(relevantCard);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns struct with item class (+ instance, if one was stored) for a given
|
||||||
|
/// case in-sensitive item name and name of the config with best permissions
|
||||||
|
/// available to the player with provided ID.
|
||||||
|
///
|
||||||
|
/// Function only returns `none` for item class if item with a given name
|
||||||
|
/// wasn't found.
|
||||||
|
/// Config name being `none` with non-`none` item class in the result means
|
||||||
|
/// that user with provided ID doesn't have permissions for using the item at
|
||||||
|
/// all.
|
||||||
|
public final function CommandAPI.ItemConfigInfo ResolveItem(BaseText itemName, BaseText textID) {
|
||||||
|
local int i;
|
||||||
|
local ItemCard relevantCard;
|
||||||
|
local CommandAPI.ItemConfigInfo result;
|
||||||
|
|
||||||
|
relevantCard = GetCard(itemName);
|
||||||
|
if (relevantCard == none) {
|
||||||
|
// At this point contains `none` for all values -> indicates a failure
|
||||||
|
// to find item in storage
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.instance = relevantCard.GetItem();
|
||||||
|
result.class = relevantCard.GetItemClass();
|
||||||
|
if (textID == none) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Look through all `permissionGroupOrder` in order to find most priviledged
|
||||||
|
// group that user with `textID` belongs to
|
||||||
|
for (i = 0; i < permissionGroupOrder.length && result.configName == none; i += 1) {
|
||||||
|
if (_.users.IsSteamIDInGroup(textID, permissionGroupOrder[i])) {
|
||||||
|
result.configName = relevantCard.GetConfigNameForGroup(permissionGroupOrder[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(relevantCard);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all item classes that are stored inside caller tool.
|
||||||
|
///
|
||||||
|
/// Doesn't check for duplicates (although with a normal usage, there shouldn't
|
||||||
|
/// be any).
|
||||||
|
public final function array< class<AcediaObject> > GetAllItemClasses() {
|
||||||
|
local array< class<AcediaObject> > result;
|
||||||
|
local ItemCard value;
|
||||||
|
local CollectionIterator iter;
|
||||||
|
|
||||||
|
for (iter = registeredCards.Iterate(); !iter.HasFinished(); iter.Next()) {
|
||||||
|
value = ItemCard(iter.Get());
|
||||||
|
if (value != none) {
|
||||||
|
result[result.length] = value.GetItemClass();
|
||||||
|
}
|
||||||
|
_.memory.Free(value);
|
||||||
|
}
|
||||||
|
iter.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns array of names of all available items.
|
||||||
|
public final function array<Text> GetItemsNames() {
|
||||||
|
local array<Text> emptyResult;
|
||||||
|
|
||||||
|
if (registeredCards != none) {
|
||||||
|
return registeredCards.GetTextKeys();
|
||||||
|
}
|
||||||
|
return emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called each time a new card is to be created and stored.
|
||||||
|
///
|
||||||
|
/// Must be reimplemented by child classes.
|
||||||
|
protected function ItemCard MakeCard(class<AcediaObject> itemClass, BaseText itemName) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called each time a certain card is to be removed from storage.
|
||||||
|
///
|
||||||
|
/// Must be reimplemented by child classes
|
||||||
|
/// (reimplementations SHOULD NOT DEALLOCATE `toDiscard`).
|
||||||
|
protected function DiscardCard(ItemCard toDiscard) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find item card for the item that was stored with a specified
|
||||||
|
/// case-insensitive name
|
||||||
|
///
|
||||||
|
/// Function only returns `none` if item with a given name wasn't found
|
||||||
|
/// (or `none` was provided as an argument).
|
||||||
|
protected final function ItemCard GetCard(BaseText itemName) {
|
||||||
|
local Text itemKey;
|
||||||
|
local ItemCard relevantCard;
|
||||||
|
|
||||||
|
if (itemName == none) return none;
|
||||||
|
if (registeredCards == none) return none;
|
||||||
|
|
||||||
|
/// The item name is transformed into lowercase, immutable value.
|
||||||
|
/// This facilitates the use of item names as keys in a [`HashTable`],
|
||||||
|
/// enabling case-insensitive matching.
|
||||||
|
itemKey = itemName.LowerCopy();
|
||||||
|
relevantCard = ItemCard(registeredCards.GetItem(itemKey));
|
||||||
|
_.memory.Free(itemKey);
|
||||||
|
return relevantCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
errItemInvalidName = (l=LOG_Error,m="Attempt at registering item with class `%1` under an invalid name \"%2\" will be ignored.")
|
||||||
|
errItemDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Attempt at registering command `%3` with the same name will be ignored.")
|
||||||
|
}
|
||||||
1045
kf_sources/AcediaCore/Classes/Collection.uc
Normal file
1045
kf_sources/AcediaCore/Classes/Collection.uc
Normal file
File diff suppressed because it is too large
Load Diff
61
kf_sources/AcediaCore/Classes/CollectionIterator.uc
Normal file
61
kf_sources/AcediaCore/Classes/CollectionIterator.uc
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Base class for collection iterators, an auxiliary object for iterating
|
||||||
|
* through objects stored inside an Acedia's collection.
|
||||||
|
* Iterators expect that collection remains unchanged while they
|
||||||
|
* are iterating through it. Otherwise their behavior becomes undefined.
|
||||||
|
* Copyright 2020-2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CollectionIterator extends Iter
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialized caller `Iterator` to iterate over a given collection.
|
||||||
|
*
|
||||||
|
* `Iterator` should only be initialized once, reinitializing `Iterator` is
|
||||||
|
* considered an undefined behavior. Collection's `Iterate()` method is
|
||||||
|
* a preferred method to create initialized `Iterator`.
|
||||||
|
*
|
||||||
|
* Initialization is not guaranteed to be successful, - each iterator class
|
||||||
|
* corresponds to a particular collection and it's not `none` reference must
|
||||||
|
* be used as an argument.
|
||||||
|
*
|
||||||
|
* @param relevantCollection `Collection` over which items `Iterator`
|
||||||
|
* must iterate.
|
||||||
|
* @param `true` if iteration was successful and `false` otherwise.
|
||||||
|
*/
|
||||||
|
public function bool Initialize(Collection relevantCollection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns key of current value pointed to by an iterator.
|
||||||
|
*
|
||||||
|
* NOTE: this method is guaranteed to return reference to the key used in
|
||||||
|
* relevant `Collection` if it actually stores key objects inside.
|
||||||
|
* However for other `Collection`s this method may create a "replacement"
|
||||||
|
* object (like `ArrayList` creating `IntBox` to simply return integer index).
|
||||||
|
*
|
||||||
|
* Does not advance iteration: use `Next()` to pick next value.
|
||||||
|
*
|
||||||
|
* @return Key of the current value being iterated over.
|
||||||
|
* If `Iterator()` has finished iterating over all values or
|
||||||
|
* was not initialized - returns `none`.
|
||||||
|
*/
|
||||||
|
public function AcediaObject GetKey();
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
96
kf_sources/AcediaCore/Classes/CollectionsAPI.uc
Normal file
96
kf_sources/AcediaCore/Classes/CollectionsAPI.uc
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Convenience API that provides methods for quickly creating collections.
|
||||||
|
* Copyright 2020 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CollectionsAPI extends AcediaObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new `ArrayList`, optionally filling it with objects from
|
||||||
|
* a given native array.
|
||||||
|
*
|
||||||
|
* @param objectArray Objects to place inside created `ArrayList`;
|
||||||
|
* if empty (by default) - new, empty `ArrayList` will be returned.
|
||||||
|
* Objects will be added in the same order as in `objectArray`.
|
||||||
|
* @param managed Flag that indicates whether objects from
|
||||||
|
* `objectArray` argument should be added as managed.
|
||||||
|
* By default `false` - they would not be managed.
|
||||||
|
* @return New `ArrayList`, optionally filled with contents of
|
||||||
|
* `objectArray`. Guaranteed to be not `none` and to not contain any items
|
||||||
|
* outside of `objectArray`.
|
||||||
|
*/
|
||||||
|
public final function ArrayList NewArrayList(array<AcediaObject> objectArray)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local ArrayList result;
|
||||||
|
result = ArrayList(_.memory.Allocate(class'ArrayList'));
|
||||||
|
for (i = 0; i < objectArray.length; i += 1) {
|
||||||
|
result.AddItem(objectArray[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new empty `ArrayList`.
|
||||||
|
*
|
||||||
|
* @return New empty instance of `ArrayList`.
|
||||||
|
*/
|
||||||
|
public final function ArrayList EmptyArrayList()
|
||||||
|
{
|
||||||
|
return ArrayList(_.memory.Allocate(class'ArrayList'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new `HashTable`, optionally filling it with entries
|
||||||
|
* (key/value pairs) from a given native array.
|
||||||
|
*
|
||||||
|
* @param entriesArray Entries (key/value pairs) to place inside created
|
||||||
|
* `HashTable`; if empty (by default) - new,
|
||||||
|
* empty `HashTable` will be returned.
|
||||||
|
* @param managed Flag that indicates whether values from
|
||||||
|
* `entriesArray` argument should be added as managed.
|
||||||
|
* By default `false` - they would not be managed.
|
||||||
|
* @return New `HashTable`, optionally filled with contents of
|
||||||
|
* `entriesArray`. Guaranteed to be not `none` and to not contain any items
|
||||||
|
* outside of `entriesArray`.
|
||||||
|
*/
|
||||||
|
public final function HashTable NewHashTable(
|
||||||
|
array<HashTable.Entry> entriesArray)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local HashTable result;
|
||||||
|
|
||||||
|
result = HashTable(_.memory.Allocate(class'HashTable'));
|
||||||
|
for (i = 0; i < entriesArray.length; i += 1) {
|
||||||
|
result.SetItem(entriesArray[i].key, entriesArray[i].value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new empty `HashTable`.
|
||||||
|
*
|
||||||
|
* @return New empty instance of `HashTable`.
|
||||||
|
*/
|
||||||
|
public final function HashTable EmptyHashTable()
|
||||||
|
{
|
||||||
|
return HashTable(_.memory.Allocate(class'HashTable'));
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
1539
kf_sources/AcediaCore/Classes/ColorAPI.uc
Normal file
1539
kf_sources/AcediaCore/Classes/ColorAPI.uc
Normal file
File diff suppressed because it is too large
Load Diff
26
kf_sources/AcediaCore/Classes/ColorAliasSource.uc
Normal file
26
kf_sources/AcediaCore/Classes/ColorAliasSource.uc
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Source intended for color aliases.
|
||||||
|
* Copyright 2020 - 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ColorAliasSource extends AliasSource
|
||||||
|
config(AcediaAliases_Colors);
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
aliasesClass = class'ColorAliases'
|
||||||
|
}
|
||||||
28
kf_sources/AcediaCore/Classes/ColorAliases.uc
Normal file
28
kf_sources/AcediaCore/Classes/ColorAliases.uc
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Per-object-configuration intended for color aliases.
|
||||||
|
* Copyright 2020 - 2021 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ColorAliases extends AliasesStorage
|
||||||
|
perObjectConfig
|
||||||
|
config(AcediaAliases_Colors);
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configName = "AcediaAliases_Colors"
|
||||||
|
sourceClass = class'ColorAliasSource'
|
||||||
|
}
|
||||||
805
kf_sources/AcediaCore/Classes/Command.uc
Normal file
805
kf_sources/AcediaCore/Classes/Command.uc
Normal file
@ -0,0 +1,805 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Command extends AcediaObject
|
||||||
|
dependson(BaseText);
|
||||||
|
|
||||||
|
//! This class is meant to represent a command type.
|
||||||
|
//!
|
||||||
|
//! Command class provides an automated way to add a command to a server through
|
||||||
|
//! AcediaCore's features. It takes care of:
|
||||||
|
//!
|
||||||
|
//! 1. Verifying that player has passed correct (expected parameters);
|
||||||
|
//! 2. Parsing these parameters into usable values (both standard, built-in
|
||||||
|
//! types like `bool`, `int`, `float`, etc. and more advanced types such
|
||||||
|
//! as players lists and JSON values);
|
||||||
|
//! 3. Allowing you to easily specify a set of players you are targeting by
|
||||||
|
//! supporting several ways to refer to them, such as *by name*, *by id*
|
||||||
|
//! and *by selector* (@ and @self refer to caller player, @all refers
|
||||||
|
//! to all players).
|
||||||
|
//! 4. It can be registered inside AcediaCore's commands feature and be
|
||||||
|
//! automatically called through the unified system that supports *chat*
|
||||||
|
//! and *mutate* inputs (as well as allowing you to hook in any other
|
||||||
|
//! input source);
|
||||||
|
//! 5. Will also automatically provide a help page through built-in "help"
|
||||||
|
//! command;
|
||||||
|
//! 6. Subcommand support - when one command can have several distinct
|
||||||
|
//! functions, depending on how its called (e.g. "inventory add" vs
|
||||||
|
//! "inventory remove"). These subcommands have a special treatment in
|
||||||
|
//! help pages, which makes them more preferable, compared to simply
|
||||||
|
//! matching first `Text` argument;
|
||||||
|
//! 7. Add support for "options" - additional flags that can modify commands
|
||||||
|
//! behavior and behave like usual command options "--force"/"-f".
|
||||||
|
//! Their short versions can even be combined:
|
||||||
|
//! "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af".
|
||||||
|
//! And they can have their own parameters: "give@all --list sharp".
|
||||||
|
//!
|
||||||
|
//! # Implementation
|
||||||
|
//!
|
||||||
|
//! The idea of `Command`'s implementation is simple: command is basically the
|
||||||
|
//! `Command.Data` struct that is filled via `CommandDataBuilder`.
|
||||||
|
//! Whenever command is called it uses `CommandParser` to parse user's input
|
||||||
|
//! based on its `Command.Data` and either report error (in case of failure) or
|
||||||
|
//! pass make `Executed()`/`ExecutedFor()` calls (in case of success).
|
||||||
|
//!
|
||||||
|
//! When command is called is decided by `Commands_Feature` that tracks possible
|
||||||
|
//! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for
|
||||||
|
//! adding custom command inputs). That feature basically parses first part of
|
||||||
|
//! the command: its name (not the subcommand's names) and target players
|
||||||
|
//! (using `PlayersParser`, but only if command is targeted).
|
||||||
|
//!
|
||||||
|
//! Majority of the command-related code either serves to build `Command.Data`
|
||||||
|
//! or to parse command input by using it (`CommandParser`).
|
||||||
|
|
||||||
|
/// Possible errors that can arise when parsing command parameters from user
|
||||||
|
/// input
|
||||||
|
enum ErrorType {
|
||||||
|
/// No error
|
||||||
|
CET_None,
|
||||||
|
/// Bad parser was provided to parse user input (this should not be possible)
|
||||||
|
CET_BadParser,
|
||||||
|
/// Sub-command name was not specified or was incorrect
|
||||||
|
/// (this should not be possible)
|
||||||
|
CET_NoSubCommands,
|
||||||
|
/// Specified sub-command does not exist
|
||||||
|
/// (only relevant when it is enforced for parser, e.g. by an alias)
|
||||||
|
CET_BadSubCommand,
|
||||||
|
/// Required param for command / option was not specified
|
||||||
|
CET_NoRequiredParam,
|
||||||
|
CET_NoRequiredParamForOption,
|
||||||
|
/// Unknown option key was specified
|
||||||
|
CET_UnknownOption,
|
||||||
|
/// Unknown short option key was specified
|
||||||
|
CET_UnknownShortOption,
|
||||||
|
/// Same option appeared twice in one command call
|
||||||
|
CET_RepeatedOption,
|
||||||
|
/// Part of user's input could not be interpreted as a part of
|
||||||
|
/// command's call
|
||||||
|
CET_UnusedCommandParameters,
|
||||||
|
/// In one short option specification (e.g. '-lah') several options require
|
||||||
|
/// parameters: this introduces ambiguity and is not allowed
|
||||||
|
CET_MultipleOptionsWithParams,
|
||||||
|
/// Targets are specified incorrectly (for targeted commands only)
|
||||||
|
CET_IncorrectTargetList,
|
||||||
|
// No targets are specified (for targeted commands only)
|
||||||
|
CET_EmptyTargetList
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Structure that contains all the information about how `Command` was called.
|
||||||
|
struct CallData {
|
||||||
|
/// Targeted players (if applicable)
|
||||||
|
var public array<EPlayer> targetPlayers;
|
||||||
|
/// Specified sub-command and parameters/options
|
||||||
|
var public Text subCommandName;
|
||||||
|
/// Provided parameters and specified options
|
||||||
|
var public HashTable parameters;
|
||||||
|
var public HashTable options;
|
||||||
|
/// Errors that occurred during command call processing are described by
|
||||||
|
/// error type.
|
||||||
|
var public ErrorType parsingError;
|
||||||
|
/// Optional error textual name of the object (parameter, option, etc.)
|
||||||
|
/// that caused it.
|
||||||
|
var public Text errorCause;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Possible types of parameters.
|
||||||
|
enum ParameterType {
|
||||||
|
/// Parses into `BoolBox`
|
||||||
|
CPT_Boolean,
|
||||||
|
/// Parses into `IntBox`
|
||||||
|
CPT_Integer,
|
||||||
|
/// Parses into `FloatBox`
|
||||||
|
CPT_Number,
|
||||||
|
/// Parses into `Text`
|
||||||
|
CPT_Text,
|
||||||
|
/// Special parameter that consumes the rest of the input into `Text`
|
||||||
|
CPT_Remainder,
|
||||||
|
/// Parses into `HashTable`
|
||||||
|
CPT_Object,
|
||||||
|
/// Parses into `ArrayList`
|
||||||
|
CPT_Array,
|
||||||
|
/// Parses into any JSON value
|
||||||
|
CPT_JSON,
|
||||||
|
/// Parses into an array of specified players
|
||||||
|
CPT_Players
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Possible forms a boolean variable can be used as.
|
||||||
|
/// Boolean parameter can define it's preferred format, which will be used for
|
||||||
|
/// help page generation.
|
||||||
|
enum PreferredBooleanFormat {
|
||||||
|
PBF_TrueFalse,
|
||||||
|
PBF_EnableDisable,
|
||||||
|
PBF_OnOff,
|
||||||
|
PBF_YesNo
|
||||||
|
};
|
||||||
|
|
||||||
|
// Defines a singular command parameter
|
||||||
|
struct Parameter {
|
||||||
|
/// Display name (for the needs of help page displaying)
|
||||||
|
var Text displayName;
|
||||||
|
/// Type of value this parameter would store
|
||||||
|
var ParameterType type;
|
||||||
|
/// Does it take only a singular value or can it contain several of them,
|
||||||
|
/// written in a list
|
||||||
|
var bool allowsList;
|
||||||
|
/// Variable name that will be used as a key to store parameter's value
|
||||||
|
var Text variableName;
|
||||||
|
/// (For `CPT_Boolean` type variables only) - preferred boolean format,
|
||||||
|
/// used in help pages
|
||||||
|
var PreferredBooleanFormat booleanFormat;
|
||||||
|
/// `CPT_Text` can be attempted to be auto-resolved as an alias from some
|
||||||
|
/// source during parsing.
|
||||||
|
/// For command to attempt that, this field must be not-`none` and contain
|
||||||
|
/// the name of the alias source (either "weapon", "color", "feature",
|
||||||
|
/// "entity" or some kind of custom alias source name).
|
||||||
|
///
|
||||||
|
/// Only relevant when given value is prefixed with "$" character.
|
||||||
|
var Text aliasSourceName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Defines a sub-command of a this command
|
||||||
|
/// (specified as "<command> <sub_command>").
|
||||||
|
///
|
||||||
|
/// Using sub-command is not optional, but if none defined
|
||||||
|
/// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
|
||||||
|
/// one is automatically created / used.
|
||||||
|
struct SubCommand {
|
||||||
|
/// Name of the sub command. Cannot be `none`.
|
||||||
|
var Text name;
|
||||||
|
/// Human-readable description of the subcommand. Can be `none`.
|
||||||
|
var Text description;
|
||||||
|
/// List of required parameters of this [`Command`].
|
||||||
|
var array<Parameter> required;
|
||||||
|
/// List of optional parameters of this [`Command`].
|
||||||
|
var array<Parameter> optional;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Defines command's option (options are specified by "--long" or "-l").
|
||||||
|
/// Options are independent from sub-commands.
|
||||||
|
struct Option {
|
||||||
|
/// [`Option`]'s short name, i.e. a single letter "f" that can be specified
|
||||||
|
/// in, e.g. "-laf" type option listings
|
||||||
|
var BaseText.Character shortName;
|
||||||
|
/// [`Option`]'s full name, e.g. "--force".
|
||||||
|
var Text longName;
|
||||||
|
/// Human-readable description of the option. Can be `none`.
|
||||||
|
var Text description;
|
||||||
|
/// List of required parameters of this [`Command::Option`].
|
||||||
|
var array<Parameter> required;
|
||||||
|
/// List of required parameters of this [`Command::Option`].
|
||||||
|
var array<Parameter> optional;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Structure that defines what sub-commands and options command has
|
||||||
|
/// (and what parameters they take)
|
||||||
|
struct Data {
|
||||||
|
/// Command group this command belongs to
|
||||||
|
var protected Text group;
|
||||||
|
/// Short summary of what command does (recommended to
|
||||||
|
/// keep it to 80 characters)
|
||||||
|
var protected Text summary;
|
||||||
|
/// Available subcommands.
|
||||||
|
var protected array<SubCommand> subCommands;
|
||||||
|
/// Available options, common to all subcommands.
|
||||||
|
var protected array<Option> options;
|
||||||
|
/// `true` iff related [`Command`] targets players.
|
||||||
|
var protected bool requiresTarget;
|
||||||
|
};
|
||||||
|
var protected Data commandData;
|
||||||
|
|
||||||
|
/// Setting variable that defines a name that will be chosen for command by
|
||||||
|
/// default.
|
||||||
|
var protected const string preferredName;
|
||||||
|
/// Name that was used to register this command.
|
||||||
|
var protected Text usedName;
|
||||||
|
/// Settings variable that defines a class to be used for this [`Command`]'s
|
||||||
|
/// permissions config
|
||||||
|
var protected const class<CommandPermissions> permissionsConfigClass;
|
||||||
|
|
||||||
|
// We do not really ever need to create more than one instance of each class
|
||||||
|
// of `Command`, so we will simply store and reuse one created instance.
|
||||||
|
var private Command mainInstance;
|
||||||
|
|
||||||
|
/// When command is being executed we create several instances of
|
||||||
|
/// `ConsoleWriter` that can be used for command output.
|
||||||
|
/// They will also be automatically deallocated once command is executed.
|
||||||
|
///
|
||||||
|
/// DO NOT modify them or deallocate any of them manually.
|
||||||
|
///
|
||||||
|
/// This should make output more convenient and standardized.
|
||||||
|
///
|
||||||
|
/// 1. `publicConsole` - sends messages to all present players;
|
||||||
|
/// 2. `callerConsole` - sends messages to the player that called the command;
|
||||||
|
/// 3. `targetConsole` - sends messages to the player that is currently being
|
||||||
|
/// targeted (different each call of `ExecutedFor()` and `none` during
|
||||||
|
/// `Executed()` call);
|
||||||
|
/// 4. `othersConsole` - sends messaged to every player that is neither
|
||||||
|
/// "caller" or "target".
|
||||||
|
var protected ConsoleWriter publicConsole, othersConsole;
|
||||||
|
var protected ConsoleWriter callerConsole, targetConsole;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
local CommandDataBuilder dataBuilder;
|
||||||
|
|
||||||
|
if (permissionsConfigClass != none) {
|
||||||
|
permissionsConfigClass.static.Initialize();
|
||||||
|
}
|
||||||
|
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
|
||||||
|
// Let user fill-in the rest
|
||||||
|
BuildData(dataBuilder);
|
||||||
|
commandData = dataBuilder.BorrowData();
|
||||||
|
dataBuilder.FreeSelf();
|
||||||
|
dataBuilder = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
local int i;
|
||||||
|
local array<SubCommand> subCommands;
|
||||||
|
local array<Option> options;
|
||||||
|
|
||||||
|
DeallocateConsoles();
|
||||||
|
_.memory.Free(usedName);
|
||||||
|
_.memory.Free(commandData.summary);
|
||||||
|
usedName = none;
|
||||||
|
commandData.summary = none;
|
||||||
|
subCommands = commandData.subCommands;
|
||||||
|
for (i = 0; i < options.length; i += 1) {
|
||||||
|
_.memory.Free(subCommands[i].name);
|
||||||
|
_.memory.Free(subCommands[i].description);
|
||||||
|
CleanParameters(subCommands[i].required);
|
||||||
|
CleanParameters(subCommands[i].optional);
|
||||||
|
subCommands[i].required.length = 0;
|
||||||
|
subCommands[i].optional.length = 0;
|
||||||
|
}
|
||||||
|
commandData.subCommands.length = 0;
|
||||||
|
options = commandData.options;
|
||||||
|
for (i = 0; i < options.length; i += 1) {
|
||||||
|
_.memory.Free(options[i].longName);
|
||||||
|
_.memory.Free(options[i].description);
|
||||||
|
CleanParameters(options[i].required);
|
||||||
|
CleanParameters(options[i].optional);
|
||||||
|
options[i].required.length = 0;
|
||||||
|
options[i].optional.length = 0;
|
||||||
|
}
|
||||||
|
commandData.options.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes command, providing it with a specific name.
|
||||||
|
///
|
||||||
|
/// Argument cannot be `none`, otherwise initialization fails.
|
||||||
|
/// [`Command`] can only be successfully initialized once.
|
||||||
|
public final function bool Initialize(BaseText commandName) {
|
||||||
|
if (commandName == none) return false;
|
||||||
|
if (usedName != none) return false;
|
||||||
|
|
||||||
|
usedName = commandName.LowerCopy();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overload this method to use `builder` to define parameters and options for
|
||||||
|
/// your command.
|
||||||
|
protected function BuildData(CommandDataBuilder builder){}
|
||||||
|
|
||||||
|
/// Overload this method to perform required actions when your command is
|
||||||
|
/// called.
|
||||||
|
///
|
||||||
|
/// [`arguments`] is a `struct` filled with parameters that your command has
|
||||||
|
/// been called with. Guaranteed to not be in error state.
|
||||||
|
/// [`instigator`] is a player that instigated this execution.
|
||||||
|
/// [`permissions`] is a config with permissions for this command call.
|
||||||
|
protected function Executed(
|
||||||
|
CallData arguments,
|
||||||
|
EPlayer instigator,
|
||||||
|
CommandPermissions permissions) {}
|
||||||
|
|
||||||
|
/// Overload this method to perform required actions when your command is called
|
||||||
|
/// with a given player as a target.
|
||||||
|
///
|
||||||
|
/// If several players have been specified - this method will be called once
|
||||||
|
/// for each.
|
||||||
|
///
|
||||||
|
/// If your command does not require a target - this method will not be called.
|
||||||
|
///
|
||||||
|
/// [`target`] is a player that this command must perform an action on.
|
||||||
|
/// [`arguments`] is a `struct` filled with parameters that your command has
|
||||||
|
/// been called with. Guaranteed to not be in error state.
|
||||||
|
/// [`instigator`] is a player that instigated this execution.
|
||||||
|
/// [`permissions`] is a config with permissions for this command call.
|
||||||
|
protected function ExecutedFor(
|
||||||
|
EPlayer target,
|
||||||
|
CallData arguments,
|
||||||
|
EPlayer instigator,
|
||||||
|
CommandPermissions permissions) {}
|
||||||
|
|
||||||
|
/// Returns an instance of command (of particular class) that is stored
|
||||||
|
/// "as a singleton" in command's class itself. Do not deallocate it.
|
||||||
|
public final static function Command GetInstance() {
|
||||||
|
if (default.mainInstance == none) {
|
||||||
|
default.mainInstance = Command(__().memory.Allocate(default.class));
|
||||||
|
}
|
||||||
|
return default.mainInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forces command to process (parse) player's input, producing a structure with
|
||||||
|
/// parsed data in Acedia's format instead.
|
||||||
|
///
|
||||||
|
/// Use `Execute()` for actually performing command's actions.
|
||||||
|
///
|
||||||
|
/// [`subCommandName`] can be optionally specified to use as sub-command.
|
||||||
|
/// If this argument's value is `none` - sub-command name will be parsed from
|
||||||
|
/// the `parser`'s data.
|
||||||
|
///
|
||||||
|
/// Returns `CallData` structure that contains all the information about
|
||||||
|
/// parameters specified in `parser`'s contents.
|
||||||
|
/// Returned structure contains objects that must be deallocated, which can
|
||||||
|
/// easily be done by the auxiliary `DeallocateCallData()` method.
|
||||||
|
public final function CallData ParseInputWith(
|
||||||
|
Parser parser,
|
||||||
|
EPlayer callerPlayer,
|
||||||
|
optional BaseText subCommandName
|
||||||
|
) {
|
||||||
|
local array<EPlayer> targetPlayers;
|
||||||
|
local CommandParser commandParser;
|
||||||
|
local CallData callData;
|
||||||
|
|
||||||
|
if (parser == none || !parser.Ok()) {
|
||||||
|
callData.parsingError = CET_BadParser;
|
||||||
|
return callData;
|
||||||
|
}
|
||||||
|
// Parse targets and handle errors that can arise here
|
||||||
|
if (commandData.requiresTarget) {
|
||||||
|
targetPlayers = ParseTargets(parser, callerPlayer);
|
||||||
|
if (!parser.Ok()) {
|
||||||
|
callData.parsingError = CET_IncorrectTargetList;
|
||||||
|
return callData;
|
||||||
|
}
|
||||||
|
if (targetPlayers.length <= 0) {
|
||||||
|
callData.parsingError = CET_EmptyTargetList;
|
||||||
|
return callData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Parse parameters themselves
|
||||||
|
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
|
||||||
|
callData = commandParser.ParseWith(
|
||||||
|
parser,
|
||||||
|
commandData,
|
||||||
|
callerPlayer,
|
||||||
|
subCommandName);
|
||||||
|
callData.targetPlayers = targetPlayers;
|
||||||
|
commandParser.FreeSelf();
|
||||||
|
return callData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes caller `Command` with data provided by `callData` if it is in
|
||||||
|
/// a correct state and reports error to `callerPlayer` if `callData` is
|
||||||
|
/// invalid.
|
||||||
|
///
|
||||||
|
/// Returns `true` if command was successfully executed and `false` otherwise.
|
||||||
|
/// Execution is considered successful if `Execute()` call was made, regardless
|
||||||
|
/// of whether `Command` can actually perform required action.
|
||||||
|
/// For example, giving a weapon to a player can fail because he does not have
|
||||||
|
/// enough space in his inventory, but it will still be considered a successful
|
||||||
|
/// execution as far as return value is concerned.
|
||||||
|
///
|
||||||
|
/// [`permissions`] argument is supposed to specify permissions with which this
|
||||||
|
/// command runs.
|
||||||
|
/// If [`permissionsConfigClass`] is `none`, it must always be `none`.
|
||||||
|
/// If [`permissionsConfigClass`] is not `none`, then [`permissions`] argument
|
||||||
|
/// being `none` should mean running with minimal priviledges.
|
||||||
|
public final function bool Execute(
|
||||||
|
CallData callData,
|
||||||
|
EPlayer callerPlayer,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
local array<EPlayer> targetPlayers;
|
||||||
|
|
||||||
|
if (callerPlayer == none) return false;
|
||||||
|
if (!callerPlayer.IsExistent()) return false;
|
||||||
|
|
||||||
|
// Report or execute
|
||||||
|
if (callData.parsingError != CET_None) {
|
||||||
|
ReportError(callData, callerPlayer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
targetPlayers = callData.targetPlayers;
|
||||||
|
publicConsole = _.console.ForAll();
|
||||||
|
callerConsole = _.console.For(callerPlayer);
|
||||||
|
callerConsole
|
||||||
|
.Write(P("Executing command `"))
|
||||||
|
.Write(usedName)
|
||||||
|
.Say(P("`"));
|
||||||
|
// `othersConsole` should also exist in time for `Executed()` call
|
||||||
|
othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
|
||||||
|
Executed(callData, callerPlayer, permissions);
|
||||||
|
_.memory.Free(othersConsole);
|
||||||
|
if (commandData.requiresTarget) {
|
||||||
|
for (i = 0; i < targetPlayers.length; i += 1) {
|
||||||
|
targetConsole = _.console.For(targetPlayers[i]);
|
||||||
|
othersConsole = _.console
|
||||||
|
.ForAll()
|
||||||
|
.ButPlayer(callerPlayer)
|
||||||
|
.ButPlayer(targetPlayers[i]);
|
||||||
|
ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions);
|
||||||
|
_.memory.Free(othersConsole);
|
||||||
|
_.memory.Free(targetConsole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
othersConsole = none;
|
||||||
|
targetConsole = none;
|
||||||
|
DeallocateConsoles();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auxiliary method that cleans up all data and deallocates all objects inside provided structure.
|
||||||
|
public final static function DeallocateCallData(/* take */ CallData callData) {
|
||||||
|
__().memory.Free(callData.subCommandName);
|
||||||
|
__().memory.Free(callData.parameters);
|
||||||
|
__().memory.Free(callData.options);
|
||||||
|
__().memory.Free(callData.errorCause);
|
||||||
|
__().memory.FreeMany(callData.targetPlayers);
|
||||||
|
if (callData.targetPlayers.length > 0) {
|
||||||
|
callData.targetPlayers.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function CleanParameters(array<Parameter> parameters) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < parameters.length; i += 1) {
|
||||||
|
_.memory.Free(parameters[i].displayName);
|
||||||
|
_.memory.Free(parameters[i].variableName);
|
||||||
|
_.memory.Free(parameters[i].aliasSourceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns name (in lower case) of the caller command class.
|
||||||
|
public final static function Text GetPreferredName() {
|
||||||
|
return __().text.FromString(Locs(default.preferredName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns name (in lower case) of the caller command class.
|
||||||
|
public final static function string GetPreferredName_S() {
|
||||||
|
return Locs(default.preferredName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns name (in lower case) of the caller command class.
|
||||||
|
public final function Text GetName() {
|
||||||
|
if (usedName == none) {
|
||||||
|
return P("").Copy();
|
||||||
|
}
|
||||||
|
return usedName.LowerCopy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns name (in lower case) of the caller command class.
|
||||||
|
public final function string GetName_S() {
|
||||||
|
if (usedName == none) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return _.text.IntoString(/*take*/ usedName.LowerCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns group name (in lower case) of the caller command class.
|
||||||
|
public final function Text GetGroupName() {
|
||||||
|
if (commandData.group == none) {
|
||||||
|
return P("").Copy();
|
||||||
|
}
|
||||||
|
return commandData.group.LowerCopy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns group name (in lower case) of the caller command class.
|
||||||
|
public final function string GetGroupName_S() {
|
||||||
|
if (commandData.group == none) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return _.text.IntoString(/*take*/ commandData.group.LowerCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads permissions config with a given name for the caller [`Command`] class.
|
||||||
|
///
|
||||||
|
/// Permission configs describe allowed usage of the [`Command`].
|
||||||
|
/// Basic settings are contained inside [`CommandPermissions`], but commands
|
||||||
|
/// should derive their own child classes for storing their settings.
|
||||||
|
///
|
||||||
|
/// Returns `none` if caller [`Command`] class didn't specify custom permission
|
||||||
|
/// settings class or provided name is invalid (according to
|
||||||
|
/// [`BaseText::IsValidName()`]).
|
||||||
|
/// Otherwise guaranteed to return a config reference.
|
||||||
|
public final static function CommandPermissions LoadConfig(BaseText configName) {
|
||||||
|
if (configName == none) return none;
|
||||||
|
if (default.permissionsConfigClass == none) return none;
|
||||||
|
|
||||||
|
// This creates default config if it is missing
|
||||||
|
default.permissionsConfigClass.static.NewConfig(configName);
|
||||||
|
return CommandPermissions(default.permissionsConfigClass.static
|
||||||
|
.GetConfigInstance(configName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads permissions config with a given name for the caller [`Command`] class.
|
||||||
|
///
|
||||||
|
/// Permission configs describe allowed usage of the [`Command`].
|
||||||
|
/// Basic settings are contained inside [`CommandPermissions`], but commands
|
||||||
|
/// should derive their own child classes for storing their settings.
|
||||||
|
///
|
||||||
|
/// Returns `none` if caller [`Command`] class didn't specify custom permission
|
||||||
|
/// settings class or provided name is invalid (according to
|
||||||
|
/// [`BaseText::IsValidName()`]).
|
||||||
|
/// Otherwise guaranteed to return a config reference.
|
||||||
|
public final static function CommandPermissions LoadConfig_S(string configName) {
|
||||||
|
local MutableText wrapper;
|
||||||
|
local CommandPermissions result;
|
||||||
|
|
||||||
|
wrapper = __().text.FromStringM(configName);
|
||||||
|
result = LoadConfig(wrapper);
|
||||||
|
__().memory.Free(wrapper);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns subcommands of caller [`Command`] according to the provided
|
||||||
|
/// permissions.
|
||||||
|
///
|
||||||
|
/// If provided `none` as permissions, returns all available sub commands.
|
||||||
|
public final function array<Text> GetSubCommands(optional CommandPermissions permissions) {
|
||||||
|
local int i, j;
|
||||||
|
local bool addSubCommand;
|
||||||
|
local array<string> forbiddenCommands;
|
||||||
|
local array<Text> result;
|
||||||
|
|
||||||
|
forbiddenCommands = permissions.forbiddenSubCommands;
|
||||||
|
if (permissions != none) {
|
||||||
|
forbiddenCommands = permissions.forbiddenSubCommands;
|
||||||
|
}
|
||||||
|
for (i = 0; i < commandData.subCommands.length; i += 1) {
|
||||||
|
addSubCommand = true;
|
||||||
|
for (j = 0; j < forbiddenCommands.length; j += 1) {
|
||||||
|
if (commandData.subCommands[i].name.ToString() ~= forbiddenCommands[j]) {
|
||||||
|
addSubCommand = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addSubCommand) {
|
||||||
|
result[result.length] = commandData.subCommands[i].name.LowerCopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns sub commands of caller [`Command`] according to the provided
|
||||||
|
/// permissions.
|
||||||
|
///
|
||||||
|
/// If provided `none` as permissions, returns all available sub commands.
|
||||||
|
public final function array<string> GetSubCommands_S(optional CommandPermissions permissions) {
|
||||||
|
return _.text.IntoStrings(GetSubCommands(permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether a given sub command (case insensitive) is allowed to be
|
||||||
|
/// executed with given permissions.
|
||||||
|
///
|
||||||
|
/// If `none` is passed as either argument, returns `true`.
|
||||||
|
///
|
||||||
|
/// Doesn't check for the existence of sub command, only that permissions do not
|
||||||
|
/// explicitly forbid it.
|
||||||
|
/// In case non-existing subcommand is passed as an argument, the result
|
||||||
|
/// should be considered undefined.
|
||||||
|
public final function bool IsSubCommandAllowed(
|
||||||
|
BaseText subCommand,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
if (subCommand == none) return true;
|
||||||
|
if (permissions == none) return true;
|
||||||
|
|
||||||
|
return IsSubCommandAllowed_S(subCommand.ToString(), permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether a given sub command (case insensitive) is allowed to be
|
||||||
|
/// executed with given permissions.
|
||||||
|
///
|
||||||
|
/// If `none` is passed for permissions, always returns `true`.
|
||||||
|
///
|
||||||
|
/// Doesn't check for the existence of sub command, only that permissions do not
|
||||||
|
/// explicitly forbid it.
|
||||||
|
/// In case non-existing sub command is passed as an argument, the result
|
||||||
|
/// should be considered undefined.
|
||||||
|
public final function bool IsSubCommandAllowed_S(
|
||||||
|
string subCommand,
|
||||||
|
CommandPermissions permissions
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
local array<string> forbiddenCommands;
|
||||||
|
|
||||||
|
if (permissions == none) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
forbiddenCommands = permissions.forbiddenSubCommands;
|
||||||
|
for (i = 0; i < forbiddenCommands.length; i += 1) {
|
||||||
|
if (subCommand ~= forbiddenCommands[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Command.Data` struct that describes caller `Command`.
|
||||||
|
///
|
||||||
|
/// Returned struct contains `Text` references that are used internally by
|
||||||
|
/// the `Command` and not their copies.
|
||||||
|
///
|
||||||
|
/// Generally this is undesired approach and leaves `Command` more vulnerable to
|
||||||
|
/// modification, but copying all the data inside would not only introduce
|
||||||
|
/// a largely pointless computational overhead, but also would require some
|
||||||
|
/// cumbersome logic.
|
||||||
|
/// This might change in the future, so deallocating any objects in the returned
|
||||||
|
/// `struct` would lead to undefined behavior.
|
||||||
|
public final function Data BorrowData() {
|
||||||
|
return commandData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function DeallocateConsoles() {
|
||||||
|
if (publicConsole != none && publicConsole.IsAllocated()) {
|
||||||
|
_.memory.Free(publicConsole);
|
||||||
|
}
|
||||||
|
if (callerConsole != none && callerConsole.IsAllocated()) {
|
||||||
|
_.memory.Free(callerConsole);
|
||||||
|
}
|
||||||
|
if (targetConsole != none && targetConsole.IsAllocated()) {
|
||||||
|
_.memory.Free(targetConsole);
|
||||||
|
}
|
||||||
|
if (othersConsole != none && othersConsole.IsAllocated()) {
|
||||||
|
_.memory.Free(othersConsole);
|
||||||
|
}
|
||||||
|
publicConsole = none;
|
||||||
|
callerConsole = none;
|
||||||
|
targetConsole = none;
|
||||||
|
othersConsole = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports given error to the `callerPlayer`, appropriately picking
|
||||||
|
/// message color
|
||||||
|
private final function ReportError(CallData callData, EPlayer callerPlayer) {
|
||||||
|
local Text errorMessage;
|
||||||
|
local ConsoleWriter console;
|
||||||
|
|
||||||
|
if (callerPlayer == none) return;
|
||||||
|
if (!callerPlayer.IsExistent()) return;
|
||||||
|
|
||||||
|
// Setup console color
|
||||||
|
console = callerPlayer.BorrowConsole();
|
||||||
|
if (callData.parsingError == CET_EmptyTargetList) {
|
||||||
|
console.UseColor(_.color.textWarning);
|
||||||
|
} else {
|
||||||
|
console.UseColor(_.color.textFailure);
|
||||||
|
}
|
||||||
|
// Send message
|
||||||
|
errorMessage = PrintErrorMessage(callData);
|
||||||
|
console.Say(errorMessage);
|
||||||
|
errorMessage.FreeSelf();
|
||||||
|
// Restore console color
|
||||||
|
console.ResetColor().Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function Text PrintErrorMessage(CallData callData) {
|
||||||
|
local Text result;
|
||||||
|
local MutableText builder;
|
||||||
|
|
||||||
|
builder = _.text.Empty();
|
||||||
|
switch (callData.parsingError) {
|
||||||
|
case CET_BadParser:
|
||||||
|
builder.Append(P("Internal error occurred: invalid parser"));
|
||||||
|
break;
|
||||||
|
case CET_NoSubCommands:
|
||||||
|
builder.Append(P("Ill defined command: no subcommands"));
|
||||||
|
break;
|
||||||
|
case CET_BadSubCommand:
|
||||||
|
builder
|
||||||
|
.Append(P("Ill defined sub-command: "))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_NoRequiredParam:
|
||||||
|
builder
|
||||||
|
.Append(P("Missing required parameter: "))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_NoRequiredParamForOption:
|
||||||
|
builder
|
||||||
|
.Append(P("Missing required parameter for option: "))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_UnknownOption:
|
||||||
|
builder
|
||||||
|
.Append(P("Invalid option specified: "))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_UnknownShortOption:
|
||||||
|
builder.Append(P("Invalid short option specified"));
|
||||||
|
break;
|
||||||
|
case CET_RepeatedOption:
|
||||||
|
builder
|
||||||
|
.Append(P("Option specified several times: "))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_UnusedCommandParameters:
|
||||||
|
builder.Append(P("Part of command could not be parsed: "))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_MultipleOptionsWithParams:
|
||||||
|
builder
|
||||||
|
.Append(P("Multiple short options in one declarations require parameters: "))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_IncorrectTargetList:
|
||||||
|
builder
|
||||||
|
.Append(P("Target players are incorrectly specified."))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
case CET_EmptyTargetList:
|
||||||
|
builder
|
||||||
|
.Append(P("List of target players is empty"))
|
||||||
|
.Append(callData.errorCause);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
result = builder.Copy();
|
||||||
|
builder.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auxiliary method for parsing list of targeted players.
|
||||||
|
// Assumes given parser is not `none` and not in a failed state.
|
||||||
|
// If parsing failed, guaranteed to return an empty array.
|
||||||
|
private final function array<EPlayer> ParseTargets(Parser parser, EPlayer callerPlayer) {
|
||||||
|
local array<EPlayer> targetPlayers;
|
||||||
|
local PlayersParser targetsParser;
|
||||||
|
|
||||||
|
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
|
||||||
|
targetsParser.SetSelf(callerPlayer);
|
||||||
|
targetsParser.ParseWith(parser);
|
||||||
|
if (parser.Ok()) {
|
||||||
|
targetPlayers = targetsParser.GetPlayers();
|
||||||
|
}
|
||||||
|
targetsParser.FreeSelf();
|
||||||
|
return targetPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
preferredName = ""
|
||||||
|
permissionsConfigClass = none
|
||||||
|
}
|
||||||
1578
kf_sources/AcediaCore/Classes/CommandAPI.uc
Normal file
1578
kf_sources/AcediaCore/Classes/CommandAPI.uc
Normal file
File diff suppressed because it is too large
Load Diff
26
kf_sources/AcediaCore/Classes/CommandAliasSource.uc
Normal file
26
kf_sources/AcediaCore/Classes/CommandAliasSource.uc
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Source intended for command aliases.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandAliasSource extends AliasSource
|
||||||
|
config(AcediaAliases_Commands);
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
aliasesClass = class'CommandAliases'
|
||||||
|
}
|
||||||
28
kf_sources/AcediaCore/Classes/CommandAliases.uc
Normal file
28
kf_sources/AcediaCore/Classes/CommandAliases.uc
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Per-object-configuration intended for command aliases.
|
||||||
|
* Copyright 2022 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandAliases extends AliasesStorage
|
||||||
|
perObjectConfig
|
||||||
|
config(AcediaAliases_Commands);
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
configName = "AcediaAliases_Commands"
|
||||||
|
sourceClass = class'CommandAliasSource'
|
||||||
|
}
|
||||||
939
kf_sources/AcediaCore/Classes/CommandDataBuilder.uc
Normal file
939
kf_sources/AcediaCore/Classes/CommandDataBuilder.uc
Normal file
@ -0,0 +1,939 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandDataBuilder extends AcediaObject
|
||||||
|
dependson(Command);
|
||||||
|
|
||||||
|
//! This is an auxiliary class for convenient creation of [`Command::Data`]
|
||||||
|
//! using a builder pattern.
|
||||||
|
//!
|
||||||
|
//! ## Implementation
|
||||||
|
//!
|
||||||
|
//! We will store all defined data in two ways:
|
||||||
|
//!
|
||||||
|
//! 1. Selected data: data about parameters for subcommand/option that is
|
||||||
|
//! currently being filled;
|
||||||
|
//! 2. Prepared data: data that was already filled as "selected data" then
|
||||||
|
//! stored in these records. Whenever we want to switch to filling another
|
||||||
|
//! subcommand/option or return already prepared data we must dump
|
||||||
|
//! "selected data" into "prepared data" first and then return the latter.
|
||||||
|
//!
|
||||||
|
//! Builder object is automatically created when new `Command` instance is
|
||||||
|
//! allocated and doesn't normally need to be allocated by hand.
|
||||||
|
|
||||||
|
// "Prepared data"
|
||||||
|
var private Text commandName, commandGroup;
|
||||||
|
var private Text commandSummary;
|
||||||
|
var private array<Command.SubCommand> subcommands;
|
||||||
|
var private array<Command.Option> options;
|
||||||
|
var private bool requiresTarget;
|
||||||
|
|
||||||
|
// Auxiliary arrays signifying that we've started adding optional parameters
|
||||||
|
// into appropriate `subcommands` and `options`.
|
||||||
|
//
|
||||||
|
// All optional parameters must follow strictly after required parameters and
|
||||||
|
// so, after user have started adding optional parameters to subcommand/option,
|
||||||
|
// we prevent them from adding required ones (to that particular
|
||||||
|
// command/option).
|
||||||
|
var private array<byte> subcommandsIsOptional;
|
||||||
|
var private array<byte> optionsIsOptional;
|
||||||
|
|
||||||
|
// "Selected data"
|
||||||
|
// `false` means we have selected sub-command, `true` - option
|
||||||
|
var private bool selectedItemIsOption;
|
||||||
|
// `name` for sub-commands, `longName` for options
|
||||||
|
var private Text selectedItemName;
|
||||||
|
// Description of selected sub-command/option
|
||||||
|
var private Text selectedDescription;
|
||||||
|
// Are we filling optional parameters (`true`)? Or required ones (`false`)?
|
||||||
|
var private bool selectionIsOptional;
|
||||||
|
// Array of parameters we are currently filling (either required or optional)
|
||||||
|
var private array<Command.Parameter> selectedParameterArray;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong;
|
||||||
|
var private LoggerAPI.Definition warnSameLongName, warnSameShortName;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
// Fill empty subcommand (no special key word) by default
|
||||||
|
SubCommand(P(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
subcommands.length = 0;
|
||||||
|
subcommandsIsOptional.length = 0;
|
||||||
|
options.length = 0;
|
||||||
|
optionsIsOptional.length = 0;
|
||||||
|
selectedParameterArray.length = 0;
|
||||||
|
commandName = none;
|
||||||
|
commandGroup = none;
|
||||||
|
commandSummary = none;
|
||||||
|
selectedItemName = none;
|
||||||
|
selectedDescription = none;
|
||||||
|
requiresTarget = false;
|
||||||
|
selectedItemIsOption = false;
|
||||||
|
selectionIsOptional = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method that starts defining a new sub-command.
|
||||||
|
///
|
||||||
|
/// Creates new sub-command with a given name (if it's missing) and then selects
|
||||||
|
/// sub-command with a given name to add parameters to.
|
||||||
|
///
|
||||||
|
/// [`name`] defines name of the sub-command user wants, case-sensitive.
|
||||||
|
/// If `none` is passed, this method will do nothing.
|
||||||
|
public final function SubCommand(BaseText name) {
|
||||||
|
local int subcommandIndex;
|
||||||
|
|
||||||
|
if (name == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RecordSelection();
|
||||||
|
subcommandIndex = FindSubCommandIndex(name);
|
||||||
|
if (subcommandIndex < 0) {
|
||||||
|
MakeEmptySelection(name, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Load appropriate prepared data, if it exists for
|
||||||
|
// sub-command with name `name`
|
||||||
|
selectedItemIsOption = false;
|
||||||
|
selectedItemName = subcommands[subcommandIndex].name;
|
||||||
|
selectedDescription = subcommands[subcommandIndex].description;
|
||||||
|
selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0;
|
||||||
|
if (selectionIsOptional) {
|
||||||
|
selectedParameterArray = subcommands[subcommandIndex].optional;
|
||||||
|
} else {
|
||||||
|
selectedParameterArray = subcommands[subcommandIndex].required;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method that starts defining a new option.
|
||||||
|
///
|
||||||
|
/// This method checks if some of the recorded options are in conflict with
|
||||||
|
/// given `longName` and `shortName` (already using one and only one of them).
|
||||||
|
/// In case there is no conflict, it creates new option with specified long and
|
||||||
|
/// short names (if such option is missing) and selects option with a long name
|
||||||
|
/// `longName` to add parameters to.
|
||||||
|
///
|
||||||
|
/// [`longName`] defines long name of the option, case-sensitive (for using
|
||||||
|
/// an option in form "--..."). Must be at least two characters long.
|
||||||
|
/// [`shortName`] defines short name of the option, case-sensitive (for using
|
||||||
|
/// an option in form "-..."). Must be exactly one character.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Errors will be logged in case either of arguments are `none`, have
|
||||||
|
/// inappropriate length or are in conflict with each other.
|
||||||
|
public final function Option(BaseText longName, optional BaseText shortName) {
|
||||||
|
local int optionIndex;
|
||||||
|
local BaseText.Character shortNameAsCharacter;
|
||||||
|
|
||||||
|
// Unlike for `SubCommand()`, we need to ensure that option naming is
|
||||||
|
// correct and does not conflict with existing options
|
||||||
|
// (user might attempt to add two options with same long names and
|
||||||
|
// different short ones).
|
||||||
|
shortNameAsCharacter = GetValidShortName(longName, shortName);
|
||||||
|
if ( !_.text.IsValidCharacter(shortNameAsCharacter)
|
||||||
|
|| VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) {
|
||||||
|
// ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()`
|
||||||
|
// are responsible for logging warnings/errors
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SelectOption(longName);
|
||||||
|
// Set short name for new options
|
||||||
|
optionIndex = FindOptionIndex(longName);
|
||||||
|
if (optionIndex < 0) {
|
||||||
|
// We can only be here if option was created for the first time
|
||||||
|
RecordSelection();
|
||||||
|
// So now it cannot fail
|
||||||
|
optionIndex = FindOptionIndex(longName);
|
||||||
|
options[optionIndex].shortName = shortNameAsCharacter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds description to the selected sub-command / option.
|
||||||
|
///
|
||||||
|
/// Highlights parts of the description in-between "`" characters.
|
||||||
|
///
|
||||||
|
/// Does nothing if nothing is yet selected.
|
||||||
|
public final function Describe(BaseText description) {
|
||||||
|
local int fromIndex, toIndex;
|
||||||
|
local BaseText.Formatting keyWordFormatting;
|
||||||
|
local bool lookingForEnd;
|
||||||
|
local MutableText coloredDescription;
|
||||||
|
|
||||||
|
if (description == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keyWordFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
|
||||||
|
coloredDescription = description.MutableCopy();
|
||||||
|
while (true) {
|
||||||
|
if (lookingForEnd) {
|
||||||
|
toIndex = coloredDescription.IndexOf(P("`"), fromIndex + 1);
|
||||||
|
} else {
|
||||||
|
fromIndex = coloredDescription.IndexOf(P("`"), toIndex + 1);
|
||||||
|
}
|
||||||
|
if (toIndex < 0 || fromIndex < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (lookingForEnd) {
|
||||||
|
coloredDescription.ChangeFormatting(
|
||||||
|
keyWordFormatting,
|
||||||
|
fromIndex,
|
||||||
|
toIndex - fromIndex + 1);
|
||||||
|
lookingForEnd = false;
|
||||||
|
} else {
|
||||||
|
lookingForEnd = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coloredDescription.Replace(P("`"), P(""));
|
||||||
|
if (lookingForEnd) {
|
||||||
|
coloredDescription.ChangeFormatting(keyWordFormatting, fromIndex);
|
||||||
|
}
|
||||||
|
_.memory.Free(selectedDescription);
|
||||||
|
selectedDescription = coloredDescription.IntoText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets new group of `Command.Data` under construction.
|
||||||
|
///
|
||||||
|
/// Group name is meant to be shared among several commands, allowing user to
|
||||||
|
/// filter or fetch commands of a certain group.
|
||||||
|
/// Group name is case-insensitive.
|
||||||
|
public final function Group(BaseText newName) {
|
||||||
|
if (newName != none && newName == commandGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_.memory.Free(commandGroup);
|
||||||
|
if (newName != none) {
|
||||||
|
commandGroup = newName.Copy();
|
||||||
|
} else {
|
||||||
|
commandGroup = none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets new summary of `Command.Data` under construction.
|
||||||
|
///
|
||||||
|
/// Summary gives a short description of the command on the whole that will
|
||||||
|
/// be displayed when "help" command is listing available command
|
||||||
|
public final function Summary(BaseText newSummary) {
|
||||||
|
if (newSummary != none && newSummary == commandSummary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_.memory.Free(commandSummary);
|
||||||
|
if (newSummary != none) {
|
||||||
|
commandSummary = newSummary.Copy();
|
||||||
|
} else {
|
||||||
|
commandSummary = none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes caller builder to mark `Command.Data` under construction to require
|
||||||
|
/// a player target.
|
||||||
|
public final function RequireTarget() {
|
||||||
|
requiresTarget = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Any parameters added to currently selected sub-command / option after
|
||||||
|
/// calling this method will be marked as optional.
|
||||||
|
///
|
||||||
|
/// Further calls when the same sub-command / option is selected will do
|
||||||
|
/// nothing.
|
||||||
|
public final function OptionalParams()
|
||||||
|
{
|
||||||
|
if (selectionIsOptional) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Record all required parameters first, otherwise there would be no way
|
||||||
|
// to distinguish between them and optional parameters
|
||||||
|
RecordSelection();
|
||||||
|
selectionIsOptional = true;
|
||||||
|
selectedParameterArray.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns data that has been constructed so far by the caller
|
||||||
|
/// [`CommandDataBuilder`].
|
||||||
|
///
|
||||||
|
/// Does not reset progress.
|
||||||
|
public final function Command.Data BorrowData() {
|
||||||
|
local Command.Data newData;
|
||||||
|
|
||||||
|
// TODO: is this copying needed?
|
||||||
|
RecordSelection();
|
||||||
|
newData.group = commandGroup;
|
||||||
|
newData.summary = commandSummary;
|
||||||
|
newData.subcommands = subcommands;
|
||||||
|
newData.options = options;
|
||||||
|
newData.requiresTarget = requiresTarget;
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds new parameter to selected sub-command / option
|
||||||
|
private final function PushParameter(Command.Parameter newParameter) {
|
||||||
|
selectedParameterArray[selectedParameterArray.length] = newParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills `Command.ParameterType` struct with given values
|
||||||
|
// (except boolean format).
|
||||||
|
// Assumes `displayName != none`.
|
||||||
|
private final function Command.Parameter NewParameter(
|
||||||
|
BaseText displayName,
|
||||||
|
Command.ParameterType parameterType,
|
||||||
|
bool isListParameter,
|
||||||
|
optional BaseText variableName
|
||||||
|
) {
|
||||||
|
local Command.Parameter newParameter;
|
||||||
|
|
||||||
|
newParameter.displayName = displayName.Copy();
|
||||||
|
newParameter.type = parameterType;
|
||||||
|
newParameter.allowsList = isListParameter;
|
||||||
|
if (variableName != none) {
|
||||||
|
newParameter.variableName = variableName.Copy();
|
||||||
|
} else {
|
||||||
|
newParameter.variableName = displayName.Copy();
|
||||||
|
}
|
||||||
|
return newParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new boolean parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`format`] defines preferred format of boolean values.
|
||||||
|
/// Command parser will still accept boolean values in any form, this setting
|
||||||
|
/// only affects how parameter will be displayed in generated help.
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command
|
||||||
|
/// input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamBoolean(
|
||||||
|
BaseText name,
|
||||||
|
optional Command.PreferredBooleanFormat format,
|
||||||
|
optional BaseText variableName
|
||||||
|
) {
|
||||||
|
local Command.Parameter newParam;
|
||||||
|
|
||||||
|
if (name != none) {
|
||||||
|
newParam = NewParameter(name, CPT_Boolean, false, variableName);
|
||||||
|
newParam.booleanFormat = format;
|
||||||
|
PushParameter(newParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new integer list parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`format`] defines preferred format of boolean values.
|
||||||
|
/// Command parser will still accept boolean values in any form, this setting
|
||||||
|
/// only affects how
|
||||||
|
/// parameter will be displayed in generated help.
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamBooleanList(
|
||||||
|
BaseText name,
|
||||||
|
optional Command.PreferredBooleanFormat format,
|
||||||
|
optional BaseText variableName
|
||||||
|
) {
|
||||||
|
local Command.Parameter newParam;
|
||||||
|
|
||||||
|
if (name != none) {
|
||||||
|
newParam = NewParameter(name, CPT_Boolean, true, variableName);
|
||||||
|
newParam.booleanFormat = format;
|
||||||
|
PushParameter(newParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new integer parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamInteger(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Integer, false, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new integer list parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter (it would appear in
|
||||||
|
/// the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamIntegerList(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Integer, true, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new numeric parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter (it would appear in the
|
||||||
|
/// generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamNumber(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Number, false, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new numeric list parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter (it would appear in the
|
||||||
|
/// generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamNumberList(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Number, true, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new text parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter (it would appear in the
|
||||||
|
/// generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
///
|
||||||
|
/// [`aliasSourceName`] defines name of the alias source that must be used to
|
||||||
|
/// auto-resolve this parameter's value. `none` means that parameter will be
|
||||||
|
/// recorded as-is, any other value (either "weapon", "color", "feature",
|
||||||
|
/// "entity" or some kind of custom alias source name) will make values prefixed
|
||||||
|
/// with "$" to be resolved as custom aliases.
|
||||||
|
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
|
||||||
|
/// two fields: "alias" - value provided by user and (in case "$" prefix was
|
||||||
|
/// used) "value" - actual resolved value of an alias.
|
||||||
|
/// If alias has failed to be resolved, `none` will be stored as a value.
|
||||||
|
public final function ParamText(
|
||||||
|
BaseText name,
|
||||||
|
optional BaseText variableName,
|
||||||
|
optional BaseText aliasSourceName
|
||||||
|
) {
|
||||||
|
local Command.Parameter newParameterValue;
|
||||||
|
|
||||||
|
if (name == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newParameterValue = NewParameter(name, CPT_Text, false, variableName);
|
||||||
|
if (aliasSourceName != none) {
|
||||||
|
newParameterValue.aliasSourceName = aliasSourceName.Copy();
|
||||||
|
}
|
||||||
|
PushParameter(newParameterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new text list parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter (it would appear in the
|
||||||
|
/// generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed. If left `none`, - will coincide with
|
||||||
|
/// `name` parameter.
|
||||||
|
///
|
||||||
|
/// [`aliasSourceName`] defines name of the alias source that must be used to
|
||||||
|
/// auto-resolve this parameter's value. `none` means that parameter will be
|
||||||
|
/// recorded as-is, any other value (either "weapon", "color", "feature",
|
||||||
|
/// "entity" or some kind of custom alias source name) will make values prefixed
|
||||||
|
/// with "$" to be resolved as custom aliases.
|
||||||
|
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
|
||||||
|
/// two fields: "alias" - value provided by user and (in case "$" prefix was
|
||||||
|
/// used) "value" - actual resolved value of an alias.
|
||||||
|
/// If alias has failed to be resolved, `none` will be stored as a value.
|
||||||
|
public final function ParamTextList(
|
||||||
|
BaseText name,
|
||||||
|
optional BaseText variableName,
|
||||||
|
optional BaseText aliasSourceName
|
||||||
|
) {
|
||||||
|
local Command.Parameter newParameterValue;
|
||||||
|
|
||||||
|
if (name == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newParameterValue = NewParameter(name, CPT_Text, true, variableName);
|
||||||
|
if (aliasSourceName != none) {
|
||||||
|
newParameterValue.aliasSourceName = aliasSourceName.Copy();
|
||||||
|
}
|
||||||
|
PushParameter(newParameterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new remainder parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// Remainder parameter is a special parameter that will simply consume all
|
||||||
|
/// remaining command's input as-is.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamRemainder(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Remainder, false, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new JSON object parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamObject(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Object, false, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new JSON object list parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamObjectList(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Object, true, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new JSON array parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamArray(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Array, false, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new JSON array list parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamArrayList(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_Array, true, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new JSON value parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamJSON(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_JSON, false, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new JSON value list parameter (required or optional depends on whether
|
||||||
|
/// `OptionalParams()` call happened) to the currently selected
|
||||||
|
/// sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamJSONList(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_JSON, true, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds new parameter that defines a set of players (required or optional
|
||||||
|
/// depends on whether `OptionalParams()` call happened) to the currently
|
||||||
|
/// selected sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamPlayers(BaseText name, optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds new parameter that defines a list of sets of players (required or
|
||||||
|
/// optional depends on whether `OptionalParams()` call happened) to
|
||||||
|
/// the currently selected sub-command / option.
|
||||||
|
///
|
||||||
|
/// Only fails if provided `name` is `none`.
|
||||||
|
///
|
||||||
|
/// List parameters expect user to enter one or more value of the same type as
|
||||||
|
/// command's arguments.
|
||||||
|
///
|
||||||
|
/// [`name`] will become the name of the parameter
|
||||||
|
/// (it would appear in the generated "help" command info).
|
||||||
|
///
|
||||||
|
/// [`variableName`] will become key for this parameter's value in `HashTable`
|
||||||
|
/// after user's command input is parsed.
|
||||||
|
/// If left `none`, - will coincide with `name` parameter.
|
||||||
|
public final function ParamPlayersList(BaseText name,optional BaseText variableName) {
|
||||||
|
if (name != none) {
|
||||||
|
PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find index of sub-command with a given name `name` in `subcommands`.
|
||||||
|
// `-1` if there's not sub-command with such name.
|
||||||
|
// Case-sensitive.
|
||||||
|
private final function int FindSubCommandIndex(BaseText name) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (name == none) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (i = 0; i < subcommands.length; i += 1) {
|
||||||
|
if (name.Compare(subcommands[i].name)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find index of option with a given name `name` in `options`.
|
||||||
|
// `-1` if there's not sub-command with such name.
|
||||||
|
// Case-sensitive.
|
||||||
|
private final function int FindOptionIndex(BaseText longName) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (longName == none) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (i = 0; i < options.length; i += 1) {
|
||||||
|
if (longName.Compare(options[i].longName)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an empty selection record for subcommand or option with name (long name) `name`.
|
||||||
|
// Doe not check whether subcommand/option with that name already exists.
|
||||||
|
// Copies passed `name`, assumes that it is not `none`.
|
||||||
|
private final function MakeEmptySelection(BaseText name, bool selectedOption) {
|
||||||
|
selectedItemIsOption = selectedOption;
|
||||||
|
selectedItemName = name.Copy();
|
||||||
|
selectedDescription = none;
|
||||||
|
selectedParameterArray.length = 0;
|
||||||
|
selectionIsOptional = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select option with a given long name `longName` from `options`.
|
||||||
|
// If there is no option with specified `longName` in prepared data - creates new record in
|
||||||
|
// selection, otherwise copies previously saved data.
|
||||||
|
// Automatically saves previously selected data into prepared data.
|
||||||
|
// Copies `name` if it has to create new record.
|
||||||
|
private final function SelectOption(BaseText longName) {
|
||||||
|
local int optionIndex;
|
||||||
|
|
||||||
|
if (longName == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(longName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RecordSelection();
|
||||||
|
optionIndex = FindOptionIndex(longName);
|
||||||
|
if (optionIndex < 0) {
|
||||||
|
MakeEmptySelection(longName, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Load appropriate prepared data, if it exists for
|
||||||
|
// option with long name `longName`
|
||||||
|
selectedItemIsOption = true;
|
||||||
|
selectedItemName = options[optionIndex].longName;
|
||||||
|
selectedDescription = options[optionIndex].description;
|
||||||
|
selectionIsOptional = optionsIsOptional[optionIndex] > 0;
|
||||||
|
if (selectionIsOptional) {
|
||||||
|
selectedParameterArray = options[optionIndex].optional;
|
||||||
|
} else {
|
||||||
|
selectedParameterArray = options[optionIndex].required;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves currently selected data into prepared data.
|
||||||
|
private final function RecordSelection() {
|
||||||
|
if (selectedItemName == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedItemIsOption) {
|
||||||
|
RecordSelectedOption();
|
||||||
|
} else {
|
||||||
|
RecordSelectedSubCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves selected sub-command into prepared records.
|
||||||
|
// Assumes that command and not an option is selected.
|
||||||
|
private final function RecordSelectedSubCommand() {
|
||||||
|
local int selectedSubCommandIndex;
|
||||||
|
local Command.SubCommand newSubcommand;
|
||||||
|
|
||||||
|
if (selectedItemName == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedSubCommandIndex = FindSubCommandIndex(selectedItemName);
|
||||||
|
if (selectedSubCommandIndex < 0) {
|
||||||
|
selectedSubCommandIndex = subcommands.length;
|
||||||
|
subcommands[selectedSubCommandIndex] = newSubcommand;
|
||||||
|
}
|
||||||
|
subcommands[selectedSubCommandIndex].name = selectedItemName;
|
||||||
|
subcommands[selectedSubCommandIndex].description = selectedDescription;
|
||||||
|
if (selectionIsOptional) {
|
||||||
|
subcommands[selectedSubCommandIndex].optional = selectedParameterArray;
|
||||||
|
subcommandsIsOptional[selectedSubCommandIndex] = 1;
|
||||||
|
} else {
|
||||||
|
subcommands[selectedSubCommandIndex].required = selectedParameterArray;
|
||||||
|
subcommandsIsOptional[selectedSubCommandIndex] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves currently selected option into prepared records.
|
||||||
|
// Assumes that option and not an command is selected.
|
||||||
|
private final function RecordSelectedOption() {
|
||||||
|
local int selectedOptionIndex;
|
||||||
|
local Command.Option newOption;
|
||||||
|
|
||||||
|
if (selectedItemName == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedOptionIndex = FindOptionIndex(selectedItemName);
|
||||||
|
if (selectedOptionIndex < 0) {
|
||||||
|
selectedOptionIndex = options.length;
|
||||||
|
options[selectedOptionIndex] = newOption;
|
||||||
|
}
|
||||||
|
options[selectedOptionIndex].longName = selectedItemName;
|
||||||
|
options[selectedOptionIndex].description = selectedDescription;
|
||||||
|
if (selectionIsOptional) {
|
||||||
|
options[selectedOptionIndex].optional = selectedParameterArray;
|
||||||
|
optionsIsOptional[selectedOptionIndex] = 1;
|
||||||
|
} else {
|
||||||
|
options[selectedOptionIndex].required = selectedParameterArray;
|
||||||
|
optionsIsOptional[selectedOptionIndex] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates names (printing errors in case of failure) for the option.
|
||||||
|
// Long name must be at least 2 characters long.
|
||||||
|
// Short name must be either:
|
||||||
|
// 1. exactly one character long;
|
||||||
|
// 2. `none`, which leads to deriving `shortName` from `longName`
|
||||||
|
// as a first character.
|
||||||
|
// Anything else will result in logging a failure and rejection of
|
||||||
|
// the option altogether.
|
||||||
|
// Returns `none` if validation failed and chosen short name otherwise
|
||||||
|
// (if `shortName` was used for it - it's value will be copied).
|
||||||
|
private final function BaseText.Character GetValidShortName(
|
||||||
|
BaseText longName,
|
||||||
|
BaseText shortName
|
||||||
|
) {
|
||||||
|
// Validate `longName`
|
||||||
|
if (longName == none) {
|
||||||
|
return _.text.GetInvalidCharacter();
|
||||||
|
}
|
||||||
|
if (longName.GetLength() < 2) {
|
||||||
|
_.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy());
|
||||||
|
return _.text.GetInvalidCharacter();
|
||||||
|
}
|
||||||
|
// Validate `shortName`,
|
||||||
|
// deriving if from `longName` if necessary & possible
|
||||||
|
if (shortName == none) {
|
||||||
|
return longName.GetCharacter(0);
|
||||||
|
}
|
||||||
|
if (shortName.IsEmpty() || shortName.GetLength() > 1) {
|
||||||
|
_.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy());
|
||||||
|
return _.text.GetInvalidCharacter();
|
||||||
|
}
|
||||||
|
return shortName.GetCharacter(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks that if any option record has a long/short name from a given pair of
|
||||||
|
// names (`longName`, `shortName`), then it also has another one.
|
||||||
|
//
|
||||||
|
// i.e. we cannot have several options with identical names:
|
||||||
|
// (--silent, -s) and (--sick, -s).
|
||||||
|
private final function bool VerifyNoOptionNamingConflict(
|
||||||
|
BaseText longName,
|
||||||
|
BaseText.Character shortName
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
local bool sameShortNames, sameLongNames;
|
||||||
|
|
||||||
|
// To make sure we will search through the up-to-date `options`,
|
||||||
|
// record selection into prepared records.
|
||||||
|
RecordSelection();
|
||||||
|
for (i = 0; i < options.length; i += 1) {
|
||||||
|
sameShortNames = _.text.AreEqual(shortName, options[i].shortName);
|
||||||
|
sameLongNames = longName.Compare(options[i].longName);
|
||||||
|
if (sameLongNames && !sameShortNames) {
|
||||||
|
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(longName.Copy());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!sameLongNames && sameShortNames) {
|
||||||
|
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2")
|
||||||
|
errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2")
|
||||||
|
warnSameLongName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.")
|
||||||
|
warnSameShortName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.")
|
||||||
|
}
|
||||||
249
kf_sources/AcediaCore/Classes/CommandList.uc
Normal file
249
kf_sources/AcediaCore/Classes/CommandList.uc
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* Config class for storing map lists.
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandList extends AcediaConfig
|
||||||
|
perObjectConfig
|
||||||
|
config(AcediaCommands);
|
||||||
|
|
||||||
|
//! `CommandList` describes a set of commands and votings that can be made
|
||||||
|
//! available to users inside Commands feature
|
||||||
|
//!
|
||||||
|
//! Optionally, permission configs can be specified for commands and votings,
|
||||||
|
//! allowing server admins to create command lists for different groups player
|
||||||
|
//! with the same commands, but different permissions.
|
||||||
|
|
||||||
|
// For storing `class<Command>` - `string` pairs in the config
|
||||||
|
struct CommandConfigStoragePair {
|
||||||
|
var public class<Command> cmd;
|
||||||
|
var public string config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// For storing `class` - `string` pairs in the config
|
||||||
|
struct VotingConfigStoragePair {
|
||||||
|
var public class<Voting> vtn;
|
||||||
|
var public string config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// For returning `class` - `Text` pairs into other Acedia classes
|
||||||
|
struct EntityConfigPair {
|
||||||
|
var public class<AcediaObject> class;
|
||||||
|
var public Text config;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Allows to specify if this list should only be added when server is running
|
||||||
|
/// in debug mode.
|
||||||
|
/// `true` means yes, `false` means that list will always be available.
|
||||||
|
var public config bool debugOnly;
|
||||||
|
/// Adds a command of specified class with a "default" permissions config.
|
||||||
|
var public config array< class<Command> > command;
|
||||||
|
/// Adds a command of specified class with specified permissions config
|
||||||
|
var public config array<CommandConfigStoragePair> commandWith;
|
||||||
|
/// Adds a voting of specified class with a "default" permissions config
|
||||||
|
var public config array< class<Voting> > voting;
|
||||||
|
/// Adds a voting of specified class with specified permissions config
|
||||||
|
var public config array<VotingConfigStoragePair> votingWith;
|
||||||
|
|
||||||
|
public final function array<EntityConfigPair> GetCommandData() {
|
||||||
|
local int i;
|
||||||
|
local EntityConfigPair nextPair;
|
||||||
|
local array<EntityConfigPair> result;
|
||||||
|
|
||||||
|
for (i = 0; i < command.length; i += 1) {
|
||||||
|
if (command[i] != none) {
|
||||||
|
nextPair.class = command[i];
|
||||||
|
result[result.length] = nextPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < commandWith.length; i += 1) {
|
||||||
|
if (commandWith[i].cmd != none) {
|
||||||
|
nextPair.class = commandWith[i].cmd;
|
||||||
|
if (commandWith[i].config != "") {
|
||||||
|
nextPair.config = _.text.FromString(commandWith[i].config);
|
||||||
|
}
|
||||||
|
result[result.length] = nextPair;
|
||||||
|
// Moved into the `result`
|
||||||
|
nextPair.config = none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function array<EntityConfigPair> GetVotingData() {
|
||||||
|
local int i;
|
||||||
|
local EntityConfigPair nextPair;
|
||||||
|
local array<EntityConfigPair> result;
|
||||||
|
|
||||||
|
for (i = 0; i < voting.length; i += 1) {
|
||||||
|
if (voting[i] != none) {
|
||||||
|
nextPair.class = voting[i];
|
||||||
|
result[result.length] = nextPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < votingWith.length; i += 1) {
|
||||||
|
if (votingWith[i].vtn != none) {
|
||||||
|
nextPair.class = votingWith[i].vtn;
|
||||||
|
if (votingWith[i].config != "") {
|
||||||
|
nextPair.config = _.text.FromString(votingWith[i].config);
|
||||||
|
}
|
||||||
|
result[result.length] = nextPair;
|
||||||
|
// Moved into the `result`
|
||||||
|
nextPair.config = none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function HashTable ToData() {
|
||||||
|
local int i;
|
||||||
|
local ArrayList entityArray;
|
||||||
|
local HashTable result, innerPair;
|
||||||
|
|
||||||
|
result = _.collections.EmptyHashTable();
|
||||||
|
result.SetBool(P("debugOnly"), debugOnly);
|
||||||
|
entityArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < command.length; i += 1) {
|
||||||
|
entityArray.AddString(string(command[i]));
|
||||||
|
}
|
||||||
|
result.SetItem(P("commands"), entityArray);
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
|
||||||
|
entityArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < voting.length; i += 1) {
|
||||||
|
entityArray.AddString(string(voting[i]));
|
||||||
|
}
|
||||||
|
result.SetItem(P("votings"), entityArray);
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
|
||||||
|
entityArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < commandWith.length; i += 1) {
|
||||||
|
innerPair = _.collections.EmptyHashTable();
|
||||||
|
innerPair.SetString(P("command"), string(commandWith[i].cmd));
|
||||||
|
innerPair.SetString(P("config"), commandWith[i].config);
|
||||||
|
entityArray.AddItem(innerPair);
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
result.SetItem(P("commandsWithConfig"), entityArray);
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
|
||||||
|
entityArray = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < votingWith.length; i += 1) {
|
||||||
|
innerPair = _.collections.EmptyHashTable();
|
||||||
|
innerPair.SetString(P("voting"), string(votingWith[i].vtn));
|
||||||
|
innerPair.SetString(P("config"), votingWith[i].config);
|
||||||
|
entityArray.AddItem(innerPair);
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
result.SetItem(P("votingsWithConfig"), entityArray);
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function FromData(HashTable source) {
|
||||||
|
local int i;
|
||||||
|
local ArrayList entityArray;
|
||||||
|
local HashTable innerPair;
|
||||||
|
local class<Command> nextCommandClass;
|
||||||
|
local class<Voting> nextVotingClass;
|
||||||
|
local CommandConfigStoragePair nextCommandPair;
|
||||||
|
local VotingConfigStoragePair nextVotingPair;
|
||||||
|
|
||||||
|
if (source == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugOnly = source.GetBool(P("debugOnly"));
|
||||||
|
command.length = 0;
|
||||||
|
entityArray = source.GetArrayList(P("commands"));
|
||||||
|
if (entityArray != none) {
|
||||||
|
for (i = 0; i < entityArray.GetLength(); i += 1) {
|
||||||
|
nextCommandClass = class<Command>(_.memory.LoadClass_S(entityArray.GetString(i)));
|
||||||
|
if (nextCommandClass != none) {
|
||||||
|
command[command.length] = nextCommandClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
|
||||||
|
voting.length = 0;
|
||||||
|
entityArray = source.GetArrayList(P("votings"));
|
||||||
|
if (entityArray != none) {
|
||||||
|
for (i = 0; i < entityArray.GetLength(); i += 1) {
|
||||||
|
nextVotingClass = class<Voting>(_.memory.LoadClass_S(entityArray.GetString(i)));
|
||||||
|
if (nextVotingClass != none) {
|
||||||
|
voting[voting.length] = nextVotingClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
|
||||||
|
commandWith.length = 0;
|
||||||
|
entityArray = source.GetArrayList(P("commandsWithConfig"));
|
||||||
|
if (entityArray != none) {
|
||||||
|
for (i = 0; i < entityArray.GetLength(); i += 1) {
|
||||||
|
innerPair = entityArray.GetHashTable(i);
|
||||||
|
if (innerPair == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextCommandPair.cmd =
|
||||||
|
class<Command>(_.memory.LoadClass_S(innerPair.GetString(P("command"))));
|
||||||
|
nextCommandPair.config = innerPair.GetString(P("config"));
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
if (nextCommandPair.cmd != none) {
|
||||||
|
commandWith[commandWith.length] = nextCommandPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
|
||||||
|
votingWith.length = 0;
|
||||||
|
entityArray = source.GetArrayList(P("votingsWithConfig"));
|
||||||
|
if (entityArray != none) {
|
||||||
|
for (i = 0; i < entityArray.GetLength(); i += 1) {
|
||||||
|
innerPair = entityArray.GetHashTable(i);
|
||||||
|
if (innerPair == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextVotingPair.vtn =
|
||||||
|
class<Voting>(_.memory.LoadClass_S(innerPair.GetString(P("voting"))));
|
||||||
|
nextVotingPair.config = innerPair.GetString(P("config"));
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
if (nextVotingPair.vtn != none) {
|
||||||
|
votingWith[votingWith.length] = nextVotingPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(entityArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function DefaultIt() {
|
||||||
|
debugOnly = false;
|
||||||
|
command.length = 0;
|
||||||
|
commandWith.length = 0;
|
||||||
|
voting.length = 0;
|
||||||
|
votingWith.length = 0;
|
||||||
|
command[0] = class'ACommandHelp';
|
||||||
|
command[1] = class'ACommandVote';
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
configName = "AcediaCommands"
|
||||||
|
supportsDataConversion = true
|
||||||
|
debugOnly = false
|
||||||
|
command(0) = class'ACommandHelp'
|
||||||
|
command(1) = class'ACommandVote'
|
||||||
|
}
|
||||||
983
kf_sources/AcediaCore/Classes/CommandParser.uc
Normal file
983
kf_sources/AcediaCore/Classes/CommandParser.uc
Normal file
@ -0,0 +1,983 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandParser extends AcediaObject
|
||||||
|
dependson(Command);
|
||||||
|
|
||||||
|
|
||||||
|
//! Class specialized for parsing user input of the command's call into
|
||||||
|
//![ `Command.CallData`] structure with the information about all parsed
|
||||||
|
//! arguments.
|
||||||
|
//!
|
||||||
|
//! [`CommandParser`] is not made to parse the whole input:
|
||||||
|
//!
|
||||||
|
//! * Command's name needs to be parsed and resolved as an alias before using
|
||||||
|
//! this parser - it won't do this hob for you;
|
||||||
|
//! * List of targeted players must also be parsed using [`PlayersParser`] -
|
||||||
|
//! [`CommandParser`] won't do this for you;
|
||||||
|
//! * Optionally one can also decide on the referred subcommand and pass it into
|
||||||
|
//! [`ParseWith()`] method. If subcommand's name is not passed -
|
||||||
|
//! [`CommandParser`] will try to parse it itself.
|
||||||
|
//! This feature is used to add support for subcommand aliases.
|
||||||
|
//!
|
||||||
|
//! However, above steps are handled by [`Commands_Feature`] and one only needs to
|
||||||
|
//! call that feature's [`HandleInput()`] methods to pass user input with command
|
||||||
|
//! call line there.
|
||||||
|
//!
|
||||||
|
//! # Usage
|
||||||
|
//!
|
||||||
|
//! Allocate [`CommandParser`] and call [`ParseWith()`] method, providing it with:
|
||||||
|
//!
|
||||||
|
//! 1. [`Parser`], filled with command call input;
|
||||||
|
//! 2. Command's data that describes subcommands, options and their parameters
|
||||||
|
//! for the command, which call we are parsing;
|
||||||
|
//! 3. (Optionally) [`EPlayer`] reference to the player that initiated
|
||||||
|
//! the command call;
|
||||||
|
//! 4. (Optionally) Subcommand to be used - this will prevent [`CommandParser`]
|
||||||
|
//! from parsing subcommand name itself. Used for implementing aliases that
|
||||||
|
//! refer to a particular subcommand.
|
||||||
|
//!
|
||||||
|
//! # Implementation
|
||||||
|
//!
|
||||||
|
//! [`CommandParser`] stores both its state and command data, relevant to parsing,
|
||||||
|
//! as its member variables during the whole parsing process, instead of passing
|
||||||
|
//! that data around in every single method.
|
||||||
|
//!
|
||||||
|
//! We will give a brief overview of how around 20 parsing methods below are
|
||||||
|
//! interconnected.
|
||||||
|
//!
|
||||||
|
//! The only public method [`ParseWith()`] is used to start parsing and it uses
|
||||||
|
//! [`PickSubCommand()`] to first try and figure out what sub command is
|
||||||
|
//! intended by user's input.
|
||||||
|
//!
|
||||||
|
//! Main bulk of the work is done by [`ParseParameterArrays()`] method, for
|
||||||
|
//! simplicity broken into two [`ParseRequiredParameterArray()`] and
|
||||||
|
//! [`ParseOptionalParameterArray()`] methods that can parse
|
||||||
|
//! parameters for both command itself and it's options.
|
||||||
|
//!
|
||||||
|
//! They go through arrays of required and optional parameters, calling
|
||||||
|
//! [`ParseParameter()`] for each parameters, which in turn can make several
|
||||||
|
//! calls of [`ParseSingleValue()`] to parse parameters' values: it is called
|
||||||
|
//! once for single-valued parameters, but possibly several times for list
|
||||||
|
//! parameters that can contain several values.
|
||||||
|
//!
|
||||||
|
//! So main parsing method looks something like:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! ParseParameterArrays() {
|
||||||
|
//! loop ParseParameter() {
|
||||||
|
//! loop ParseSingleValue()
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! [`ParseSingleValue()`] is essentially that redirects it's method call to
|
||||||
|
//! another, more specific, parsing method based on the parameter type.
|
||||||
|
//!
|
||||||
|
//! Finally, to allow users to specify options at any point in command, we call
|
||||||
|
//! [`TryParsingOptions()`] at the beginning of every [`ParseSingleValue()`]
|
||||||
|
//! (the only parameter that has higher priority than options is
|
||||||
|
//! [`CPT_Remainder`]), since option definition can appear at any place between
|
||||||
|
//! parameters. We also call `TryParsingOptions()` *after* we've parsed all
|
||||||
|
//! command's parameters, since that case won't be detected by parsing them
|
||||||
|
//! *before* every parameter.
|
||||||
|
//!
|
||||||
|
//! [`TryParsingOptions()`] itself simply tries to detect "-" and "--" prefixes
|
||||||
|
//! (filtering out negative numeric values) and then redirect the call to either
|
||||||
|
//! of more specialized methods: [`ParseLongOption()`] or
|
||||||
|
//! [`ParseShortOption()`], that can in turn make another
|
||||||
|
//! [`ParseParameterArrays()`] call, if specified option has parameters.
|
||||||
|
//!
|
||||||
|
//! NOTE: [`ParseParameterArrays()`] can only nest in itself once, since option
|
||||||
|
//! declaration always interrupts previous option's parameter list.
|
||||||
|
//! Rest of the methods perform simple auxiliary functions.
|
||||||
|
|
||||||
|
// Describes which parameters we are currently parsing, classifying them
|
||||||
|
// as either "necessary" or "extra".
|
||||||
|
//
|
||||||
|
// E.g. if last require parameter is a list of integers,
|
||||||
|
// then after parsing first integer we are:
|
||||||
|
//
|
||||||
|
// * Still parsing required *parameter* "integer list";
|
||||||
|
// * But no more integers are *necessary* for successful parsing.
|
||||||
|
//
|
||||||
|
// Therefore we consider parameter "necessary" if the lack of it will
|
||||||
|
// result in failed parsing and "extra" otherwise.
|
||||||
|
enum ParsingTarget {
|
||||||
|
// We are in the process of parsing required parameters, that must all
|
||||||
|
// be present.
|
||||||
|
// This case does not include parsing last required parameter: it needs
|
||||||
|
// to be treated differently to track when we change from "necessary" to
|
||||||
|
// "extra" parameters.
|
||||||
|
CPT_NecessaryParameter,
|
||||||
|
// We are parsing last necessary parameter.
|
||||||
|
CPT_LastNecessaryParameter,
|
||||||
|
// We are not parsing extra parameters that can be safely omitted.
|
||||||
|
CPT_ExtraParameter,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parser filled with user input.
|
||||||
|
var private Parser commandParser;
|
||||||
|
// Data for sub-command specified by both command we are parsing
|
||||||
|
// and user's input; determined early during parsing.
|
||||||
|
var private Command.SubCommand pickedSubCommand;
|
||||||
|
// Options available for the command we are parsing.
|
||||||
|
var private array<Command.Option> availableOptions;
|
||||||
|
// Result variable we are filling during the parsing process,
|
||||||
|
// should be `none` outside of [`self.ParseWith()`] method call.
|
||||||
|
var private Command.CallData nextResult;
|
||||||
|
|
||||||
|
// Parser for player parameters, setup with a caller for current parsing
|
||||||
|
var private PlayersParser currentPlayersParser;
|
||||||
|
// Current [`ParsingTarget`], see it's enum description for more details
|
||||||
|
var private ParsingTarget currentTarget;
|
||||||
|
// `true` means we are parsing parameters for a command's option and
|
||||||
|
// `false` means we are parsing command's own parameters
|
||||||
|
var private bool currentTargetIsOption;
|
||||||
|
// If we are parsing parameters for an option (`currentTargetIsOption == true`)
|
||||||
|
// this variable will store that option's data.
|
||||||
|
var private Command.Option targetOption;
|
||||||
|
// Last successful state of [`commandParser`].
|
||||||
|
var Parser.ParserState confirmedState;
|
||||||
|
// Options we have so far encountered during parsing, necessary since we want
|
||||||
|
// to forbid specifying th same option more than once.
|
||||||
|
var private array<Command.Option> usedOptions;
|
||||||
|
|
||||||
|
// Literals that can be used as boolean values
|
||||||
|
var private array<string> booleanTrueEquivalents;
|
||||||
|
var private array<string> booleanFalseEquivalents;
|
||||||
|
|
||||||
|
var LoggerAPI.Definition errNoSubCommands;
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses user's input given in [`parser`] using command's information given by
|
||||||
|
/// [`commandData`].
|
||||||
|
///
|
||||||
|
/// Optionally, sub-command can be specified for the [`CommandParser`] to use
|
||||||
|
/// via [`specifiedSubCommand`] argument.
|
||||||
|
/// If this argument's value is `none` - it will be parsed from [`parser`]'s
|
||||||
|
/// data instead.
|
||||||
|
///
|
||||||
|
/// Returns results of parsing, described by [`Command.CallData`].
|
||||||
|
/// Returned object is guaranteed to be not `none`.
|
||||||
|
public final function Command.CallData ParseWith(
|
||||||
|
Parser parser,
|
||||||
|
Command.Data commandData,
|
||||||
|
EPlayer callerPlayer,
|
||||||
|
optional BaseText specifiedSubCommand
|
||||||
|
) {
|
||||||
|
local HashTable commandParameters;
|
||||||
|
// Temporary object to return `nextResult` while setting variable to `none`
|
||||||
|
local Command.CallData toReturn;
|
||||||
|
|
||||||
|
nextResult.parameters = _.collections.EmptyHashTable();
|
||||||
|
nextResult.options = _.collections.EmptyHashTable();
|
||||||
|
if (commandData.subCommands.length == 0) {
|
||||||
|
DeclareError(CET_NoSubCommands, none);
|
||||||
|
toReturn = nextResult;
|
||||||
|
Reset();
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
if (parser == none || !parser.Ok()) {
|
||||||
|
DeclareError(CET_BadParser, none);
|
||||||
|
toReturn = nextResult;
|
||||||
|
Reset();
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
commandParser = parser;
|
||||||
|
availableOptions = commandData.options;
|
||||||
|
currentPlayersParser =
|
||||||
|
PlayersParser(_.memory.Allocate(class'PlayersParser'));
|
||||||
|
currentPlayersParser.SetSelf(callerPlayer);
|
||||||
|
// (subcommand) (parameters, possibly with options) and nothing else!
|
||||||
|
PickSubCommand(commandData, specifiedSubCommand);
|
||||||
|
nextResult.subCommandName = pickedSubCommand.name.Copy();
|
||||||
|
commandParameters = ParseParameterArrays(pickedSubCommand.required, pickedSubCommand.optional);
|
||||||
|
AssertNoTrailingInput(); // make sure there is nothing else
|
||||||
|
if (commandParser.Ok()) {
|
||||||
|
nextResult.parameters = commandParameters;
|
||||||
|
} else {
|
||||||
|
_.memory.Free(commandParameters);
|
||||||
|
}
|
||||||
|
// Clean up
|
||||||
|
toReturn = nextResult;
|
||||||
|
Reset();
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero important variables
|
||||||
|
private final function Reset() {
|
||||||
|
local Command.CallData blankCallData;
|
||||||
|
|
||||||
|
_.memory.Free(currentPlayersParser);
|
||||||
|
currentPlayersParser = none;
|
||||||
|
// We didn't create this one and are not meant to free it either
|
||||||
|
commandParser = none;
|
||||||
|
nextResult = blankCallData;
|
||||||
|
currentTarget = CPT_NecessaryParameter;
|
||||||
|
currentTargetIsOption = false;
|
||||||
|
usedOptions.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auxiliary method for recording errors
|
||||||
|
private final function DeclareError(Command.ErrorType type, optional BaseText cause) {
|
||||||
|
nextResult.parsingError = type;
|
||||||
|
if (cause != none) {
|
||||||
|
nextResult.errorCause = cause.Copy();
|
||||||
|
}
|
||||||
|
if (commandParser != none) {
|
||||||
|
commandParser.Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser != none`, is in successful state.
|
||||||
|
//
|
||||||
|
// Picks a sub command based on it's contents (parser's pointer must be before
|
||||||
|
// where subcommand's name is specified).
|
||||||
|
//
|
||||||
|
// If [`specifiedSubCommand`] is not `none` - will always use that value instead
|
||||||
|
// of parsing it from [`commandParser`].
|
||||||
|
private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) {
|
||||||
|
local int i;
|
||||||
|
local MutableText candidateSubCommandName;
|
||||||
|
local Command.SubCommand emptySubCommand;
|
||||||
|
local array<Command.SubCommand> allSubCommands;
|
||||||
|
|
||||||
|
allSubCommands = commandData.subCommands;
|
||||||
|
if (allSubcommands.length == 0) {
|
||||||
|
_.logger.Auto(errNoSubCommands).ArgClass(class);
|
||||||
|
pickedSubCommand = emptySubCommand;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get candidate name
|
||||||
|
confirmedState = commandParser.GetCurrentState();
|
||||||
|
if (specifiedSubCommand != none) {
|
||||||
|
candidateSubCommandName = specifiedSubCommand.MutableCopy();
|
||||||
|
} else {
|
||||||
|
commandParser.Skip().MUntil(candidateSubCommandName,, true);
|
||||||
|
}
|
||||||
|
// Try matching it to sub commands
|
||||||
|
pickedSubCommand = allSubcommands[0];
|
||||||
|
if (candidateSubCommandName.IsEmpty()) {
|
||||||
|
candidateSubCommandName.FreeSelf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i = 0; i < allSubcommands.length; i += 1) {
|
||||||
|
if (candidateSubCommandName.Compare(allSubcommands[i].name)) {
|
||||||
|
candidateSubCommandName.FreeSelf();
|
||||||
|
pickedSubCommand = allSubcommands[i];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We will only reach here if we did not match any sub commands,
|
||||||
|
// meaning that whatever consumed by[ `candidateSubCommandName`] probably
|
||||||
|
// has a different meaning.
|
||||||
|
commandParser.RestoreState(confirmedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` is not `none`
|
||||||
|
// Declares an error if `commandParser` still has any input left
|
||||||
|
private final function AssertNoTrailingInput() {
|
||||||
|
local Text remainder;
|
||||||
|
|
||||||
|
if (!commandParser.Ok()) return;
|
||||||
|
if (commandParser.Skip().GetRemainingLength() <= 0) return;
|
||||||
|
|
||||||
|
remainder = commandParser.GetRemainder();
|
||||||
|
DeclareError(CET_UnusedCommandParameters, remainder);
|
||||||
|
remainder.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` is not `none`.
|
||||||
|
// Parses given required and optional parameters along with any possible option
|
||||||
|
// declarations.
|
||||||
|
// Returns `HashTable` filled with (variable, parsed value) pairs.
|
||||||
|
// Failure is equal to `commandParser` entering into a failed state.
|
||||||
|
private final function HashTable ParseParameterArrays(
|
||||||
|
array<Command.Parameter> requiredParameters,
|
||||||
|
array<Command.Parameter> optionalParameters
|
||||||
|
) {
|
||||||
|
local HashTable parsedParameters;
|
||||||
|
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
parsedParameters = _.collections.EmptyHashTable();
|
||||||
|
// Parse parameters
|
||||||
|
ParseRequiredParameterArray(parsedParameters, requiredParameters);
|
||||||
|
ParseOptionalParameterArray(parsedParameters, optionalParameters);
|
||||||
|
// Parse trailing options
|
||||||
|
while (TryParsingOptions());
|
||||||
|
return parsedParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
//
|
||||||
|
// Parses given required parameters along with any possible option declarations into given
|
||||||
|
// `parsedParameters` `HashTable`.
|
||||||
|
private final function ParseRequiredParameterArray(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
array<Command.Parameter> requiredParameters
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentTarget = CPT_NecessaryParameter;
|
||||||
|
while (i < requiredParameters.length) {
|
||||||
|
if (i == requiredParameters.length - 1) {
|
||||||
|
currentTarget = CPT_LastNecessaryParameter;
|
||||||
|
}
|
||||||
|
// Parse parameters one-by-one, reporting appropriate errors
|
||||||
|
if (!ParseParameter(parsedParameters, requiredParameters[i])) {
|
||||||
|
// Any failure to parse required parameter leads to error
|
||||||
|
if (currentTargetIsOption) {
|
||||||
|
DeclareError( CET_NoRequiredParamForOption,
|
||||||
|
targetOption.longName);
|
||||||
|
} else {
|
||||||
|
DeclareError( CET_NoRequiredParam,
|
||||||
|
requiredParameters[i].displayName);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
currentTarget = CPT_ExtraParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
//
|
||||||
|
// Parses given optional parameters along with any possible option declarations
|
||||||
|
// into given `parsedParameters` hash table.
|
||||||
|
private final function ParseOptionalParameterArray(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
array<Command.Parameter> optionalParameters
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (i < optionalParameters.length) {
|
||||||
|
confirmedState = commandParser.GetCurrentState();
|
||||||
|
// Parse parameters one-by-one, reporting appropriate errors
|
||||||
|
if (!ParseParameter(parsedParameters, optionalParameters[i])) {
|
||||||
|
// Propagate errors
|
||||||
|
if (nextResult.parsingError != CET_None) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Failure to parse optional parameter is fine if
|
||||||
|
// it is caused by that parameters simply missing
|
||||||
|
commandParser.RestoreState(confirmedState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
//
|
||||||
|
// Parses one given parameter along with any possible option declarations into
|
||||||
|
// given `parsedParameters` `HashTable`.
|
||||||
|
//
|
||||||
|
// Returns `true` if we've successfully parsed given parameter without any
|
||||||
|
// errors.
|
||||||
|
private final function bool ParseParameter(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local bool parsedEnough;
|
||||||
|
|
||||||
|
confirmedState = commandParser.GetCurrentState();
|
||||||
|
while (ParseSingleValue(parsedParameters, expectedParameter)) {
|
||||||
|
if (currentTarget == CPT_LastNecessaryParameter) {
|
||||||
|
currentTarget = CPT_ExtraParameter;
|
||||||
|
}
|
||||||
|
parsedEnough = true;
|
||||||
|
// We are done if there is either no more input or we only needed
|
||||||
|
// to parse a single value
|
||||||
|
if (!expectedParameter.allowsList) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (commandParser.Skip().HasFinished()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
confirmedState = commandParser.GetCurrentState();
|
||||||
|
}
|
||||||
|
// We only succeeded in parsing if we've parsed enough for
|
||||||
|
// a given parameter and did not encounter any errors
|
||||||
|
if (parsedEnough && nextResult.parsingError == CET_None) {
|
||||||
|
commandParser.RestoreState(confirmedState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Clean up any values `ParseSingleValue` might have recorded
|
||||||
|
parsedParameters.RemoveItem(expectedParameter.variableName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
//
|
||||||
|
// Parses a single value for a given parameter (e.g. one integer for integer or
|
||||||
|
// integer list parameter types) along with any possible option declarations
|
||||||
|
// into given `parsedParameters`.
|
||||||
|
//
|
||||||
|
// Returns `true` if we've successfully parsed a single value without
|
||||||
|
// any errors.
|
||||||
|
private final function bool ParseSingleValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
// Before parsing any other value we need to check if user has specified any options instead.
|
||||||
|
//
|
||||||
|
// However this might lead to errors if we are already parsing necessary parameters of another
|
||||||
|
// option: we must handle such situation and report an error.
|
||||||
|
if (currentTargetIsOption) {
|
||||||
|
// There is no problem is option's parameter is remainder
|
||||||
|
if (expectedParameter.type == CPT_Remainder) {
|
||||||
|
return ParseRemainderValue(parsedParameters, expectedParameter);
|
||||||
|
}
|
||||||
|
if (currentTarget != CPT_ExtraParameter && TryParsingOptions()) {
|
||||||
|
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (TryParsingOptions());
|
||||||
|
// First we try `CPT_Remainder` parameter, since it is a special case that
|
||||||
|
// consumes all further input
|
||||||
|
if (expectedParameter.type == CPT_Remainder) {
|
||||||
|
return ParseRemainderValue(parsedParameters, expectedParameter);
|
||||||
|
}
|
||||||
|
// Propagate errors after parsing options
|
||||||
|
if (nextResult.parsingError != CET_None) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Try parsing one of the variable types
|
||||||
|
if (expectedParameter.type == CPT_Boolean) {
|
||||||
|
return ParseBooleanValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_Integer) {
|
||||||
|
return ParseIntegerValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_Number) {
|
||||||
|
return ParseNumberValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_Text) {
|
||||||
|
return ParseTextValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_Remainder) {
|
||||||
|
return ParseRemainderValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_Object) {
|
||||||
|
return ParseObjectValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_Array) {
|
||||||
|
return ParseArrayValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_JSON) {
|
||||||
|
return ParseJSONValue(parsedParameters, expectedParameter);
|
||||||
|
} else if (expectedParameter.type == CPT_Players) {
|
||||||
|
return ParsePlayersValue(parsedParameters, expectedParameter);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
//
|
||||||
|
// Parses a single boolean value into given `parsedParameters` hash table.
|
||||||
|
private final function bool ParseBooleanValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
local bool isValidBooleanLiteral;
|
||||||
|
local bool booleanValue;
|
||||||
|
local MutableText parsedLiteral;
|
||||||
|
|
||||||
|
commandParser.Skip().MUntil(parsedLiteral,, true);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
_.memory.Free(parsedLiteral);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Try to match parsed literal to any recognizable boolean literals
|
||||||
|
for (i = 0; i < booleanTrueEquivalents.length; i += 1) {
|
||||||
|
if (parsedLiteral.CompareToString(booleanTrueEquivalents[i], SCASE_INSENSITIVE)) {
|
||||||
|
isValidBooleanLiteral = true;
|
||||||
|
booleanValue = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < booleanFalseEquivalents.length; i += 1) {
|
||||||
|
if (isValidBooleanLiteral) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (parsedLiteral.CompareToString(booleanFalseEquivalents[i], SCASE_INSENSITIVE)) {
|
||||||
|
isValidBooleanLiteral = true;
|
||||||
|
booleanValue = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsedLiteral.FreeSelf();
|
||||||
|
if (!isValidBooleanLiteral) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, _.box.bool(booleanValue));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
// Parses a single integer value into given `parsedParameters` hash table.
|
||||||
|
private final function bool ParseIntegerValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local int integerValue;
|
||||||
|
|
||||||
|
commandParser.Skip().MInteger(integerValue);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, _.box.int(integerValue));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
// Parses a single number (float) value into given `parsedParameters`
|
||||||
|
// hash table.
|
||||||
|
private final function bool ParseNumberValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local float numberValue;
|
||||||
|
|
||||||
|
commandParser.Skip().MNumber(numberValue);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, _.box.float(numberValue));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
// Parses a single `Text` value into given `parsedParameters`
|
||||||
|
// hash table.
|
||||||
|
private final function bool ParseTextValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local bool failedParsing;
|
||||||
|
local MutableText textValue;
|
||||||
|
local Parser.ParserState initialState;
|
||||||
|
local HashTable resolvedPair;
|
||||||
|
|
||||||
|
// (needs some work for reading formatting `string`s from `Text` objects)
|
||||||
|
initialState = commandParser.Skip().GetCurrentState();
|
||||||
|
// Try manually parsing as a string literal first, since then we will
|
||||||
|
// allow empty `textValue` as a result
|
||||||
|
commandParser.MStringLiteral(textValue);
|
||||||
|
failedParsing = !commandParser.Ok();
|
||||||
|
// Otherwise - empty values are not allowed
|
||||||
|
if (failedParsing) {
|
||||||
|
_.memory.Free(textValue);
|
||||||
|
commandParser.RestoreState(initialState).MString(textValue);
|
||||||
|
failedParsing = (!commandParser.Ok() || textValue.IsEmpty());
|
||||||
|
}
|
||||||
|
if (failedParsing) {
|
||||||
|
_.memory.Free(textValue);
|
||||||
|
commandParser.Fail();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resolvedPair = AutoResolveAlias(textValue, expectedParameter.aliasSourceName);
|
||||||
|
if (resolvedPair != none) {
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, resolvedPair);
|
||||||
|
_.memory.Free(textValue);
|
||||||
|
} else {
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, textValue.IntoText());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves alias and returns it, along with the resolved value, if parameter
|
||||||
|
// was specified to be auto-resolved.
|
||||||
|
// Returns `none` otherwise.
|
||||||
|
private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) {
|
||||||
|
local HashTable result;
|
||||||
|
local Text resolvedValue, immutableValue;
|
||||||
|
|
||||||
|
if (textValue == none) return none;
|
||||||
|
if (aliasSourceName == none) return none;
|
||||||
|
|
||||||
|
// Always create `HashTable` with at least "alias" key
|
||||||
|
result = _.collections.EmptyHashTable();
|
||||||
|
immutableValue = textValue.Copy();
|
||||||
|
result.SetItem(P("alias"), immutableValue);
|
||||||
|
_.memory.Free(immutableValue);
|
||||||
|
// Add "value" key only after we've checked for "$" prefix
|
||||||
|
if (!textValue.StartsWithS("$")) {
|
||||||
|
result.SetItem(P("value"), immutableValue);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (aliasSourceName.Compare(P("weapon"))) {
|
||||||
|
resolvedValue = _.alias.ResolveWeapon(textValue, true);
|
||||||
|
} else if (aliasSourceName.Compare(P("color"))) {
|
||||||
|
resolvedValue = _.alias.ResolveColor(textValue, true);
|
||||||
|
} else if (aliasSourceName.Compare(P("feature"))) {
|
||||||
|
resolvedValue = _.alias.ResolveFeature(textValue, true);
|
||||||
|
} else if (aliasSourceName.Compare(P("entity"))) {
|
||||||
|
resolvedValue = _.alias.ResolveEntity(textValue, true);
|
||||||
|
} else {
|
||||||
|
resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true);
|
||||||
|
}
|
||||||
|
result.SetItem(P("value"), resolvedValue);
|
||||||
|
_.memory.Free(resolvedValue);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
//
|
||||||
|
// Parses a single `Text` value into given `parsedParameters` hash table,
|
||||||
|
// consuming all remaining contents.
|
||||||
|
private final function bool ParseRemainderValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local MutableText value;
|
||||||
|
|
||||||
|
commandParser.Skip().MUntil(value);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, value.IntoText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
//
|
||||||
|
// Parses a single JSON object into given `parsedParameters` hash table.
|
||||||
|
private final function bool ParseObjectValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local HashTable objectValue;
|
||||||
|
|
||||||
|
objectValue = _.json.ParseHashTableWith(commandParser);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, objectValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
// Parses a single JSON array into given `parsedParameters` hash table.
|
||||||
|
private final function bool ParseArrayValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local ArrayList arrayValue;
|
||||||
|
|
||||||
|
arrayValue = _.json.ParseArrayListWith(commandParser);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, arrayValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
// Parses a single JSON value into given `parsedParameters`
|
||||||
|
// hash table.
|
||||||
|
private final function bool ParseJSONValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter
|
||||||
|
) {
|
||||||
|
local AcediaObject jsonValue;
|
||||||
|
|
||||||
|
jsonValue = _.json.ParseWith(commandParser);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, jsonValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `parsedParameters` are not `none`.
|
||||||
|
// Parses a single JSON value into given `parsedParameters` hash table.
|
||||||
|
private final function bool ParsePlayersValue(
|
||||||
|
HashTable parsedParameters,
|
||||||
|
Command.Parameter expectedParameter)
|
||||||
|
{
|
||||||
|
local ArrayList resultPlayerList;
|
||||||
|
local array<EPlayer> targetPlayers;
|
||||||
|
|
||||||
|
currentPlayersParser.ParseWith(commandParser);
|
||||||
|
if (commandParser.Ok()) {
|
||||||
|
targetPlayers = currentPlayersParser.GetPlayers();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resultPlayerList = _.collections.NewArrayList(targetPlayers);
|
||||||
|
_.memory.FreeMany(targetPlayers);
|
||||||
|
RecordParameter(parsedParameters, expectedParameter, resultPlayerList);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `parsedParameters` is not `none`.
|
||||||
|
//
|
||||||
|
// Records `value` for a given `parameter` into a given `parametersArray`.
|
||||||
|
// If parameter is not a list type - simply records `value` as value under
|
||||||
|
// `parameter.variableName` key.
|
||||||
|
// If parameter is a list type - pushed value at the end of an array, recorded at
|
||||||
|
// `parameter.variableName` key (creating it if missing).
|
||||||
|
//
|
||||||
|
// All recorded values are managed by `parametersArray`.
|
||||||
|
private final function RecordParameter(
|
||||||
|
HashTable parametersArray,
|
||||||
|
Command.Parameter parameter,
|
||||||
|
/*take*/ AcediaObject value
|
||||||
|
) {
|
||||||
|
local ArrayList parameterVariable;
|
||||||
|
|
||||||
|
if (!parameter.allowsList) {
|
||||||
|
parametersArray.SetItem(parameter.variableName, value);
|
||||||
|
_.memory.Free(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parameterVariable = ArrayList(parametersArray.GetItem(parameter.variableName));
|
||||||
|
if (parameterVariable == none) {
|
||||||
|
parameterVariable = _.collections.EmptyArrayList();
|
||||||
|
}
|
||||||
|
parameterVariable.AddItem(value);
|
||||||
|
_.memory.Free(value);
|
||||||
|
parametersArray.SetItem(parameter.variableName, parameterVariable);
|
||||||
|
_.memory.Free(parameterVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` is not `none`.
|
||||||
|
//
|
||||||
|
// Tries to parse an option declaration (along with all of it's parameters) with
|
||||||
|
// `commandParser`.
|
||||||
|
//
|
||||||
|
// Returns `true` on success and `false` otherwise.
|
||||||
|
//
|
||||||
|
// In case of failure to detect option declaration also reverts state of
|
||||||
|
// `commandParser` to that before `TryParsingOptions()` call.
|
||||||
|
// However, if option declaration was present, but invalid (or had invalid
|
||||||
|
// parameters) parser will be left in a failed state.
|
||||||
|
private final function bool TryParsingOptions() {
|
||||||
|
local int temporaryInt;
|
||||||
|
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
confirmedState = commandParser.GetCurrentState();
|
||||||
|
// Long options
|
||||||
|
commandParser.Skip().Match(P("--"));
|
||||||
|
if (commandParser.Ok()) {
|
||||||
|
return ParseLongOption();
|
||||||
|
}
|
||||||
|
// Filter out negative numbers that start similarly to short options:
|
||||||
|
// -3, -5.7, -.9
|
||||||
|
commandParser
|
||||||
|
.RestoreState(confirmedState)
|
||||||
|
.Skip()
|
||||||
|
.Match(P("-"))
|
||||||
|
.MUnsignedInteger(temporaryInt, 10, 1);
|
||||||
|
if (commandParser.Ok()) {
|
||||||
|
commandParser.RestoreState(confirmedState);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
commandParser.RestoreState(confirmedState).Skip().Match(P("-."));
|
||||||
|
if (commandParser.Ok()) {
|
||||||
|
commandParser.RestoreState(confirmedState);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Short options
|
||||||
|
commandParser.RestoreState(confirmedState).Skip().Match(P("-"));
|
||||||
|
if (commandParser.Ok()) {
|
||||||
|
return ParseShortOption();
|
||||||
|
}
|
||||||
|
commandParser.RestoreState(confirmedState);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` is not `none`.
|
||||||
|
//
|
||||||
|
// Tries to parse a long option name along with all of it's possible parameters
|
||||||
|
// with `commandParser`.
|
||||||
|
//
|
||||||
|
// Returns `true` on success and `false` otherwise. At the point this method is
|
||||||
|
// called, option declaration is already assumed to be detected and any failure
|
||||||
|
// implies parsing error (ending in failed `Command.CallData`).
|
||||||
|
private final function bool ParseLongOption() {
|
||||||
|
local int i, optionIndex;
|
||||||
|
local MutableText optionName;
|
||||||
|
|
||||||
|
commandParser.MUntil(optionName,, true);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while (optionIndex < availableOptions.length) {
|
||||||
|
if (optionName.Compare(availableOptions[optionIndex].longName)) break;
|
||||||
|
optionIndex += 1;
|
||||||
|
}
|
||||||
|
if (optionIndex >= availableOptions.length) {
|
||||||
|
DeclareError(CET_UnknownOption, optionName);
|
||||||
|
optionName.FreeSelf();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i = 0; i < usedOptions.length; i += 1) {
|
||||||
|
if (optionName.Compare(usedOptions[i].longName)) {
|
||||||
|
DeclareError(CET_RepeatedOption, optionName);
|
||||||
|
optionName.FreeSelf();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//usedOptions[usedOptions.length] = availableOptions[optionIndex];
|
||||||
|
optionName.FreeSelf();
|
||||||
|
return ParseOptionParameters(availableOptions[optionIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `nextResult` are not `none`.
|
||||||
|
//
|
||||||
|
// Tries to parse a short option name along with all of it's possible parameters
|
||||||
|
// with `commandParser`.
|
||||||
|
//
|
||||||
|
// Returns `true` on success and `false` otherwise. At the point this
|
||||||
|
// method is called, option declaration is already assumed to be detected
|
||||||
|
// and any failure implies parsing error (ending in failed `Command.CallData`).
|
||||||
|
private final function bool ParseShortOption() {
|
||||||
|
local int i;
|
||||||
|
local bool pickedOptionWithParameters;
|
||||||
|
local MutableText optionsList;
|
||||||
|
|
||||||
|
commandParser.MUntil(optionsList,, true);
|
||||||
|
if (!commandParser.Ok()) {
|
||||||
|
optionsList.FreeSelf();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i = 0; i < optionsList.GetLength(); i += 1) {
|
||||||
|
if (nextResult.parsingError != CET_None) break;
|
||||||
|
pickedOptionWithParameters =
|
||||||
|
AddOptionByCharacter(
|
||||||
|
optionsList.GetCharacter(i),
|
||||||
|
optionsList,
|
||||||
|
pickedOptionWithParameters)
|
||||||
|
|| pickedOptionWithParameters;
|
||||||
|
}
|
||||||
|
optionsList.FreeSelf();
|
||||||
|
return (nextResult.parsingError == CET_None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `commandParser` and `nextResult` are not `none`.
|
||||||
|
//
|
||||||
|
// Auxiliary method that adds option by it's short version's character
|
||||||
|
// `optionCharacter`.
|
||||||
|
//
|
||||||
|
// It also accepts `optionSourceList` that describes short option expression
|
||||||
|
// (e.g. "-rtV") from
|
||||||
|
// which it originated for error reporting and `forbidOptionWithParameters`
|
||||||
|
// that, when set to `true`, forces this method to cause the
|
||||||
|
// `CET_MultipleOptionsWithParams` error if new option has non-empty parameters.
|
||||||
|
//
|
||||||
|
// Method returns `true` if added option had non-empty parameters and `false`
|
||||||
|
// otherwise.
|
||||||
|
//
|
||||||
|
// Any parsing failure inside this method always causes
|
||||||
|
// `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()`
|
||||||
|
// to check if method has failed.
|
||||||
|
private final function bool AddOptionByCharacter(
|
||||||
|
BaseText.Character optionCharacter,
|
||||||
|
BaseText optionSourceList,
|
||||||
|
bool forbidOptionWithParameters
|
||||||
|
) {
|
||||||
|
local int i;
|
||||||
|
local bool optionHasParameters;
|
||||||
|
|
||||||
|
// Prevent same option appearing twice
|
||||||
|
for (i = 0; i < usedOptions.length; i += 1) {
|
||||||
|
if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName)) {
|
||||||
|
DeclareError(CET_RepeatedOption, usedOptions[i].longName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it's a new option - look it up in all available options
|
||||||
|
for (i = 0; i < availableOptions.length; i += 1) {
|
||||||
|
if (!_.text.AreEqual(optionCharacter, availableOptions[i].shortName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
usedOptions[usedOptions.length] = availableOptions[i];
|
||||||
|
optionHasParameters = (availableOptions[i].required.length > 0
|
||||||
|
|| availableOptions[i].optional.length > 0);
|
||||||
|
// Enforce `forbidOptionWithParameters` flag restriction
|
||||||
|
if (optionHasParameters && forbidOptionWithParameters) {
|
||||||
|
DeclareError(CET_MultipleOptionsWithParams, optionSourceList);
|
||||||
|
return optionHasParameters;
|
||||||
|
}
|
||||||
|
// Parse parameters (even if they are empty) and bail
|
||||||
|
commandParser.Skip();
|
||||||
|
ParseOptionParameters(availableOptions[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i >= availableOptions.length) {
|
||||||
|
DeclareError(CET_UnknownShortOption);
|
||||||
|
}
|
||||||
|
return optionHasParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auxiliary method for parsing option's parameters (including empty ones).
|
||||||
|
// Automatically fills `nextResult` with parsed parameters (or `none` if option
|
||||||
|
// has no parameters).
|
||||||
|
// Assumes `commandParser` and `nextResult` are not `none`.
|
||||||
|
private final function bool ParseOptionParameters(Command.Option pickedOption) {
|
||||||
|
local HashTable optionParameters;
|
||||||
|
|
||||||
|
// If we are already parsing other option's parameters and did not finish
|
||||||
|
// parsing all required ones - we cannot start another option
|
||||||
|
if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) {
|
||||||
|
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) {
|
||||||
|
nextResult.options.SetItem(pickedOption.longName, none);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
currentTargetIsOption = true;
|
||||||
|
targetOption = pickedOption;
|
||||||
|
optionParameters = ParseParameterArrays(
|
||||||
|
pickedOption.required,
|
||||||
|
pickedOption.optional);
|
||||||
|
currentTargetIsOption = false;
|
||||||
|
if (commandParser.Ok()) {
|
||||||
|
nextResult.options.SetItem(pickedOption.longName, optionParameters);
|
||||||
|
_.memory.Free(optionParameters);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_.memory.Free(optionParameters);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
booleanTrueEquivalents(0) = "true"
|
||||||
|
booleanTrueEquivalents(1) = "enable"
|
||||||
|
booleanTrueEquivalents(2) = "on"
|
||||||
|
booleanTrueEquivalents(3) = "yes"
|
||||||
|
booleanFalseEquivalents(0) = "false"
|
||||||
|
booleanFalseEquivalents(1) = "disable"
|
||||||
|
booleanFalseEquivalents(2) = "off"
|
||||||
|
booleanFalseEquivalents(3) = "no"
|
||||||
|
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.")
|
||||||
|
}
|
||||||
66
kf_sources/AcediaCore/Classes/CommandPermissions.uc
Normal file
66
kf_sources/AcediaCore/Classes/CommandPermissions.uc
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandPermissions extends AcediaConfig
|
||||||
|
perobjectconfig
|
||||||
|
config(AcediaCommands)
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
var public config array<string> forbiddenSubCommands;
|
||||||
|
|
||||||
|
protected function HashTable ToData() {
|
||||||
|
local int i;
|
||||||
|
local HashTable data;
|
||||||
|
local ArrayList forbiddenList;
|
||||||
|
|
||||||
|
data = _.collections.EmptyHashTable();
|
||||||
|
forbiddenList = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < forbiddenSubCommands.length; i += 1) {
|
||||||
|
forbiddenList.AddString(Locs(forbiddenSubCommands[i]));
|
||||||
|
}
|
||||||
|
data.SetItem(P("forbiddenSubCommands"), forbiddenList);
|
||||||
|
_.memory.Free(forbiddenList);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function FromData(HashTable source) {
|
||||||
|
local int i;
|
||||||
|
local ArrayList forbiddenList;
|
||||||
|
|
||||||
|
if (source == none) return;
|
||||||
|
forbiddenList = source.GetArrayList(P("forbiddenSubCommands"));
|
||||||
|
if (forbiddenList == none) return;
|
||||||
|
|
||||||
|
forbiddenSubCommands.length = 0;
|
||||||
|
for (i = 0; i < forbiddenList.GetLength(); i += 1) {
|
||||||
|
forbiddenSubCommands[i] = forbiddenList.GetString(i);
|
||||||
|
}
|
||||||
|
_.memory.Free(forbiddenList);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function DefaultIt() {
|
||||||
|
forbiddenSubCommands.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
configName = "AcediaCommands"
|
||||||
|
supportsDataConversion = true
|
||||||
|
}
|
||||||
81
kf_sources/AcediaCore/Classes/CommandRegistrationJob.uc
Normal file
81
kf_sources/AcediaCore/Classes/CommandRegistrationJob.uc
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandRegistrationJob extends SchedulerJob
|
||||||
|
dependson(CommandAPI);
|
||||||
|
|
||||||
|
var private CommandAPI.AsyncTask nextItem;
|
||||||
|
|
||||||
|
// Expecting 300 units of work, this gives us registering 20 commands per tick
|
||||||
|
const ADDING_COMMAND_COST = 15;
|
||||||
|
// Adding voting option is approximately the same as adding a command's
|
||||||
|
// single sub-command - we'll estimate it as 1/3rd of the full value
|
||||||
|
const ADDING_VOTING_COST = 5;
|
||||||
|
// Authorizing is relatively cheap, whether it's commands or voting
|
||||||
|
const AUTHORIZING_COST = 1;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
nextItem = _.commands._popPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
|
||||||
|
nextItem.entityClass = none;
|
||||||
|
nextItem.entityName = none;
|
||||||
|
nextItem.userGroup = none;
|
||||||
|
nextItem.configName = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bool IsCompleted() {
|
||||||
|
return (nextItem.entityName == none);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function DoWork(int allottedWorkUnits) {
|
||||||
|
while (allottedWorkUnits > 0 && nextItem.entityName != none) {
|
||||||
|
if (nextItem.type == CAJT_AddCommand) {
|
||||||
|
allottedWorkUnits -= ADDING_COMMAND_COST;
|
||||||
|
_.commands.AddCommand(class<Command>(nextItem.entityClass), nextItem.entityName);
|
||||||
|
_.memory.Free(nextItem.entityName);
|
||||||
|
} else if (nextItem.type == CAJT_AddVoting) {
|
||||||
|
allottedWorkUnits -= ADDING_VOTING_COST;
|
||||||
|
_.commands.AddVoting(class<Voting>(nextItem.entityClass), nextItem.entityName);
|
||||||
|
_.memory.Free(nextItem.entityName);
|
||||||
|
} else if (nextItem.type == CAJT_AuthorizeCommand) {
|
||||||
|
allottedWorkUnits -= AUTHORIZING_COST;
|
||||||
|
_.commands.AuthorizeCommandUsage(
|
||||||
|
nextItem.entityName,
|
||||||
|
nextItem.userGroup,
|
||||||
|
nextItem.configName);
|
||||||
|
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
|
||||||
|
} else /*if (nextItem.type == CAJT_AuthorizeVoting)*/ {
|
||||||
|
allottedWorkUnits -= AUTHORIZING_COST;
|
||||||
|
_.commands.AuthorizeVotingUsage(
|
||||||
|
nextItem.entityName,
|
||||||
|
nextItem.userGroup,
|
||||||
|
nextItem.configName);
|
||||||
|
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
|
||||||
|
}
|
||||||
|
nextItem = _.commands._popPending();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
230
kf_sources/AcediaCore/Classes/Commands.uc
Normal file
230
kf_sources/AcediaCore/Classes/Commands.uc
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Commands extends FeatureConfig
|
||||||
|
perobjectconfig
|
||||||
|
config(AcediaCommands);
|
||||||
|
|
||||||
|
/// Auxiliary struct for describing adding a particular command set to
|
||||||
|
/// a particular group of users.
|
||||||
|
struct CommandSetGroupPair {
|
||||||
|
/// Name of the command set to add
|
||||||
|
var public string name;
|
||||||
|
/// Name of the group, for which to add this set
|
||||||
|
var public string for;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Auxiliary struct for describing a rule to rename a particular command for
|
||||||
|
/// compatibility reasons.
|
||||||
|
struct RenamingRulePair {
|
||||||
|
/// Command class to rename
|
||||||
|
var public class<AcediaObject> rename;
|
||||||
|
/// Name to use for that class
|
||||||
|
var public string to;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Setting this to `true` enables players to input commands with "mutate"
|
||||||
|
/// console command.
|
||||||
|
/// Default is `true`.
|
||||||
|
var public config bool useMutateInput;
|
||||||
|
/// Setting this to `true` enables players to input commands right in the chat
|
||||||
|
/// by prepending them with [`chatCommandPrefix`].
|
||||||
|
/// Default is `true`.
|
||||||
|
var public config bool useChatInput;
|
||||||
|
/// Chat messages, prepended by this prefix will be treated as commands.
|
||||||
|
/// Default is "!". Empty values are also treated as "!".
|
||||||
|
var public config string chatCommandPrefix;
|
||||||
|
/// Allows to specify which user groups are used in determining command/votings
|
||||||
|
/// permission.
|
||||||
|
/// They must be specified in the order of importance: from the group with
|
||||||
|
/// highest level of permissions to the lowest. When determining player's
|
||||||
|
/// permission to use a certain command/voting, his group with the highest
|
||||||
|
/// available permissions will be used.
|
||||||
|
var public config array<string> commandGroup;
|
||||||
|
/// Add a specified `CommandList` to the specified user group
|
||||||
|
var public config array<CommandSetGroupPair> addCommandList;
|
||||||
|
/// Allows to specify a name for a certain command class
|
||||||
|
///
|
||||||
|
/// NOTE:By default command choses that name by itself and its not recommended
|
||||||
|
/// to override it. You should only use this setting in case there is naming
|
||||||
|
/// conflict between commands from different packages.
|
||||||
|
var public config array<RenamingRulePair> renamingRule;
|
||||||
|
/// Allows to specify a name for a certain voting class
|
||||||
|
///
|
||||||
|
/// NOTE:By default voting choses that name by itself and its not recommended
|
||||||
|
/// to override it. You should only use this setting in case there is naming
|
||||||
|
/// conflict between votings from different packages.
|
||||||
|
var public config array<RenamingRulePair> votingRenamingRule;
|
||||||
|
|
||||||
|
protected function HashTable ToData() {
|
||||||
|
local int i;
|
||||||
|
local HashTable data;
|
||||||
|
local ArrayList innerList;
|
||||||
|
local HashTable innerPair;
|
||||||
|
|
||||||
|
data = __().collections.EmptyHashTable();
|
||||||
|
data.SetBool(P("useChatInput"), useChatInput, true);
|
||||||
|
data.SetBool(P("useMutateInput"), useMutateInput, true);
|
||||||
|
data.SetString(P("chatCommandPrefix"), chatCommandPrefix);
|
||||||
|
|
||||||
|
// Serialize `commandGroup`
|
||||||
|
innerList = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < commandGroup.length; i += 1) {
|
||||||
|
innerList.AddString(commandGroup[i]);
|
||||||
|
}
|
||||||
|
data.SetItem(P("commandGroups"), innerList);
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
|
||||||
|
// Serialize `addCommandSet`
|
||||||
|
innerList = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < addCommandList.length; i += 1) {
|
||||||
|
innerPair = _.collections.EmptyHashTable();
|
||||||
|
innerPair.SetString(P("name"), addCommandList[i].name);
|
||||||
|
innerPair.SetString(P("for"), addCommandList[i].for);
|
||||||
|
innerList.AddItem(innerPair);
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
data.SetItem(P("commandSets"), innerList);
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
|
||||||
|
// Serialize `renamingRule`
|
||||||
|
innerList = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < renamingRule.length; i += 1) {
|
||||||
|
innerPair = _.collections.EmptyHashTable();
|
||||||
|
innerPair.SetString(P("rename"), string(renamingRule[i].rename));
|
||||||
|
innerPair.SetString(P("to"), renamingRule[i].to);
|
||||||
|
innerList.AddItem(innerPair);
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
data.SetItem(P("renamingRules"), innerList);
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
|
||||||
|
// Serialize `votingRenamingRule`
|
||||||
|
innerList = _.collections.EmptyArrayList();
|
||||||
|
for (i = 0; i < votingRenamingRule.length; i += 1) {
|
||||||
|
innerPair = _.collections.EmptyHashTable();
|
||||||
|
innerPair.SetString(P("rename"), string(votingRenamingRule[i].rename));
|
||||||
|
innerPair.SetString(P("to"), votingRenamingRule[i].to);
|
||||||
|
innerList.AddItem(innerPair);
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
data.SetItem(P("votingRenamingRules"), innerList);
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function FromData(HashTable source) {
|
||||||
|
local int i;
|
||||||
|
local ArrayList innerList;
|
||||||
|
local HashTable innerPair;
|
||||||
|
local CommandSetGroupPair nextCommandSetGroupPair;
|
||||||
|
local RenamingRulePair nextRenamingRule;
|
||||||
|
local class<AcediaObject> nextClass;
|
||||||
|
|
||||||
|
if (source == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
useChatInput = source.GetBool(P("useChatInput"));
|
||||||
|
useMutateInput = source.GetBool(P("useMutateInput"));
|
||||||
|
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!");
|
||||||
|
|
||||||
|
// De-serialize `commandGroup`
|
||||||
|
commandGroup.length = 0;
|
||||||
|
innerList = source.GetArrayList(P("commandGroups"));
|
||||||
|
if (innerList != none) {
|
||||||
|
for (i = 0; i < commandGroup.length; i += 1) {
|
||||||
|
commandGroup[i] = innerList.GetString(i);
|
||||||
|
}
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-serialize `addCommandSet`
|
||||||
|
addCommandList.length = 0;
|
||||||
|
innerList = source.GetArrayList(P("commandSets"));
|
||||||
|
if (innerList != none) {
|
||||||
|
for (i = 0; i < addCommandList.length; i += 1) {
|
||||||
|
innerPair = innerList.GetHashTable(i);
|
||||||
|
if (innerPair != none) {
|
||||||
|
nextCommandSetGroupPair.name = innerPair.GetString(P("name"));
|
||||||
|
nextCommandSetGroupPair.for = innerPair.GetString(P("for"));
|
||||||
|
addCommandList[addCommandList.length] = nextCommandSetGroupPair;
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-serialize `renamingRule`
|
||||||
|
renamingRule.length = 0;
|
||||||
|
innerList = source.GetArrayList(P("renamingRules"));
|
||||||
|
if (innerList != none) {
|
||||||
|
for (i = 0; i < renamingRule.length; i += 1) {
|
||||||
|
innerPair = innerList.GetHashTable(i);
|
||||||
|
if (innerPair != none) {
|
||||||
|
nextClass =
|
||||||
|
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
|
||||||
|
nextRenamingRule.rename = nextClass;
|
||||||
|
nextRenamingRule.to = innerPair.GetString(P("to"));
|
||||||
|
renamingRule[renamingRule.length] = nextRenamingRule;
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-serialize `votingRenamingRule`
|
||||||
|
votingRenamingRule.length = 0;
|
||||||
|
innerList = source.GetArrayList(P("votingRenamingRules"));
|
||||||
|
if (innerList != none) {
|
||||||
|
for (i = 0; i < votingRenamingRule.length; i += 1) {
|
||||||
|
innerPair = innerList.GetHashTable(i);
|
||||||
|
if (innerPair != none) {
|
||||||
|
nextClass =
|
||||||
|
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
|
||||||
|
nextRenamingRule.rename = nextClass;
|
||||||
|
nextRenamingRule.to = innerPair.GetString(P("to"));
|
||||||
|
votingRenamingRule[votingRenamingRule.length] = nextRenamingRule;
|
||||||
|
_.memory.Free(innerPair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_.memory.Free(innerList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function DefaultIt() {
|
||||||
|
local CommandSetGroupPair defaultPair;
|
||||||
|
|
||||||
|
useChatInput = true;
|
||||||
|
useMutateInput = true;
|
||||||
|
chatCommandPrefix = "!";
|
||||||
|
commandGroup[0] = "admin";
|
||||||
|
commandGroup[1] = "moderator";
|
||||||
|
commandGroup[2] = "trusted";
|
||||||
|
addCommandList.length = 0;
|
||||||
|
defaultPair.name = "default";
|
||||||
|
defaultPair.for = "all";
|
||||||
|
addCommandList[0] = defaultPair;
|
||||||
|
renamingRule.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
configName = "AcediaCommands"
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnCommandAdded_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function bool Emit(class<Command> addedClass, Text usedName) {
|
||||||
|
local Slot nextSlot;
|
||||||
|
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none) {
|
||||||
|
CommandsAPI_OnCommandAdded_Slot(nextSlot).connect(addedClass, usedName);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
relatedSlotClass = class'CommandsAPI_OnCommandAdded_Slot'
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnCommandAdded_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate connect(class<Command> addedClass, Text usedName) {
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnCommandRemoved_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function bool Emit(class<Command> removedClass) {
|
||||||
|
local Slot nextSlot;
|
||||||
|
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none) {
|
||||||
|
CommandsAPI_OnCommandRemoved_Slot(nextSlot).connect(removedClass);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
relatedSlotClass = class'CommandsAPI_OnCommandRemoved_Slot'
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnCommandRemoved_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate connect(class<Command> removedClass) {
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnVotingAdded_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function bool Emit(class<Voting> addedClass, Text usedName) {
|
||||||
|
local Slot nextSlot;
|
||||||
|
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none) {
|
||||||
|
CommandsAPI_OnVotingAdded_Slot(nextSlot).connect(addedClass, usedName);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
relatedSlotClass = class'CommandsAPI_OnVotingAdded_Slot'
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnVotingAdded_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate connect(class<Voting> addedClass, Text usedName) {
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnVotingEnded_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function bool Emit(bool success, HashTable arguments) {
|
||||||
|
local Slot nextSlot;
|
||||||
|
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none) {
|
||||||
|
CommandsAPI_OnVotingEnded_Slot(nextSlot).connect(success, arguments);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
relatedSlotClass = class'CommandsAPI_OnVotingEnded_Slot'
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnVotingEnded_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate connect(bool success, HashTable arguments) {
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnVotingRemoved_Signal extends Signal;
|
||||||
|
|
||||||
|
public final function bool Emit(class<Voting> removedClass) {
|
||||||
|
local Slot nextSlot;
|
||||||
|
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none) {
|
||||||
|
CommandsAPI_OnVotingRemoved_Slot(nextSlot).connect(removedClass);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
relatedSlotClass = class'CommandsAPI_OnVotingRemoved_Slot'
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsAPI_OnVotingRemoved_Slot extends Slot;
|
||||||
|
|
||||||
|
delegate connect(class<Voting> removedClass) {
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
142
kf_sources/AcediaCore/Classes/CommandsTool.uc
Normal file
142
kf_sources/AcediaCore/Classes/CommandsTool.uc
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CommandsTool extends CmdItemsTool;
|
||||||
|
|
||||||
|
//! This is a base class for auxiliary objects that will be used for storing
|
||||||
|
//! named [`Command`] instances.
|
||||||
|
//!
|
||||||
|
//! This storage class allows for efficient manipulation and retrieval of
|
||||||
|
//! [`Command`]s, along with information about what use groups were authorized
|
||||||
|
//! to use them.
|
||||||
|
//!
|
||||||
|
//! Additionally, this tool allows for efficient fetching of commands that
|
||||||
|
//! belong to a particular *command group*.
|
||||||
|
|
||||||
|
/// [`HashTable`] that maps a command group name to a set of command names that
|
||||||
|
/// belong to it.
|
||||||
|
var private HashTable groupedCommands;
|
||||||
|
|
||||||
|
protected function Constructor() {
|
||||||
|
super.Constructor();
|
||||||
|
groupedCommands = _.collections.EmptyHashTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer() {
|
||||||
|
super.Finalizer();
|
||||||
|
_.memory.Free(groupedCommands);
|
||||||
|
groupedCommands = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all known command groups' names.
|
||||||
|
public final function array<Text> GetGroupsNames() {
|
||||||
|
local array<Text> emptyResult;
|
||||||
|
|
||||||
|
if (groupedCommands != none) {
|
||||||
|
return groupedCommands.GetTextKeys();
|
||||||
|
}
|
||||||
|
return emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns array of names of all available commands belonging to the specified
|
||||||
|
/// group.
|
||||||
|
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) {
|
||||||
|
local int i;
|
||||||
|
local ArrayList commandNamesArray;
|
||||||
|
local array<Text> result;
|
||||||
|
|
||||||
|
if (groupedCommands == none) return result;
|
||||||
|
commandNamesArray = groupedCommands.GetArrayList(groupName);
|
||||||
|
if (commandNamesArray == none) return result;
|
||||||
|
|
||||||
|
for (i = 0; i < commandNamesArray.GetLength(); i += 1) {
|
||||||
|
result[result.length] = commandNamesArray.GetText(i);
|
||||||
|
}
|
||||||
|
_.memory.Free(commandNamesArray);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function ItemCard MakeCard(class<AcediaObject> commandClass, BaseText itemName) {
|
||||||
|
local Command newCommandInstance;
|
||||||
|
local ItemCard newCard;
|
||||||
|
local Text commandGroup;
|
||||||
|
|
||||||
|
if (class<Command>(commandClass) != none) {
|
||||||
|
newCommandInstance = Command(_.memory.Allocate(commandClass, true));
|
||||||
|
newCommandInstance.Initialize(itemName);
|
||||||
|
newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
|
||||||
|
newCard.InitializeWithInstance(newCommandInstance);
|
||||||
|
|
||||||
|
// Guaranteed to be lower case (keys of [`HashTable`])
|
||||||
|
if (itemName != none) {
|
||||||
|
itemName = itemName.LowerCopy();
|
||||||
|
} else {
|
||||||
|
itemName = newCommandInstance.GetPreferredName();
|
||||||
|
}
|
||||||
|
commandGroup = newCommandInstance.GetGroupName();
|
||||||
|
AssociateGroupAndName(commandGroup, itemName);
|
||||||
|
_.memory.Free3(newCommandInstance, itemName, commandGroup);
|
||||||
|
}
|
||||||
|
return newCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function DiscardCard(ItemCard toDiscard) {
|
||||||
|
local Text groupKey, commandName;
|
||||||
|
local Command storedCommand;
|
||||||
|
local ArrayList listOfCommands;
|
||||||
|
|
||||||
|
if (toDiscard == none) return;
|
||||||
|
// Guaranteed to store a [`Command`]
|
||||||
|
storedCommand = Command(toDiscard.GetItem());
|
||||||
|
if (storedCommand == none) return;
|
||||||
|
|
||||||
|
// Guaranteed to be stored in a lower case
|
||||||
|
commandName = storedCommand.GetName();
|
||||||
|
listOfCommands = groupedCommands.GetArrayList(groupKey);
|
||||||
|
if (listOfCommands != none && commandName != none) {
|
||||||
|
listOfCommands.RemoveItem(commandName);
|
||||||
|
}
|
||||||
|
_.memory.Free2(commandName, storedCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect both arguments to be not `none`.
|
||||||
|
// Expect both arguments to be lower-case.
|
||||||
|
private final function AssociateGroupAndName(BaseText groupKey, BaseText commandName) {
|
||||||
|
local ArrayList listOfCommands;
|
||||||
|
|
||||||
|
if (groupedCommands != none) {
|
||||||
|
listOfCommands = groupedCommands.GetArrayList(groupKey);
|
||||||
|
if (listOfCommands == none) {
|
||||||
|
listOfCommands = _.collections.EmptyArrayList();
|
||||||
|
}
|
||||||
|
if (listOfCommands.Find(commandName) < 0) {
|
||||||
|
// `< 0` means not found
|
||||||
|
listOfCommands.AddItem(commandName);
|
||||||
|
}
|
||||||
|
// Set `listOfCommands` in case we've just created that array.
|
||||||
|
// Won't do anything if it is already recorded there.
|
||||||
|
groupedCommands.SetItem(groupKey, listOfCommands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
ruleBaseClass = class'Command';
|
||||||
|
}
|
||||||
708
kf_sources/AcediaCore/Classes/Commands_Feature.uc
Normal file
708
kf_sources/AcediaCore/Classes/Commands_Feature.uc
Normal file
@ -0,0 +1,708 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Commands_Feature extends Feature
|
||||||
|
dependson(CommandAPI)
|
||||||
|
dependson(Commands);
|
||||||
|
|
||||||
|
//! This feature manages commands that automatically parse their arguments into standard Acedia
|
||||||
|
//! collections.
|
||||||
|
//!
|
||||||
|
//! # Implementation
|
||||||
|
//!
|
||||||
|
//! Implementation is simple: calling a method `RegisterCommand()` adds
|
||||||
|
//! command into two caches `registeredCommands` for obtaining registered
|
||||||
|
//! commands by name and `groupedCommands` for obtaining arrays of commands by
|
||||||
|
//! their group name. These arrays are used for providing methods for fetching
|
||||||
|
//! arrays of commands and obtaining pre-allocated `Command` instances by their
|
||||||
|
//! name.
|
||||||
|
//! Depending on settings, this feature also connects to corresponding
|
||||||
|
//! signals for catching "mutate"/chat input, then it checks user-specified name
|
||||||
|
//! for being an alias and picks correct command from `registeredCommands`.
|
||||||
|
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
|
||||||
|
//! enforces connecting to the "mutate" input.
|
||||||
|
|
||||||
|
/// Auxiliary struct for passing name of the command to call with pre-specified
|
||||||
|
/// sub-command name.
|
||||||
|
///
|
||||||
|
/// Normally sub-command name is parsed by the command itself, however command
|
||||||
|
/// aliases can try to enforce one.
|
||||||
|
struct CommandCallPair {
|
||||||
|
var MutableText commandName;
|
||||||
|
/// Not `none` in case it is enforced by an alias
|
||||||
|
var MutableText subCommandName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Auxiliary struct that stores all the information needed to load
|
||||||
|
/// a certain command
|
||||||
|
struct EntityLoadInfo {
|
||||||
|
/// Command class to load.
|
||||||
|
var public class<AcediaObject> entityClass;
|
||||||
|
/// Name to load that command class under.
|
||||||
|
var public Text name;
|
||||||
|
/// Groups that are authorized to use that command.
|
||||||
|
var public array<Text> authorizedGroups;
|
||||||
|
/// Groups that are authorized to use that command.
|
||||||
|
var public array<Text> groupsConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Auxiliary struct for describing adding a particular command set to
|
||||||
|
/// a particular group of users.
|
||||||
|
struct CommandListGroupPair {
|
||||||
|
/// Name of the command set to add
|
||||||
|
var public Text commandListName;
|
||||||
|
/// Name of the group, for which to add this set
|
||||||
|
var public Text permissionGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Auxiliary struct for describing a rule to rename a particular command for
|
||||||
|
/// compatibility reasons.
|
||||||
|
struct RenamingRulePair {
|
||||||
|
/// Command class to rename
|
||||||
|
var public class<AcediaObject> class;
|
||||||
|
/// Name to use for that class
|
||||||
|
var public Text newName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Tools that provide functionality of managing registered commands and votings
|
||||||
|
var private CommandAPI.CommandFeatureTools tools;
|
||||||
|
|
||||||
|
/// Delimiters that always separate command name from it's parameters
|
||||||
|
var private array<Text> commandDelimiters;
|
||||||
|
|
||||||
|
/// When this flag is set to true, mutate input becomes available despite
|
||||||
|
/// [`useMutateInput`] flag to allow to unlock server in case of an error
|
||||||
|
var private bool emergencyEnabledMutate;
|
||||||
|
|
||||||
|
var private /*config*/ bool useChatInput;
|
||||||
|
var private /*config*/ bool useMutateInput;
|
||||||
|
var private /*config*/ Text chatCommandPrefix;
|
||||||
|
var public /*config*/ array<string> commandGroup;
|
||||||
|
var public /*config*/ array<Commands.CommandSetGroupPair> addCommandSet;
|
||||||
|
var public /*config*/ array<Commands.RenamingRulePair> renamingRule;
|
||||||
|
var public /*config*/ array<Commands.RenamingRulePair> votingRenamingRule;
|
||||||
|
|
||||||
|
// Converted version of `commandGroup`
|
||||||
|
var private array<Text> permissionGroupOrder;
|
||||||
|
/// Converted version of `addCommandSet`
|
||||||
|
var private array<CommandListGroupPair> usedCommandLists;
|
||||||
|
/// Converted version of `renamingRule` and `votingRenamingRule`
|
||||||
|
var private array<RenamingRulePair> commandRenamingRules;
|
||||||
|
var private array<RenamingRulePair> votingRenamingRules;
|
||||||
|
// Name, under which `ACommandHelp` is registered
|
||||||
|
var private Text helpCommandName;
|
||||||
|
|
||||||
|
var LoggerAPI.Definition errServerAPIUnavailable, warnDuplicateRenaming, warnNoCommandList;
|
||||||
|
var LoggerAPI.Definition infoCommandAdded, infoVotingAdded;
|
||||||
|
|
||||||
|
protected function OnEnabled() {
|
||||||
|
helpCommandName = P("help");
|
||||||
|
// Macro selector
|
||||||
|
commandDelimiters[0] = _.text.FromString("@");
|
||||||
|
// Key selector
|
||||||
|
commandDelimiters[1] = _.text.FromString("#");
|
||||||
|
// Player array (possibly JSON array)
|
||||||
|
commandDelimiters[2] = _.text.FromString("[");
|
||||||
|
// Negation of the selector
|
||||||
|
// NOT the same thing as default command prefix in chat
|
||||||
|
commandDelimiters[3] = _.text.FromString("!");
|
||||||
|
if (useChatInput) {
|
||||||
|
_.chat.OnMessage(self).connect = HandleCommands;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_.chat.OnMessage(self).Disconnect();
|
||||||
|
}
|
||||||
|
if (useMutateInput || emergencyEnabledMutate) {
|
||||||
|
if (__server() != none) {
|
||||||
|
__server().unreal.mutator.OnMutate(self).connect = HandleMutate;
|
||||||
|
} else {
|
||||||
|
_.logger.Auto(errServerAPIUnavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoadConfigArrays();
|
||||||
|
// `SetPermissionGroupOrder()` must be called *after* loading configs
|
||||||
|
tools.commands = CommandsTool(_.memory.Allocate(class'CommandsTool'));
|
||||||
|
tools.votings = VotingsTool(_.memory.Allocate(class'VotingsTool'));
|
||||||
|
tools.commands.SetPermissionGroupOrder(permissionGroupOrder);
|
||||||
|
tools.votings.SetPermissionGroupOrder(permissionGroupOrder);
|
||||||
|
_.commands._reloadFeature();
|
||||||
|
// Uses `CommandAPI`, so must be done after `_reloadFeature()` call
|
||||||
|
LoadCommands();
|
||||||
|
LoadVotings();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function OnDisabled() {
|
||||||
|
if (useChatInput) {
|
||||||
|
_.chat.OnMessage(self).Disconnect();
|
||||||
|
}
|
||||||
|
if (useMutateInput && __server() != none) {
|
||||||
|
__server().unreal.mutator.OnMutate(self).Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
useChatInput = false;
|
||||||
|
useMutateInput = false;
|
||||||
|
_.memory.Free3(tools.commands, tools.votings, chatCommandPrefix);
|
||||||
|
tools.commands = none;
|
||||||
|
tools.votings = none;
|
||||||
|
chatCommandPrefix = none;
|
||||||
|
|
||||||
|
_.memory.FreeMany(commandDelimiters);
|
||||||
|
commandDelimiters.length = 0;
|
||||||
|
|
||||||
|
_.memory.FreeMany(permissionGroupOrder);
|
||||||
|
permissionGroupOrder.length = 0;
|
||||||
|
|
||||||
|
FreeUsedCommandSets();
|
||||||
|
FreeRenamingRules();
|
||||||
|
|
||||||
|
_.commands._reloadFeature();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function SwapConfig(FeatureConfig config) {
|
||||||
|
local Commands newConfig;
|
||||||
|
|
||||||
|
newConfig = Commands(config);
|
||||||
|
if (newConfig == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_.memory.Free(chatCommandPrefix);
|
||||||
|
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
|
||||||
|
useChatInput = newConfig.useChatInput;
|
||||||
|
useMutateInput = newConfig.useMutateInput;
|
||||||
|
commandGroup = newConfig.commandGroup;
|
||||||
|
addCommandSet = newConfig.addCommandList;
|
||||||
|
renamingRule = newConfig.renamingRule;
|
||||||
|
votingRenamingRule = newConfig.votingRenamingRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method allows to forcefully enable `Command_Feature` along with
|
||||||
|
/// "mutate" input in case something goes wrong.
|
||||||
|
///
|
||||||
|
/// `Command_Feature` is a critical command to have running on your server and,
|
||||||
|
/// if disabled by accident, there will be no way of starting it again without
|
||||||
|
/// restarting the level or even editing configs.
|
||||||
|
public final static function EmergencyEnable() {
|
||||||
|
local bool noWayToInputCommands;
|
||||||
|
local Text autoConfig;
|
||||||
|
local Commands_Feature feature;
|
||||||
|
|
||||||
|
if (!IsEnabled()) {
|
||||||
|
autoConfig = GetAutoEnabledConfig();
|
||||||
|
EnableMe(autoConfig);
|
||||||
|
__().memory.Free(autoConfig);
|
||||||
|
}
|
||||||
|
feature = Commands_Feature(GetEnabledInstance());
|
||||||
|
noWayToInputCommands = !feature.emergencyEnabledMutate
|
||||||
|
&& !feature.IsUsingMutateInput()
|
||||||
|
&& !feature.IsUsingChatInput();
|
||||||
|
if (noWayToInputCommands) {
|
||||||
|
default.emergencyEnabledMutate = true;
|
||||||
|
feature.emergencyEnabledMutate = true;
|
||||||
|
if (__server() != none) {
|
||||||
|
__server().unreal.mutator.OnMutate(feature).connect = HandleMutate;
|
||||||
|
} else {
|
||||||
|
__().logger.Auto(default.errServerAPIUnavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if `Commands_Feature` currently uses chat as input.
|
||||||
|
///
|
||||||
|
/// If `Commands_Feature` is not enabled, then it does not use anything
|
||||||
|
/// as input.
|
||||||
|
public final static function bool IsUsingChatInput() {
|
||||||
|
local Commands_Feature instance;
|
||||||
|
|
||||||
|
instance = Commands_Feature(GetEnabledInstance());
|
||||||
|
if (instance != none) {
|
||||||
|
return instance.useChatInput;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if `Commands_Feature` currently uses mutate command as input.
|
||||||
|
///
|
||||||
|
/// If `Commands_Feature` is not enabled, then it does not use anything
|
||||||
|
/// as input.
|
||||||
|
public final static function bool IsUsingMutateInput() {
|
||||||
|
local Commands_Feature instance;
|
||||||
|
|
||||||
|
instance = Commands_Feature(GetEnabledInstance());
|
||||||
|
if (instance != none) {
|
||||||
|
return instance.useMutateInput;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns prefix that will indicate that chat message is intended to be
|
||||||
|
/// a command. By default "!".
|
||||||
|
///
|
||||||
|
/// If `Commands_Feature` is disabled, always returns `none`.
|
||||||
|
public final static function Text GetChatPrefix() {
|
||||||
|
local Commands_Feature instance;
|
||||||
|
|
||||||
|
instance = Commands_Feature(GetEnabledInstance());
|
||||||
|
if (instance != none && instance.chatCommandPrefix != none) {
|
||||||
|
return instance.chatCommandPrefix.Copy();
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns name, under which [`ACommandHelp`] is registered.
|
||||||
|
///
|
||||||
|
/// If `Commands_Feature` is disabled, always returns `none`.
|
||||||
|
public final static function Text GetHelpCommandName() {
|
||||||
|
local Commands_Feature instance;
|
||||||
|
|
||||||
|
instance = Commands_Feature(GetEnabledInstance());
|
||||||
|
if (instance != none && instance.helpCommandName != none) {
|
||||||
|
return instance.helpCommandName.Copy();
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes command based on the input.
|
||||||
|
///
|
||||||
|
/// Takes [`commandLine`] as input with command's call, finds appropriate
|
||||||
|
/// registered command instance and executes it with parameters specified in
|
||||||
|
/// the [`commandLine`].
|
||||||
|
///
|
||||||
|
/// [`callerPlayer`] has to be specified and represents instigator of this
|
||||||
|
/// command that will receive appropriate result/error messages.
|
||||||
|
///
|
||||||
|
/// Returns `true` iff command was successfully executed.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Doesn't log any errors, but can complain about errors in name or parameters
|
||||||
|
/// to the [`callerPlayer`]
|
||||||
|
public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
|
||||||
|
local bool result;
|
||||||
|
local Parser wrapper;
|
||||||
|
|
||||||
|
if (input == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
wrapper = input.Parse();
|
||||||
|
result = HandleInputWith(wrapper, callerPlayer);
|
||||||
|
wrapper.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes command based on the input.
|
||||||
|
///
|
||||||
|
/// Takes [`commandLine`] as input with command's call, finds appropriate
|
||||||
|
/// registered command instance and executes it with parameters specified in
|
||||||
|
/// the [`commandLine`].
|
||||||
|
///
|
||||||
|
/// [`callerPlayer`] has to be specified and represents instigator of this
|
||||||
|
/// command that will receive appropriate result/error messages.
|
||||||
|
///
|
||||||
|
/// Returns `true` iff command was successfully executed.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Doesn't log any errors, but can complain about errors in name or parameters
|
||||||
|
/// to the [`callerPlayer`]
|
||||||
|
public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) {
|
||||||
|
local bool errorOccured;
|
||||||
|
local User identity;
|
||||||
|
local CommandAPI.CommandConfigInfo commandPair;
|
||||||
|
local Command.CallData callData;
|
||||||
|
local CommandCallPair callPair;
|
||||||
|
|
||||||
|
if (parser == none) return false;
|
||||||
|
if (callerPlayer == none) return false;
|
||||||
|
if (!parser.Ok()) return false;
|
||||||
|
|
||||||
|
identity = callerPlayer.GetIdentity();
|
||||||
|
callPair = ParseCommandCallPairWith(parser);
|
||||||
|
commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity);
|
||||||
|
if (commandPair.instance == none || commandPair.usageForbidden) {
|
||||||
|
if (callerPlayer != none && callerPlayer.IsExistent()) {
|
||||||
|
callerPlayer
|
||||||
|
.BorrowConsole()
|
||||||
|
.Flush()
|
||||||
|
.Say(F("{$TextFailure Command not found!}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parser.Ok() && commandPair.instance != none && !commandPair.usageForbidden) {
|
||||||
|
callData =
|
||||||
|
commandPair.instance.ParseInputWith(parser, callerPlayer, callPair.subCommandName);
|
||||||
|
errorOccured = commandPair.instance.Execute(callData, callerPlayer, commandPair.config);
|
||||||
|
commandPair.instance.DeallocateCallData(callData);
|
||||||
|
}
|
||||||
|
_.memory.Free4(commandPair.instance, callPair.commandName, callPair.subCommandName, identity);
|
||||||
|
return errorOccured;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses command's name into `CommandCallPair` - sub-command is filled in case
|
||||||
|
// specified name is an alias with specified sub-command name.
|
||||||
|
private final function CommandCallPair ParseCommandCallPairWith(Parser parser) {
|
||||||
|
local Text resolvedValue;
|
||||||
|
local MutableText userSpecifiedName;
|
||||||
|
local CommandCallPair result;
|
||||||
|
local Text.Character dotCharacter;
|
||||||
|
|
||||||
|
if (parser == none) return result;
|
||||||
|
if (!parser.Ok()) return result;
|
||||||
|
|
||||||
|
parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true);
|
||||||
|
resolvedValue = _.alias.ResolveCommand(userSpecifiedName);
|
||||||
|
// This isn't an alias
|
||||||
|
if (resolvedValue == none) {
|
||||||
|
result.commandName = userSpecifiedName;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// It is an alias - parse it
|
||||||
|
dotCharacter = _.text.GetCharacter(".");
|
||||||
|
resolvedValue.Parse()
|
||||||
|
.MUntil(result.commandName, dotCharacter)
|
||||||
|
.MatchS(".")
|
||||||
|
.MUntil(result.subCommandName, dotCharacter)
|
||||||
|
.FreeSelf();
|
||||||
|
if (result.subCommandName.IsEmpty()) {
|
||||||
|
result.subCommandName.FreeSelf();
|
||||||
|
result.subCommandName = none;
|
||||||
|
}
|
||||||
|
resolvedValue.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) {
|
||||||
|
local Parser parser;
|
||||||
|
|
||||||
|
// We are only interested in messages that start with `chatCommandPrefix`
|
||||||
|
parser = _.text.Parse(message);
|
||||||
|
if (!parser.Match(chatCommandPrefix).Ok()) {
|
||||||
|
parser.FreeSelf();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Pass input to command feature
|
||||||
|
HandleInputWith(parser, sender);
|
||||||
|
parser.FreeSelf();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function HandleMutate(string command, PlayerController sendingPlayer) {
|
||||||
|
local Parser parser;
|
||||||
|
local EPlayer sender;
|
||||||
|
|
||||||
|
// A lot of other mutators use these commands
|
||||||
|
if (command ~= "help") return;
|
||||||
|
if (command ~= "version") return;
|
||||||
|
if (command ~= "status") return;
|
||||||
|
if (command ~= "credits") return;
|
||||||
|
|
||||||
|
parser = _.text.ParseString(command);
|
||||||
|
sender = _.players.FromController(sendingPlayer);
|
||||||
|
HandleInputWith(parser, sender);
|
||||||
|
sender.FreeSelf();
|
||||||
|
parser.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function LoadConfigArrays() {
|
||||||
|
local int i;
|
||||||
|
local CommandListGroupPair nextCommandSetGroupPair;
|
||||||
|
|
||||||
|
for (i = 0; i < commandGroup.length; i += 1) {
|
||||||
|
permissionGroupOrder[i] = _.text.FromString(commandGroup[i]);
|
||||||
|
}
|
||||||
|
for (i = 0; i < addCommandSet.length; i += 1) {
|
||||||
|
nextCommandSetGroupPair.commandListName = _.text.FromString(addCommandSet[i].name);
|
||||||
|
nextCommandSetGroupPair.permissionGroup = _.text.FromString(addCommandSet[i].for);
|
||||||
|
usedCommandLists[i] = nextCommandSetGroupPair;
|
||||||
|
}
|
||||||
|
FreeRenamingRules();
|
||||||
|
commandRenamingRules = LoadRenamingRules(renamingRule);
|
||||||
|
votingRenamingRules = LoadRenamingRules(votingRenamingRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function array<RenamingRulePair> LoadRenamingRules(
|
||||||
|
array<Commands.RenamingRulePair> inputRules) {
|
||||||
|
local int i, j;
|
||||||
|
local RenamingRulePair nextRule;
|
||||||
|
local array<RenamingRulePair> result;
|
||||||
|
|
||||||
|
// Clear away duplicates
|
||||||
|
for (i = 0; i < inputRules.length; i += 1) {
|
||||||
|
j = i + 1;
|
||||||
|
while (j < inputRules.length) {
|
||||||
|
if (inputRules[i].rename == inputRules[j].rename) {
|
||||||
|
_.logger.Auto(warnDuplicateRenaming)
|
||||||
|
.ArgClass(inputRules[i].rename)
|
||||||
|
.Arg(_.text.FromString(inputRules[i].to))
|
||||||
|
.Arg(_.text.FromString(inputRules[j].to));
|
||||||
|
inputRules.Remove(j, 1);
|
||||||
|
} else {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Translate rules
|
||||||
|
for (i = 0; i < inputRules.length; i += 1) {
|
||||||
|
nextRule.class = inputRules[i].rename;
|
||||||
|
nextRule.newName = _.text.FromString(inputRules[i].to);
|
||||||
|
if (nextRule.class == class'ACommandHelp') {
|
||||||
|
_.memory.Free(helpCommandName);
|
||||||
|
helpCommandName = nextRule.newName.Copy();
|
||||||
|
}
|
||||||
|
result[result.length] = nextRule;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function LoadCommands() {
|
||||||
|
local int i, j;
|
||||||
|
local Text nextName;
|
||||||
|
local array<EntityLoadInfo> commandClassesToLoad;
|
||||||
|
|
||||||
|
commandClassesToLoad = CollectAllCommandClasses();
|
||||||
|
// Load command names to use, according to preferred names and name rules
|
||||||
|
for (i = 0; i < commandClassesToLoad.length; i += 1) {
|
||||||
|
nextName = none;
|
||||||
|
for (j = 0; j < commandRenamingRules.length; j += 1) {
|
||||||
|
if (commandClassesToLoad[i].entityClass == commandRenamingRules[j].class) {
|
||||||
|
nextName = commandRenamingRules[j].newName.Copy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextName == none) {
|
||||||
|
nextName = class<Command>(commandClassesToLoad[i].entityClass)
|
||||||
|
.static.GetPreferredName();
|
||||||
|
}
|
||||||
|
commandClassesToLoad[i].name = nextName;
|
||||||
|
}
|
||||||
|
// Actually load commands
|
||||||
|
for (i = 0; i < commandClassesToLoad.length; i += 1) {
|
||||||
|
_.commands.AddCommandAsync(
|
||||||
|
class<Command>(commandClassesToLoad[i].entityClass),
|
||||||
|
commandClassesToLoad[i].name);
|
||||||
|
for (j = 0; j < commandClassesToLoad[i].authorizedGroups.length; j += 1) {
|
||||||
|
_.commands.AuthorizeCommandUsageAsync(
|
||||||
|
commandClassesToLoad[i].name,
|
||||||
|
commandClassesToLoad[i].authorizedGroups[j],
|
||||||
|
commandClassesToLoad[i].groupsConfig[j]);
|
||||||
|
}
|
||||||
|
_.logger.Auto(infoCommandAdded)
|
||||||
|
.ArgClass(commandClassesToLoad[i].entityClass)
|
||||||
|
.Arg(/*take*/ commandClassesToLoad[i].name);
|
||||||
|
}
|
||||||
|
for (i = 0; i < commandClassesToLoad.length; i += 1) {
|
||||||
|
// `name` field was already released through `Arg()` logger function
|
||||||
|
_.memory.FreeMany(commandClassesToLoad[i].authorizedGroups);
|
||||||
|
_.memory.FreeMany(commandClassesToLoad[i].groupsConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function LoadVotings() {
|
||||||
|
local int i, j;
|
||||||
|
local Text nextName;
|
||||||
|
local array<EntityLoadInfo> votingClassesToLoad;
|
||||||
|
|
||||||
|
votingClassesToLoad = CollectAllVotingClasses();
|
||||||
|
// Load voting names to use, according to preferred names and name rules
|
||||||
|
for (i = 0; i < votingClassesToLoad.length; i += 1) {
|
||||||
|
nextName = none;
|
||||||
|
for (j = 0; j < votingRenamingRules.length; j += 1) {
|
||||||
|
if (votingClassesToLoad[i].entityClass == votingRenamingRules[j].class) {
|
||||||
|
nextName = votingRenamingRules[j].newName.Copy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextName == none) {
|
||||||
|
nextName = class<Voting>(votingClassesToLoad[i].entityClass)
|
||||||
|
.static.GetPreferredName();
|
||||||
|
}
|
||||||
|
votingClassesToLoad[i].name = nextName;
|
||||||
|
}
|
||||||
|
// Actually load votings
|
||||||
|
for (i = 0; i < votingClassesToLoad.length; i += 1) {
|
||||||
|
_.commands.AddVotingAsync(
|
||||||
|
class<Voting>(votingClassesToLoad[i].entityClass),
|
||||||
|
votingClassesToLoad[i].name);
|
||||||
|
for (j = 0; j < votingClassesToLoad[i].authorizedGroups.length; j += 1) {
|
||||||
|
_.commands.AuthorizeVotingUsageAsync(
|
||||||
|
votingClassesToLoad[i].name,
|
||||||
|
votingClassesToLoad[i].authorizedGroups[j],
|
||||||
|
votingClassesToLoad[i].groupsConfig[j]);
|
||||||
|
}
|
||||||
|
_.logger.Auto(infoVotingAdded)
|
||||||
|
.ArgClass(votingClassesToLoad[i].entityClass)
|
||||||
|
.Arg(/*take*/ votingClassesToLoad[i].name);
|
||||||
|
}
|
||||||
|
for (i = 0; i < votingClassesToLoad.length; i += 1) {
|
||||||
|
// `name` field was already released through `Arg()` logger function
|
||||||
|
_.memory.FreeMany(votingClassesToLoad[i].authorizedGroups);
|
||||||
|
_.memory.FreeMany(votingClassesToLoad[i].groupsConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed to not return `none` items in the array
|
||||||
|
private final function array<EntityLoadInfo> CollectAllCommandClasses() {
|
||||||
|
local int i;
|
||||||
|
local bool debugging;
|
||||||
|
local CommandList nextList;
|
||||||
|
local array<EntityLoadInfo> result;
|
||||||
|
|
||||||
|
debugging = _.environment.IsDebugging();
|
||||||
|
class'CommandList'.static.Initialize();
|
||||||
|
for (i = 0; i < usedCommandLists.length; i += 1) {
|
||||||
|
nextList = CommandList(class'CommandList'.static
|
||||||
|
.GetConfigInstance(usedCommandLists[i].commandListName));
|
||||||
|
if (nextList != none) {
|
||||||
|
if (!debugging && nextList.debugOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MergeEntityClassArrays(
|
||||||
|
result,
|
||||||
|
/*take*/ nextList.GetCommandData(),
|
||||||
|
usedCommandLists[i].permissionGroup);
|
||||||
|
} else {
|
||||||
|
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed to not return `none` items in the array
|
||||||
|
private final function array<EntityLoadInfo> CollectAllVotingClasses() {
|
||||||
|
local int i;
|
||||||
|
local bool debugging;
|
||||||
|
local CommandList nextList;
|
||||||
|
local array<EntityLoadInfo> result;
|
||||||
|
|
||||||
|
debugging = _.environment.IsDebugging();
|
||||||
|
class'CommandList'.static.Initialize();
|
||||||
|
for (i = 0; i < usedCommandLists.length; i += 1) {
|
||||||
|
nextList = CommandList(class'CommandList'.static
|
||||||
|
.GetConfigInstance(usedCommandLists[i].commandListName));
|
||||||
|
if (nextList != none) {
|
||||||
|
if (!debugging && nextList.debugOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MergeEntityClassArrays(
|
||||||
|
result,
|
||||||
|
/*take*/ nextList.GetVotingData(),
|
||||||
|
usedCommandLists[i].permissionGroup);
|
||||||
|
} else {
|
||||||
|
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds `newCommands` into `infoArray`, adding `commandsGroup` to
|
||||||
|
// their array `authorizedGroups`
|
||||||
|
//
|
||||||
|
// Assumes that all arguments aren't `none`.
|
||||||
|
//
|
||||||
|
// Guaranteed to not add `none` commands from `newCommands`.
|
||||||
|
//
|
||||||
|
// Assumes that items from `infoArray` all have `name` field set to `none`,
|
||||||
|
// will also leave them as `none`.
|
||||||
|
private final function MergeEntityClassArrays(
|
||||||
|
out array<EntityLoadInfo> infoArray,
|
||||||
|
/*take*/ array<CommandList.EntityConfigPair> newCommands,
|
||||||
|
Text commandsGroup
|
||||||
|
) {
|
||||||
|
local int i, infoToEditIndex;
|
||||||
|
local EntityLoadInfo infoToEdit;
|
||||||
|
local array<Text> editedArray;
|
||||||
|
|
||||||
|
for (i = 0; i < newCommands.length; i += 1) {
|
||||||
|
if (newCommands[i].class == none) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Search for an existing record of class `newCommands[i]` in
|
||||||
|
// `infoArray`. If found, copy to `infoToEdit` and index into
|
||||||
|
// `infoToEditIndex`, else `infoToEditIndex` will hold the next unused
|
||||||
|
// index in `infoArray`.
|
||||||
|
infoToEditIndex = 0;
|
||||||
|
while (infoToEditIndex < infoArray.length) {
|
||||||
|
if (infoArray[infoToEditIndex].entityClass == newCommands[i].class) {
|
||||||
|
infoToEdit = infoArray[infoToEditIndex];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
infoToEditIndex += 1;
|
||||||
|
}
|
||||||
|
// Update data inside `infoToEdit`.
|
||||||
|
infoToEdit.entityClass = newCommands[i].class;
|
||||||
|
|
||||||
|
editedArray = infoToEdit.authorizedGroups;
|
||||||
|
editedArray[editedArray.length] = commandsGroup.Copy();
|
||||||
|
infoToEdit.authorizedGroups = editedArray;
|
||||||
|
|
||||||
|
editedArray = infoToEdit.groupsConfig;
|
||||||
|
editedArray[editedArray.length] = newCommands[i].config; // moving here
|
||||||
|
infoToEdit.groupsConfig = editedArray;
|
||||||
|
// Update `infoArray` with the modified record.
|
||||||
|
infoArray[infoToEditIndex] = infoToEdit;
|
||||||
|
// Forget about data `authorizedGroups` and `groupsConfig`:
|
||||||
|
//
|
||||||
|
// 1. Their references have already been moved into `infoArray` and
|
||||||
|
// don't need to be released;
|
||||||
|
// 2. If we don't find corresponding struct inside `infoArray` on
|
||||||
|
// the next iteration, we'll override `commandClass`,
|
||||||
|
// but not `authorizedGroups`/`groupsConfig`, so we'll just reset them
|
||||||
|
// to empty now.
|
||||||
|
// (`name` field is expected to be `none` during this method)
|
||||||
|
infoToEdit.authorizedGroups.length = 0;
|
||||||
|
infoToEdit.groupsConfig.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function FreeUsedCommandSets() {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < usedCommandLists.length; i += 1) {
|
||||||
|
_.memory.Free(usedCommandLists[i].commandListName);
|
||||||
|
_.memory.Free(usedCommandLists[i].permissionGroup);
|
||||||
|
}
|
||||||
|
usedCommandLists.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function FreeRenamingRules() {
|
||||||
|
local int i;
|
||||||
|
|
||||||
|
for (i = 0; i < commandRenamingRules.length; i += 1) {
|
||||||
|
_.memory.Free(commandRenamingRules[i].newName);
|
||||||
|
}
|
||||||
|
commandRenamingRules.length = 0;
|
||||||
|
|
||||||
|
for (i = 0; i < votingRenamingRules.length; i += 1) {
|
||||||
|
_.memory.Free(votingRenamingRules[i].newName);
|
||||||
|
}
|
||||||
|
votingRenamingRules.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function CommandAPI.CommandFeatureTools _borrowTools() {
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
configClass = class'Commands'
|
||||||
|
errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.")
|
||||||
|
warnDuplicateRenaming = (l=LOG_Warning,m="Class `%1` has duplicate renaming rules: \"%2\" and \"%3\". Latter will be discarded. It is recommended that you fix your `AcediaCommands` configuration file.")
|
||||||
|
warnNoCommandList = (l=LOG_Warning,m="Command list \"%1\" not found.")
|
||||||
|
infoCommandAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load command `%1` as \"%2\".")
|
||||||
|
infoVotingAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load voting `%1` as \"%2\".")
|
||||||
|
}
|
||||||
77
kf_sources/AcediaCore/Classes/Component.uc
Normal file
77
kf_sources/AcediaCore/Classes/Component.uc
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Author: dkanus
|
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
||||||
|
* License: GPL
|
||||||
|
* Copyright 2024 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class Component extends AcediaObject
|
||||||
|
abstract;
|
||||||
|
|
||||||
|
//! A base class to implement components from, supposed to function as a special data storage
|
||||||
|
//! for in-game [`Entity`]s.
|
||||||
|
//!
|
||||||
|
//! On the current stage of implementation doesn't do anything by itself.
|
||||||
|
|
||||||
|
/// Creates an independent copy of the component.
|
||||||
|
///
|
||||||
|
/// This function duplicates the caller component, copying all data in an identical
|
||||||
|
/// fashion, including references to other entities (stored as [`EntityRef`]).
|
||||||
|
/// The result is a new component that is a direct copy of the original, maintaining
|
||||||
|
/// all internal states and links.
|
||||||
|
///
|
||||||
|
/// To make a new component using this one like a template use [`Component::TemplateCopy()`].
|
||||||
|
public final function Component Copy() {
|
||||||
|
return _copy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a new component instance using the caller as a template.
|
||||||
|
///
|
||||||
|
/// This function uses the caller component as a template to create a new component.
|
||||||
|
/// Unlike the [`Component::Copy()`] method, references to other entities (stored as [`EntityRef`])
|
||||||
|
/// are not directly copied.
|
||||||
|
/// Instead, they are dropped, and new references are created based on the template names provided
|
||||||
|
/// by [`EntityRef`] if available.
|
||||||
|
/// This method is intended for scenarios where a new, similar instance is needed without retaining
|
||||||
|
/// direct links to the entities referenced by the original component.
|
||||||
|
public final function Component TemplateCopy() {
|
||||||
|
return _copy(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal component copy function to avoid code duplication for copying logic in
|
||||||
|
/// [`Component::Copy()`] and [`Component::TemplateCopy()`].
|
||||||
|
///
|
||||||
|
/// This protected function serves as the core implementation for copying component instances.
|
||||||
|
/// It can operate in two modes depending on the `actAsTemplate` flag:
|
||||||
|
/// 1. When `actAsTemplate` is false, it performs a direct copy of the component,
|
||||||
|
/// replicating all data and entity references exactly as in the original component.
|
||||||
|
/// 2. When `actAsTemplate` is true, it creates a new component based on the template
|
||||||
|
/// characteristics of the caller.
|
||||||
|
/// In this mode, entity references (stored as [`EntityRef`]) are not directly copied.
|
||||||
|
/// Instead, these references are dropped, and new instances are created based on the template
|
||||||
|
/// names provided by the [`EntityRef`] if available, ensuring that the new component does not
|
||||||
|
/// retain direct links to the entities of the original.
|
||||||
|
///
|
||||||
|
/// This method should not be called directly; use `Copy()` or `TemplateCopy()` as
|
||||||
|
/// public interfaces.
|
||||||
|
/// They correspond to modes 1 and 2 respectively.
|
||||||
|
protected function Component _copy(bool actAsTemplate) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties {
|
||||||
|
}
|
||||||
260
kf_sources/AcediaCore/Classes/ConsoleAPI.uc
Normal file
260
kf_sources/AcediaCore/Classes/ConsoleAPI.uc
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* API that provides functions for outputting text in
|
||||||
|
* Killing Floor's console. It takes care of coloring output and breaking up
|
||||||
|
* long lines (since allowing game to handle line breaking completely
|
||||||
|
* messes up console output).
|
||||||
|
*
|
||||||
|
* Actual output is taken care of by `ConsoleWriter` objects that this
|
||||||
|
* API generates.
|
||||||
|
* Copyright 2020 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ConsoleAPI extends AcediaObject
|
||||||
|
config(AcediaSystem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main issue with console output in Killing Floor is
|
||||||
|
* automatic line breaking of long enough messages:
|
||||||
|
* it breaks formatting and can lead to an ugly text overlapping.
|
||||||
|
* To fix this we will try to break up user's output into lines ourselves,
|
||||||
|
* before game does it for us.
|
||||||
|
*
|
||||||
|
* We are not 100% sure how Killing Floor decides when to break the line,
|
||||||
|
* but it seems to calculate how much text can actually fit in a certain
|
||||||
|
* area on screen.
|
||||||
|
* There are two issues:
|
||||||
|
* 1. We do not know for sure what this limit value is.
|
||||||
|
* Even if we knew how to compute it, we cannot do that in server mode,
|
||||||
|
* since it depends on a screen resolution and font, which
|
||||||
|
* can vary for different players.
|
||||||
|
* 2. Even invisible characters, such as color change sequences,
|
||||||
|
* that do not take any space on the screen, contribute towards
|
||||||
|
* that limit. So for a heavily colored text we will have to
|
||||||
|
* break line much sooner than for the plain text.
|
||||||
|
* Both issues are solved by introducing two limits that users themselves
|
||||||
|
* are allowed to change: visible character limit and total character limit.
|
||||||
|
* ~ Total character limit will be a hard limit on a character amount in
|
||||||
|
* a line (including hidden ones used for color change sequences) that
|
||||||
|
* will be used to prevent Killing Floor's native line breaks.
|
||||||
|
* ~ Visible character limit will be a lower limit on amount of actually
|
||||||
|
* visible character. It introduction basically reserves some space that can be
|
||||||
|
* used only for color change sequences. Without this limit lines with
|
||||||
|
* colored lines will appear to be shorter that mono-colored ones.
|
||||||
|
* Visible limit will help to alleviate this problem.
|
||||||
|
*
|
||||||
|
* For example, if we set total limit to `120` and visible limit to `80`:
|
||||||
|
* 1. Line not formatted with color will all break at
|
||||||
|
* around length of `80`.
|
||||||
|
* 2. Since color change sequence consists of 4 characters:
|
||||||
|
* we can fit up to `(120 - 80) / 4 = 10` color swaps into each line,
|
||||||
|
* while still breaking them at a around the same length of `80`.
|
||||||
|
* ~ To differentiate our line breaks from line breaks intended by
|
||||||
|
* the user, we will also add 2 symbols worth of padding in front of all our
|
||||||
|
* output:
|
||||||
|
* 1. Before intended new line they will be just two spaces.
|
||||||
|
* 2. After our line break we will replace first space with "|" to indicate
|
||||||
|
* that we had to break a long line.
|
||||||
|
*
|
||||||
|
* Described measures are not perfect:
|
||||||
|
* 1. Since Killing Floor's console does not use monospaced font,
|
||||||
|
* the same amount of characters on the line does not mean lines of
|
||||||
|
* visually the same length;
|
||||||
|
* 2. Heavily enough colored lines are still going to be shorter;
|
||||||
|
* 3. Depending on a resolution, default limits may appear to either use
|
||||||
|
* too little space (for high resolutions) or, on the contrary,
|
||||||
|
* not prevent native line breaks (low resolutions).
|
||||||
|
* In these cases user might be required to manually set limits;
|
||||||
|
* 4. There are probably more.
|
||||||
|
* But if seems to provide good enough results for the average use case.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures how text will be rendered in target console(s).
|
||||||
|
*/
|
||||||
|
struct ConsoleDisplaySettings
|
||||||
|
{
|
||||||
|
// What color to use for text by default
|
||||||
|
var Color defaultColor;
|
||||||
|
// How many visible characters in be displayed in a line?
|
||||||
|
var int maxVisibleLineWidth;
|
||||||
|
// How many total characters can be output at once?
|
||||||
|
var int maxTotalLineWidth;
|
||||||
|
};
|
||||||
|
// We will store data for `ConsoleDisplaySettings` separately for the ease of
|
||||||
|
// configuration.
|
||||||
|
var private config Color defaultColor;
|
||||||
|
var private config int maxVisibleLineWidth;
|
||||||
|
var private config int maxTotalLineWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current global visible limit that describes how many (at most)
|
||||||
|
* visible characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* Instances of `ConsoleWriter` are initialized with this value,
|
||||||
|
* but can later change this value independently.
|
||||||
|
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||||
|
*
|
||||||
|
* @return Current global visible limit.
|
||||||
|
*/
|
||||||
|
public final function int GetVisibleLineLength()
|
||||||
|
{
|
||||||
|
return maxVisibleLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets current global visible limit that describes how many (at most) visible
|
||||||
|
* characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* Instances of `ConsoleWriter` are initialized with this value,
|
||||||
|
* but can later change this value independently.
|
||||||
|
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||||
|
*
|
||||||
|
* @param newMaxVisibleLineWidth New global visible character limit.
|
||||||
|
*/
|
||||||
|
public final function SetVisibleLineLength(int newMaxVisibleLineWidth)
|
||||||
|
{
|
||||||
|
maxVisibleLineWidth = newMaxVisibleLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current global total limit that describes how many (at most)
|
||||||
|
* characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* Instances of `ConsoleWriter` are initialized with this value,
|
||||||
|
* but can later change this value independently.
|
||||||
|
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||||
|
*
|
||||||
|
* @return Current global total limit.
|
||||||
|
*/
|
||||||
|
public final function int GetTotalLineLength()
|
||||||
|
{
|
||||||
|
return maxTotalLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets current global total limit that describes how many (at most)
|
||||||
|
* characters can be output in the console line, counting both visible symbols
|
||||||
|
* and color change sequences.
|
||||||
|
*
|
||||||
|
* Instances of `ConsoleWriter` are initialized with this value,
|
||||||
|
* but can later change this value independently.
|
||||||
|
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||||
|
*
|
||||||
|
* @param newMaxTotalLineWidth New global total character limit.
|
||||||
|
*/
|
||||||
|
public final function SetTotalLineLength(int newMaxTotalLineWidth)
|
||||||
|
{
|
||||||
|
maxTotalLineWidth = newMaxTotalLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current global total limit that describes how many (at most)
|
||||||
|
* characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* Instances of `ConsoleWriter` are initialized with this value,
|
||||||
|
* but can later change this value independently.
|
||||||
|
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||||
|
*
|
||||||
|
* @return Current default output color.
|
||||||
|
*/
|
||||||
|
public final function Color GetDefaultColor(int newMaxTotalLineWidth)
|
||||||
|
{
|
||||||
|
return defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets current global default color for console output.
|
||||||
|
*
|
||||||
|
* Instances of `ConsoleWriter` are initialized with this value,
|
||||||
|
* but can later change this value independently.
|
||||||
|
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||||
|
*
|
||||||
|
* @param newMaxTotalLineWidth New global default output color.
|
||||||
|
*/
|
||||||
|
public final function SetDefaultColor(Color newDefaultColor)
|
||||||
|
{
|
||||||
|
defaultColor = newDefaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new `ConsoleWriter` instance that will write into
|
||||||
|
* consoles of all players.
|
||||||
|
*
|
||||||
|
* @return ConsoleWriter New `ConsoleWriter` instance, configured to
|
||||||
|
* write into consoles of all players.
|
||||||
|
* Guaranteed to not be `none`.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter ForAll()
|
||||||
|
{
|
||||||
|
local ConsoleDisplaySettings globalSettings;
|
||||||
|
globalSettings.defaultColor = defaultColor;
|
||||||
|
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
|
||||||
|
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
|
||||||
|
return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter'))
|
||||||
|
.Initialize(globalSettings).ForAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new `ConsoleWriter` instance that will write into
|
||||||
|
* console of the given player.
|
||||||
|
*
|
||||||
|
* @param targetPlayer Player, to whom console we want to write.
|
||||||
|
* If `none` - returned `ConsoleWriter` would be configured to
|
||||||
|
* throw messages away.
|
||||||
|
* @return New `ConsoleWriter` instance, configured to
|
||||||
|
* write into console of `targetPlayer`.
|
||||||
|
* Guaranteed to not be `none`.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter For(EPlayer targetPlayer)
|
||||||
|
{
|
||||||
|
local ConsoleDisplaySettings globalSettings;
|
||||||
|
globalSettings.defaultColor = defaultColor;
|
||||||
|
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
|
||||||
|
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
|
||||||
|
return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter'))
|
||||||
|
.Initialize(globalSettings).ForPlayer(targetPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new `ConsoleWriter` instance that will write into
|
||||||
|
* console of the given player.
|
||||||
|
*
|
||||||
|
* @param targetPlayer Player, to whom console we want to write.
|
||||||
|
* If `none` - returned `ConsoleWriter` would be configured to
|
||||||
|
* throw messages away.
|
||||||
|
* @return New `ConsoleWriter` instance, configured to
|
||||||
|
* write into console of `targetPlayer`.
|
||||||
|
* Guaranteed to not be `none`.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter ForController(PlayerController targetPlayer)
|
||||||
|
{
|
||||||
|
local EPlayer wrapper;
|
||||||
|
local ConsoleWriter result;
|
||||||
|
wrapper = _.players.FromController(targetPlayer);
|
||||||
|
result = For(wrapper);
|
||||||
|
_.memory.Free(wrapper);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
defaultColor = (R=255,G=255,B=255,A=255)
|
||||||
|
// These should guarantee decent text output even at
|
||||||
|
// 640x480 shit resolution
|
||||||
|
maxVisibleLineWidth = 80
|
||||||
|
maxTotalLineWidth = 108
|
||||||
|
}
|
||||||
417
kf_sources/AcediaCore/Classes/ConsoleBuffer.uc
Normal file
417
kf_sources/AcediaCore/Classes/ConsoleBuffer.uc
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
/**
|
||||||
|
* Object that provides a buffer functionality for Killing Floor's (in-game)
|
||||||
|
* console output: it accepts content that user want to output and breaks it
|
||||||
|
* into lines that will be well-rendered according to the given
|
||||||
|
* `ConsoleDisplaySettings`.
|
||||||
|
* Copyright 2020 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ConsoleBuffer extends AcediaObject
|
||||||
|
dependson(BaseText)
|
||||||
|
dependson(ConsoleAPI);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `ConsoleBuffer` works by breaking it's input into words, counting how much
|
||||||
|
* space they take up and only then deciding to which line to append them
|
||||||
|
* (new or the next, new one).
|
||||||
|
*
|
||||||
|
* It is implemented making heavier use of `string`s instead of `Text`.
|
||||||
|
* This is because:
|
||||||
|
* 1. `string`s that are passed to console are broken into lines,
|
||||||
|
* that need to be specifically prepared anyway;
|
||||||
|
* 2. It was coded before I the switch to mostly using `Text`,
|
||||||
|
* when a lot of methods were also accepting `string`s
|
||||||
|
* and `array<Character>` types as parameters.
|
||||||
|
* And i really do not want to have to reimplement it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var private int CODEPOINT_ESCAPE;
|
||||||
|
var private int CODEPOINT_NEWLINE;
|
||||||
|
var private int COLOR_SEQUENCE_LENGTH;
|
||||||
|
|
||||||
|
// Display settings according to which to format our output
|
||||||
|
var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure is used to both share results of our work and for tracking
|
||||||
|
* information about the line we are currently filling.
|
||||||
|
*/
|
||||||
|
struct LineRecord
|
||||||
|
{
|
||||||
|
// Contents of the line, as a colored `string`.
|
||||||
|
// Not a `Text`, because it has to be prepared exactly how we want it.
|
||||||
|
var string contents;
|
||||||
|
// Is this a wrapped line?
|
||||||
|
// `true` means that this line was supposed to be part part of another,
|
||||||
|
// singular line of text, that had to be broken into smaller pieces.
|
||||||
|
// Such lines will start with "|" in front of them in Acedia's
|
||||||
|
// `ConsoleWriter`.
|
||||||
|
var bool wrappedLine;
|
||||||
|
// Information variables that describe how many visible and total symbols
|
||||||
|
// (visible + color change sequences) are stored int the `line`
|
||||||
|
var int visibleSymbolsStored;
|
||||||
|
var int totalSymbolsStored;
|
||||||
|
// Does `contents` contain a color change sequence?
|
||||||
|
// Non-empty line can have no such sequence if they consist of whitespaces.
|
||||||
|
var private bool colorInserted;
|
||||||
|
// If `colorInserted == true`, stores the last inserted color.
|
||||||
|
var private Color endColor;
|
||||||
|
};
|
||||||
|
// Lines that are ready to be output to the console
|
||||||
|
var private array<LineRecord> completedLines;
|
||||||
|
|
||||||
|
// Line we are currently building
|
||||||
|
var private LineRecord currentLine;
|
||||||
|
// Word we are currently building, colors of it's characters will be
|
||||||
|
// automatically converted into `STRCOLOR_Struct`, according to the default
|
||||||
|
// color setting at the time of their addition.
|
||||||
|
// We are using array of `Character`s instead of `MutableText` since
|
||||||
|
// we want to have a more directly control over how it is converted into
|
||||||
|
// a colored string anyway and otherwise only need an ability to
|
||||||
|
// append `Character`s to it.
|
||||||
|
var private array<Text.Character> wordBuffer;
|
||||||
|
// Amount of color swaps inside `wordBuffer`
|
||||||
|
var private int colorSwapsInWordBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current setting used by this buffer to break up it's input into
|
||||||
|
* lines fit to be output in console.
|
||||||
|
*
|
||||||
|
* @return Currently used `ConsoleDisplaySettings`.
|
||||||
|
*/
|
||||||
|
public final function ConsoleAPI.ConsoleDisplaySettings GetSettings()
|
||||||
|
{
|
||||||
|
return displaySettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets new setting to be used by this buffer to break up it's input into
|
||||||
|
* lines fit to be output in console.
|
||||||
|
*
|
||||||
|
* It is recommended (although not required) to call `Flush()` before
|
||||||
|
* changing settings. Not doing so would not lead to any errors or warnings,
|
||||||
|
* but can lead to some wonky results and is considered an undefined behavior.
|
||||||
|
*
|
||||||
|
* @param newSettings New `ConsoleDisplaySettings` to be used.
|
||||||
|
* @return Returns caller `ConsoleBuffer` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleBuffer SetSettings(
|
||||||
|
ConsoleAPI.ConsoleDisplaySettings newSettings)
|
||||||
|
{
|
||||||
|
displaySettings = newSettings;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does caller `ConsoleBuffer` has any completed lines that can be output?
|
||||||
|
*
|
||||||
|
* "Completed line" means that nothing else will be added to it.
|
||||||
|
* So negative (`false`) response does not mean that the buffer is empty, -
|
||||||
|
* it can still contain an uncompleted and non-empty line that can still be
|
||||||
|
* expanded with `Insert()`. If you want to completely empty the buffer -
|
||||||
|
* call the `Flush()` method.
|
||||||
|
* Also see `IsEmpty()`.
|
||||||
|
*
|
||||||
|
* @return `true` if caller `ConsoleBuffer` has no completed lines and
|
||||||
|
* `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool HasCompletedLines()
|
||||||
|
{
|
||||||
|
return (completedLines.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does caller `ConsoleBuffer` has any unprocessed input?
|
||||||
|
*
|
||||||
|
* Note that `ConsoleBuffer` can be non-empty, but no completed line if it
|
||||||
|
* currently builds one.
|
||||||
|
* See `Flush()` and `HasCompletedLines()` methods.
|
||||||
|
*
|
||||||
|
* @return `true` if `ConsoleBuffer` is completely empty
|
||||||
|
* (either did not receive or already returned all processed input) and
|
||||||
|
* `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool IsEmpty()
|
||||||
|
{
|
||||||
|
if (HasCompletedLines()) return false;
|
||||||
|
if (currentLine.totalSymbolsStored > 0) return false;
|
||||||
|
if (wordBuffer.length > 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the buffer of all data, but leaving current settings intact.
|
||||||
|
* After this calling method `IsEmpty()` should return `true`.
|
||||||
|
*
|
||||||
|
* @return Returns caller `ConsoleBuffer` to allow method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleBuffer Clear()
|
||||||
|
{
|
||||||
|
local LineRecord newLineRecord;
|
||||||
|
currentLine = newLineRecord;
|
||||||
|
completedLines.length = 0;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a string into the buffer. This method does not automatically break
|
||||||
|
* the line after the `input`, call `Flush()` or add line feed symbol "\n"
|
||||||
|
* at the end of the `input` if you want that.
|
||||||
|
*
|
||||||
|
* @param input `Text` to be added to the current line in caller
|
||||||
|
* `ConsoleBuffer`. Does nothing if passed `none`.
|
||||||
|
* @param inputType How to treat given `string` regarding coloring.
|
||||||
|
* @return Returns caller `ConsoleBuffer` to allow method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleBuffer Insert(BaseText input)
|
||||||
|
{
|
||||||
|
local int inputConsumed;
|
||||||
|
local BaseText.Character nextCharacter;
|
||||||
|
if (input == none) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
// Regular symbols and whitespaces are treated differently when
|
||||||
|
// breaking input into lines, so alternate between adding them,
|
||||||
|
// switching the logic appropriately
|
||||||
|
while (inputConsumed < input.GetLength())
|
||||||
|
{
|
||||||
|
while (inputConsumed < input.GetLength())
|
||||||
|
{
|
||||||
|
nextCharacter = input.GetCharacter(inputConsumed);
|
||||||
|
if (_.text.IsWhitespace(nextCharacter)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
InsertIntoWordBuffer(input.GetCharacter(inputConsumed));
|
||||||
|
inputConsumed += 1;
|
||||||
|
}
|
||||||
|
// If we didn't encounter any whitespace symbols - bail
|
||||||
|
if (inputConsumed >= input.GetLength()) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
FlushWordBuffer();
|
||||||
|
// Dump whitespaces into lines
|
||||||
|
while (inputConsumed < input.GetLength())
|
||||||
|
{
|
||||||
|
nextCharacter = input.GetCharacter(inputConsumed);
|
||||||
|
if (!_.text.IsWhitespace(nextCharacter)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AppendWhitespaceToCurrentLine(nextCharacter);
|
||||||
|
inputConsumed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns (and makes caller `ConsoleBuffer` forget) next completed line that
|
||||||
|
* can be output to console in `STRING_Colored` format.
|
||||||
|
*
|
||||||
|
* If there are no completed line to return - returns an empty one.
|
||||||
|
*
|
||||||
|
* @return Next completed line that can be output, in `STRING_Colored` format.
|
||||||
|
*/
|
||||||
|
public final function LineRecord PopNextLine()
|
||||||
|
{
|
||||||
|
local LineRecord result;
|
||||||
|
if (completedLines.length <= 0) return result;
|
||||||
|
result = completedLines[0];
|
||||||
|
completedLines.Remove(0, 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces all buffered data into "completed line" array, making it retrievable
|
||||||
|
* by `PopNextLine()`.
|
||||||
|
*
|
||||||
|
* @return Next completed line that can be output, in `STRING_Colored` format.
|
||||||
|
*/
|
||||||
|
public final function ConsoleBuffer Flush()
|
||||||
|
{
|
||||||
|
FlushWordBuffer();
|
||||||
|
BreakLine(false);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is assumed that passed characters are not whitespace, -
|
||||||
|
// responsibility to check is on the one calling this method.
|
||||||
|
private final function InsertIntoWordBuffer(BaseText.Character newCharacter)
|
||||||
|
{
|
||||||
|
local int newCharacterIndex;
|
||||||
|
local BaseText.Formatting newFormatting;
|
||||||
|
local Color oldColor, newColor;
|
||||||
|
// Fix text color in the buffer to remember default color, if we use it.
|
||||||
|
newFormatting = _.text.GetCharacterFormatting(newCharacter);
|
||||||
|
newFormatting.color =
|
||||||
|
_.text.GetCharacterColor(newCharacter, displaySettings.defaultColor);
|
||||||
|
newFormatting.isColored = true;
|
||||||
|
newCharacter = _.text.SetFormatting(newCharacter, newFormatting);
|
||||||
|
|
||||||
|
// Add new character and check if color swapped
|
||||||
|
newCharacterIndex = wordBuffer.length;
|
||||||
|
wordBuffer[newCharacterIndex] = newCharacter;
|
||||||
|
if (newCharacterIndex <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newColor = newFormatting.color;
|
||||||
|
oldColor = _.text.GetCharacterColor(wordBuffer[newCharacterIndex - 1]);
|
||||||
|
if (!_.color.AreEqual(oldColor, newColor, true)) {
|
||||||
|
colorSwapsInWordBuffer += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pushes whole `wordBuffer` into lines
|
||||||
|
private final function FlushWordBuffer()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local Color newColor;
|
||||||
|
if (!WordCanFitInCurrentLine() && WordCanFitInNewLine()) {
|
||||||
|
BreakLine(true);
|
||||||
|
}
|
||||||
|
for (i = 0; i < wordBuffer.length; i += 1)
|
||||||
|
{
|
||||||
|
if (!CanAppendNonWhitespaceIntoLine(wordBuffer[i])) {
|
||||||
|
BreakLine(true);
|
||||||
|
}
|
||||||
|
newColor = _.text.GetCharacterColor(wordBuffer[i]);
|
||||||
|
if (MustSwapColorsFor(newColor))
|
||||||
|
{
|
||||||
|
currentLine.contents $= _.color.GetColorTag(newColor);
|
||||||
|
currentLine.totalSymbolsStored += COLOR_SEQUENCE_LENGTH;
|
||||||
|
currentLine.colorInserted = true;
|
||||||
|
currentLine.endColor = newColor;
|
||||||
|
}
|
||||||
|
currentLine.contents $= Chr(wordBuffer[i].codePoint);
|
||||||
|
currentLine.totalSymbolsStored += 1;
|
||||||
|
currentLine.visibleSymbolsStored += 1;
|
||||||
|
}
|
||||||
|
wordBuffer.length = 0;
|
||||||
|
colorSwapsInWordBuffer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function BreakLine(bool makeWrapped)
|
||||||
|
{
|
||||||
|
local LineRecord newLineRecord;
|
||||||
|
if (currentLine.visibleSymbolsStored > 0) {
|
||||||
|
completedLines[completedLines.length] = currentLine;
|
||||||
|
}
|
||||||
|
currentLine = newLineRecord;
|
||||||
|
currentLine.wrappedLine = makeWrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool MustSwapColorsFor(Color newColor)
|
||||||
|
{
|
||||||
|
if (!currentLine.colorInserted) return true;
|
||||||
|
return !_.color.AreEqual(currentLine.endColor, newColor, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool CanAppendWhitespaceIntoLine()
|
||||||
|
{
|
||||||
|
// We always allow to append at least something into empty line,
|
||||||
|
// otherwise we can never insert it anywhere
|
||||||
|
if (currentLine.totalSymbolsStored <= 0) return true;
|
||||||
|
if (currentLine.totalSymbolsStored >= displaySettings.maxTotalLineWidth)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentLine.visibleSymbolsStored >= displaySettings.maxVisibleLineWidth)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool CanAppendNonWhitespaceIntoLine(
|
||||||
|
BaseText.Character nextCharacter)
|
||||||
|
{
|
||||||
|
// We always allow to insert at least something into empty line,
|
||||||
|
// otherwise we can never insert it anywhere
|
||||||
|
if (currentLine.totalSymbolsStored <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check if we can fit a single character by fitting a whitespace symbol.
|
||||||
|
if (!CanAppendWhitespaceIntoLine()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!MustSwapColorsFor(_.text.GetCharacterColor(nextCharacter))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Can we fit character + color swap sequence?
|
||||||
|
return ( currentLine.totalSymbolsStored + COLOR_SEQUENCE_LENGTH + 1
|
||||||
|
<= displaySettings.maxTotalLineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For performance reasons assumes that passed character is a whitespace,
|
||||||
|
// the burden of checking is on the caller.
|
||||||
|
private final function AppendWhitespaceToCurrentLine(
|
||||||
|
BaseText.Character whitespace)
|
||||||
|
{
|
||||||
|
if (_.text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) {
|
||||||
|
BreakLine(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!CanAppendWhitespaceIntoLine()) {
|
||||||
|
BreakLine(true);
|
||||||
|
}
|
||||||
|
currentLine.contents $= Chr(whitespace.codePoint);
|
||||||
|
currentLine.totalSymbolsStored += 1;
|
||||||
|
currentLine.visibleSymbolsStored += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool WordCanFitInNewLine()
|
||||||
|
{
|
||||||
|
local int totalCharactersInWord;
|
||||||
|
if (wordBuffer.length <= 0) return true;
|
||||||
|
if (wordBuffer.length > displaySettings.maxVisibleLineWidth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// `(colorSwapsInWordBuffer + 1)` counts how many times we must
|
||||||
|
// switch color inside a word + 1 for setting initial color
|
||||||
|
totalCharactersInWord = wordBuffer.length
|
||||||
|
+ (colorSwapsInWordBuffer + 1) * COLOR_SEQUENCE_LENGTH;
|
||||||
|
return (totalCharactersInWord <= displaySettings.maxTotalLineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool WordCanFitInCurrentLine()
|
||||||
|
{
|
||||||
|
local int totalLimit, visibleLimit;
|
||||||
|
local int totalCharactersInWord;
|
||||||
|
if (wordBuffer.length <= 0) return true;
|
||||||
|
totalLimit =
|
||||||
|
displaySettings.maxTotalLineWidth - currentLine.totalSymbolsStored;
|
||||||
|
visibleLimit =
|
||||||
|
displaySettings.maxVisibleLineWidth - currentLine.visibleSymbolsStored;
|
||||||
|
// Visible symbols check
|
||||||
|
if (wordBuffer.length > visibleLimit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Total symbols check
|
||||||
|
totalCharactersInWord = wordBuffer.length
|
||||||
|
+ colorSwapsInWordBuffer * COLOR_SEQUENCE_LENGTH;
|
||||||
|
if (MustSwapColorsFor(_.text.GetCharacterColor(wordBuffer[0]))) {
|
||||||
|
totalCharactersInWord += COLOR_SEQUENCE_LENGTH;
|
||||||
|
}
|
||||||
|
return (totalCharactersInWord <= totalLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
CODEPOINT_ESCAPE = 27
|
||||||
|
CODEPOINT_NEWLINE = 10
|
||||||
|
// CODEPOINT_ESCAPE + <redByte> + <greenByte> + <blueByte>
|
||||||
|
COLOR_SEQUENCE_LENGTH = 4
|
||||||
|
}
|
||||||
668
kf_sources/AcediaCore/Classes/ConsoleWriter.uc
Normal file
668
kf_sources/AcediaCore/Classes/ConsoleWriter.uc
Normal file
@ -0,0 +1,668 @@
|
|||||||
|
/**
|
||||||
|
* Object that provides simple access to console output.
|
||||||
|
* Can either write to a certain player's console or to all consoles at once.
|
||||||
|
* Supports "fancy" and "raw" output (for more details @see `ConsoleAPI`).
|
||||||
|
* Copyright 2020 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class ConsoleWriter extends AcediaObject
|
||||||
|
dependson(ConsoleAPI)
|
||||||
|
dependson(ConnectionService);
|
||||||
|
|
||||||
|
// Prefixes we output before every line to signify whether they were broken
|
||||||
|
// or not
|
||||||
|
var private string NEWLINE_PREFIX;
|
||||||
|
var private string BROKENLINE_PREFIX;
|
||||||
|
var private string INDENTATION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes current output target of the `ConsoleWriter`.
|
||||||
|
*/
|
||||||
|
enum ConsoleWriterTarget
|
||||||
|
{
|
||||||
|
// No one. Can happed if our target disconnects.
|
||||||
|
CWTARGET_None,
|
||||||
|
// A certain set of players.
|
||||||
|
CWTARGET_Players,
|
||||||
|
// All players.
|
||||||
|
CWTARGET_All
|
||||||
|
};
|
||||||
|
var private ConsoleWriterTarget targetType;
|
||||||
|
// Players that will receive output passed to this `ConsoleWriter`.
|
||||||
|
// Only used when `targetType == CWTARGET_Players`
|
||||||
|
// Cannot be allowed to contain `none` values.
|
||||||
|
var private array<EPlayer> outputTargets;
|
||||||
|
var private ConsoleBuffer outputBuffer;
|
||||||
|
|
||||||
|
var private bool needToResetColor;
|
||||||
|
var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
|
||||||
|
// Sometimes we want to output a certain part of text with a color
|
||||||
|
// different from the default one. However this requires to remember current
|
||||||
|
// default in additional variable, set new color and then return the old one.
|
||||||
|
// To slightly simplify this process we use pair of `UseColor()`/`ResetColor()`
|
||||||
|
// methods that use this variable to remember "real" default color and allowing
|
||||||
|
// us to quickly reset back to it.
|
||||||
|
// This also means that `displaySettings` can sometimes store "default"
|
||||||
|
// color information instead.
|
||||||
|
var private Color defaultColor;
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
_.memory.FreeMany(outputTargets);
|
||||||
|
_.memory.Free(outputBuffer);
|
||||||
|
outputTargets.length = 0;
|
||||||
|
outputBuffer = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final function ConsoleWriter Initialize(
|
||||||
|
ConsoleAPI.ConsoleDisplaySettings newDisplaySettings)
|
||||||
|
{
|
||||||
|
defaultColor = newDisplaySettings.defaultColor;
|
||||||
|
displaySettings = newDisplaySettings;
|
||||||
|
if (outputBuffer == none) {
|
||||||
|
outputBuffer = ConsoleBuffer(_.memory.Allocate(class'ConsoleBuffer'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
outputBuffer.Clear();
|
||||||
|
}
|
||||||
|
outputBuffer.SetSettings(displaySettings);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return default color setting for caller `ConsoleWriter`. It can be
|
||||||
|
* temporarily overwritten by `UseColor()` method.
|
||||||
|
*
|
||||||
|
* This method returns default color setting, i.e. color that will be used
|
||||||
|
* if no other is specified by text you're outputting and if it was not
|
||||||
|
* overwritten with `UseColor()` method.
|
||||||
|
* To get color currently used for outputting text, see `GetColor()`
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* Do note that `ConsoleWriter` can have two "default" colors: a "real"
|
||||||
|
* default and a "temporary" default: "temporary" one can be set with
|
||||||
|
* `UseColor()` or `UseColorOnce()` method calls to temporarily color certain
|
||||||
|
* part of the output and then revert to the "real" default color.
|
||||||
|
* This method always returns "real" default color.
|
||||||
|
*
|
||||||
|
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||||
|
* (or such value from any other `ConsoleWriter`) and affects only
|
||||||
|
* output produced by this `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* @return Current default color (the one that will be used to color output
|
||||||
|
* text after `ResetColor()` method call).
|
||||||
|
*/
|
||||||
|
public final function Color GetDefaultColor()
|
||||||
|
{
|
||||||
|
return defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return currently used default color for caller `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* This method returns default color, i.e. color that will be used if
|
||||||
|
* no other is specified by text you're outputting. If color is specified,
|
||||||
|
* this value is ignored.
|
||||||
|
* See also `GetDefaultColor()`.
|
||||||
|
*
|
||||||
|
* Do note that `ConsoleWriter` can have two "default" colors: a "real"
|
||||||
|
* default and a "temporary" default: "temporary" one can be set with
|
||||||
|
* `UseColor()` or `UseColorOnce()` method calls to temporarily color certain
|
||||||
|
* part of the output and then revert to the "real" default color.
|
||||||
|
* This method always return the color that will be actually used to
|
||||||
|
* output text, so "temporary" default if it's set and "real" default
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @return Current default color (currently used to output text information).
|
||||||
|
*/
|
||||||
|
public final function Color GetColor()
|
||||||
|
{
|
||||||
|
return displaySettings.defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets default color for caller 'ConsoleWriter`'s output.
|
||||||
|
*
|
||||||
|
* This only changes default color, i.e. color that will be used if no other is
|
||||||
|
* specified by "temporary" color. If color is specified, this value will
|
||||||
|
* be ignored.
|
||||||
|
*
|
||||||
|
* If you only want to quickly color certain part of output, it is better to
|
||||||
|
* use `UseColor()` or `UseColorOnce()` methods that temporarily changes used
|
||||||
|
* default color and allow to return to actual default color with
|
||||||
|
* `ResetColor()` method.
|
||||||
|
*
|
||||||
|
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||||
|
* (or such value from any other `ConsoleWriter`) and affects only
|
||||||
|
* output produced by this `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* @param newDefaultColor New color to use when none specified by text itself.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter SetColor(Color newDefaultColor)
|
||||||
|
{
|
||||||
|
defaultColor = newDefaultColor;
|
||||||
|
displaySettings.defaultColor = newDefaultColor;
|
||||||
|
if (outputBuffer != none) {
|
||||||
|
outputBuffer.SetSettings(displaySettings);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets "temporary" default color that can be reverted to "real" default color
|
||||||
|
* with `ResetColor()` method.
|
||||||
|
*
|
||||||
|
* For quickly coloring certain parts of output:
|
||||||
|
* `console.UseColor(_.color.blue).Write(blueMessage)
|
||||||
|
* .Write(moreBlueMessage).ResetColor()`.
|
||||||
|
* Also see `UseColorOnce()` method.
|
||||||
|
*
|
||||||
|
* Consecutive calls do not "stack up" colors - only last one is remembered:
|
||||||
|
* `console.UseColor(_.color.blue).UseColor(_.color.green)` is the same as
|
||||||
|
* `console.UseColor(_.color.green)`.
|
||||||
|
*
|
||||||
|
* Use `SetColor()` to set both "real" and "temporary" color.
|
||||||
|
*
|
||||||
|
* @param temporaryColor Color to use as default one in the next console
|
||||||
|
* output calls until the `ResetColor()` method call.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter UseColor(Color temporaryColor)
|
||||||
|
{
|
||||||
|
needToResetColor = false;
|
||||||
|
displaySettings.defaultColor = temporaryColor;
|
||||||
|
if (outputBuffer != none) {
|
||||||
|
outputBuffer.SetSettings(displaySettings);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets "temporary" default color that will be automatically reverted to "real"
|
||||||
|
* default color after any "writing" method (i.e. `Write()`, `WriteLine()`,
|
||||||
|
* `WriteBlock()` or `Say()`) or when `ResetColor()` method is called.
|
||||||
|
*
|
||||||
|
* For quickly coloring certain parts of output:
|
||||||
|
* `console.UseColorOnce(_.color.blue).Write(blueMessage)`.
|
||||||
|
*
|
||||||
|
* Consecutive calls do not "stack up" colors - only last one is remembered:
|
||||||
|
* `console.UseColorOnce(_.color.blue).UseColorOnce(_.color.green)`
|
||||||
|
* is the same as `console.UseColorOnce(_.color.green)`.
|
||||||
|
*
|
||||||
|
* Use `SetColor()` to set both "real" and "temporary" color.
|
||||||
|
*
|
||||||
|
* @param temporaryColor Color to use as default one in the next console
|
||||||
|
* output calls until any "writing" method (or `ResetColor()` method)
|
||||||
|
* is called.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter UseColorOnce(Color temporaryColor)
|
||||||
|
{
|
||||||
|
needToResetColor = true;
|
||||||
|
displaySettings.defaultColor = temporaryColor;
|
||||||
|
if (outputBuffer != none) {
|
||||||
|
outputBuffer.SetSettings(displaySettings);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets "temporary" default text color to "real" default color.
|
||||||
|
* See `UseColor()` and `UseColorOnce()` for details.
|
||||||
|
*
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter ResetColor()
|
||||||
|
{
|
||||||
|
needToResetColor = false;
|
||||||
|
displaySettings.defaultColor = defaultColor;
|
||||||
|
if (outputBuffer != none) {
|
||||||
|
outputBuffer.SetSettings(displaySettings);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current visible limit that describes how many (at most)
|
||||||
|
* visible characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||||
|
* (or such value from any other `ConsoleWriter`) and affects only
|
||||||
|
* output produced by this `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* @return Current global visible limit.
|
||||||
|
*/
|
||||||
|
public final function int GetVisibleLineLength()
|
||||||
|
{
|
||||||
|
return displaySettings.maxVisibleLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets current visible limit that describes how many (at most) visible
|
||||||
|
* characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||||
|
* (or such value from any other `ConsoleWriter`) and affects only
|
||||||
|
* output produced by this `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* @param newVisibleLimit New global visible limit.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter SetVisibleLineLength(
|
||||||
|
int newMaxVisibleLineWidth
|
||||||
|
)
|
||||||
|
{
|
||||||
|
displaySettings.maxVisibleLineWidth = newMaxVisibleLineWidth;
|
||||||
|
if (outputBuffer != none) {
|
||||||
|
outputBuffer.SetSettings(displaySettings);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current total limit that describes how many (at most)
|
||||||
|
* characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||||
|
* (or such value from any other `ConsoleWriter`) and affects only
|
||||||
|
* output produced by this `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* @return Current global total limit.
|
||||||
|
*/
|
||||||
|
public final function int GetTotalLineLength()
|
||||||
|
{
|
||||||
|
return displaySettings.maxTotalLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets current total limit that describes how many (at most)
|
||||||
|
* characters can be output in the console line.
|
||||||
|
*
|
||||||
|
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||||
|
* (or such value from any other `ConsoleWriter`) and affects only
|
||||||
|
* output produced by this `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* @param newTotalLimit New global total limit.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter SetTotalLineLength(int newMaxTotalLineWidth)
|
||||||
|
{
|
||||||
|
displaySettings.maxTotalLineWidth = newMaxTotalLineWidth;
|
||||||
|
if (outputBuffer != none) {
|
||||||
|
outputBuffer.SetSettings(displaySettings);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures caller `ConsoleWriter` to output to all players.
|
||||||
|
* `Flush()` will be automatically called if target actually has to switch
|
||||||
|
* (before the switch occurs).
|
||||||
|
*
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter ForAll()
|
||||||
|
{
|
||||||
|
if (targetType != CWTARGET_All) {
|
||||||
|
Flush();
|
||||||
|
}
|
||||||
|
targetType = CWTARGET_All;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures caller `ConsoleWriter` to output only to the given player.
|
||||||
|
* `Flush()` will be automatically called if target actually has to switch
|
||||||
|
* (before the switch occurs).
|
||||||
|
*
|
||||||
|
* @param targetPlayer Player, to whom console we want to write.
|
||||||
|
* If `none` - caller `ConsoleWriter` would be configured to
|
||||||
|
* throw messages away.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter ForPlayer(EPlayer targetPlayer)
|
||||||
|
{
|
||||||
|
if (targetPlayer == none)
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
targetType = CWTARGET_None;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
if (targetType != CWTARGET_Players) {
|
||||||
|
Flush();
|
||||||
|
}
|
||||||
|
outputTargets.length = 0;
|
||||||
|
targetType = CWTARGET_Players;
|
||||||
|
outputTargets[0] = targetPlayer;
|
||||||
|
targetPlayer.NewRef();
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures caller `ConsoleWriter` to output to one more player,
|
||||||
|
* given by `targetPlayer`.
|
||||||
|
* `Flush()` will be automatically called if target actually has to switch
|
||||||
|
* (before the switch occurs).
|
||||||
|
*
|
||||||
|
* @param targetPlayer Player, to whom console we want to write.
|
||||||
|
* If `none` - this method will do nothing.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter AndPlayer(EPlayer targetPlayer)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
if (targetPlayer == none) return self;
|
||||||
|
if (!targetPlayer.IsExistent()) return self;
|
||||||
|
|
||||||
|
if (targetType != CWTARGET_Players)
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
_.memory.FreeMany(outputTargets);
|
||||||
|
if (targetType == CWTARGET_None) {
|
||||||
|
outputTargets.length = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
outputTargets = _.players.GetAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetType = CWTARGET_Players;
|
||||||
|
for (i = 0; i < outputTargets.length; i += 1)
|
||||||
|
{
|
||||||
|
if (targetPlayer.SameAs(outputTargets[i])) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flush();
|
||||||
|
outputTargets[outputTargets.length] = targetPlayer;
|
||||||
|
targetPlayer.NewRef();
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures caller `ConsoleWriter` to output to one less player,
|
||||||
|
* given by `targetPlayer`.
|
||||||
|
* `Flush()` will be automatically called if target actually has to switch
|
||||||
|
* (before the switch occurs).
|
||||||
|
*
|
||||||
|
* @param targetPlayer Player, to whom console we no longer want to write.
|
||||||
|
* If `none` - this method will do nothing.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter ButPlayer(EPlayer playerToRemove)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
if (targetType == CWTARGET_None) return self;
|
||||||
|
if (playerToRemove == none) return self;
|
||||||
|
if (!playerToRemove.IsExistent()) return self;
|
||||||
|
|
||||||
|
if (targetType == CWTARGET_All)
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
_.memory.FreeMany(outputTargets);
|
||||||
|
outputTargets = _.players.GetAll();
|
||||||
|
}
|
||||||
|
targetType = CWTARGET_Players;
|
||||||
|
while (i < outputTargets.length)
|
||||||
|
{
|
||||||
|
if (playerToRemove.SameAs(outputTargets[i]))
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
_.memory.Free(outputTargets[i]);
|
||||||
|
outputTargets.Remove(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns type of current target for the caller `ConsoleWriter`.
|
||||||
|
*
|
||||||
|
* @return `ConsoleWriterTarget` value, describing current target of
|
||||||
|
* the caller `ConsoleWriter`.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriterTarget CurrentTarget()
|
||||||
|
{
|
||||||
|
if (targetType == CWTARGET_Players && outputTargets.length == 0) {
|
||||||
|
targetType = CWTARGET_None;
|
||||||
|
}
|
||||||
|
return targetType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `EPlayer` to whom console caller `ConsoleWriter` is
|
||||||
|
* outputting messages.
|
||||||
|
* If caller `ConsoleWriter` is setup to message several different players,
|
||||||
|
* returns an arbitrary one of them.
|
||||||
|
*
|
||||||
|
* @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is
|
||||||
|
* outputting messages. Returns `none` iff it currently outputs to
|
||||||
|
* every player or to no one.
|
||||||
|
*/
|
||||||
|
public final function EPlayer GetTargetPlayer()
|
||||||
|
{
|
||||||
|
if (targetType == CWTARGET_All) return none;
|
||||||
|
if (outputTargets.length <= 0) return none;
|
||||||
|
|
||||||
|
outputTargets[0].NewRef();
|
||||||
|
return outputTargets[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array of `EPlayer`s, to whom console caller `ConsoleWriter` is
|
||||||
|
* outputting messages.
|
||||||
|
*
|
||||||
|
* @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is
|
||||||
|
* outputting messages. Returned array is guaranteed to not contain `none`s
|
||||||
|
* or `EPlayer` interfaces with non-existent status.
|
||||||
|
*/
|
||||||
|
public final function array<EPlayer> GetTargetPlayers()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<EPlayer> result;
|
||||||
|
if (targetType == CWTARGET_None) return result;
|
||||||
|
if (targetType == CWTARGET_All) return _.players.GetAll();
|
||||||
|
|
||||||
|
for (i = 0; i < outputTargets.length; i += 1)
|
||||||
|
{
|
||||||
|
if (outputTargets[i].IsExistent()) {
|
||||||
|
result[result.length] = outputTargets[i];
|
||||||
|
outputTargets[i].NewRef();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs all buffered input and moves further output onto a new line.
|
||||||
|
*
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter Flush()
|
||||||
|
{
|
||||||
|
outputBuffer.Flush();
|
||||||
|
SendBuffer();
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes text's contents into console.
|
||||||
|
*
|
||||||
|
* Does not trigger console output, for that use `WriteLine()` or `Flush()`.
|
||||||
|
*
|
||||||
|
* @param message `Text` to output.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter Write(optional BaseText message)
|
||||||
|
{
|
||||||
|
outputBuffer.Insert(message);
|
||||||
|
if (needToResetColor) {
|
||||||
|
ResetColor();
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes text's contents into console.
|
||||||
|
* Result will be output immediately, starts a new line.
|
||||||
|
*
|
||||||
|
* @param message `Text` to output.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter WriteLine(optional BaseText message)
|
||||||
|
{
|
||||||
|
return Write(message).Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes text's indented contents into console.
|
||||||
|
*
|
||||||
|
* Acts like a `WriteLine()` call, except all output contents will be
|
||||||
|
* additionally indented by four whitespace symbols
|
||||||
|
* (including lines after line breaks).
|
||||||
|
*
|
||||||
|
* Result will be output immediately, starts a new line.
|
||||||
|
*
|
||||||
|
* @param message `Text` to output.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter WriteBlock(optional BaseText message)
|
||||||
|
{
|
||||||
|
outputBuffer.Insert(message).Flush();
|
||||||
|
SendBuffer(true);
|
||||||
|
if (needToResetColor) {
|
||||||
|
ResetColor();
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes text's contents into console as a player's chat message, causing them
|
||||||
|
* to appear on screen in vanilla UI.
|
||||||
|
*
|
||||||
|
* All the buffer stored in caller `ConsoleWriter` so far will be flushed.
|
||||||
|
* Result will be output immediately. Starts a new line.
|
||||||
|
*
|
||||||
|
* @param message `Text` to output.
|
||||||
|
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||||
|
*/
|
||||||
|
public final function ConsoleWriter Say(optional BaseText message)
|
||||||
|
{
|
||||||
|
outputBuffer.Insert(message).Flush();
|
||||||
|
SendBuffer(, true);
|
||||||
|
if (needToResetColor) {
|
||||||
|
ResetColor();
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all completed lines from an `outputBuffer`.
|
||||||
|
// Setting `indented` to `true` will cause additional four whitespaces to
|
||||||
|
// be added to the output.
|
||||||
|
private final function SendBuffer(optional bool asIndented, optional bool asSay)
|
||||||
|
{
|
||||||
|
local string prefix;
|
||||||
|
local ConsoleBuffer.LineRecord nextLineRecord;
|
||||||
|
local array<PlayerController> recipients;
|
||||||
|
|
||||||
|
recipients = GetRecipientsControllers();
|
||||||
|
while (outputBuffer.HasCompletedLines())
|
||||||
|
{
|
||||||
|
nextLineRecord = outputBuffer.PopNextLine();
|
||||||
|
if (nextLineRecord.wrappedLine) {
|
||||||
|
prefix = NEWLINE_PREFIX;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
prefix = BROKENLINE_PREFIX;
|
||||||
|
}
|
||||||
|
if (asIndented) {
|
||||||
|
prefix $= INDENTATION;
|
||||||
|
}
|
||||||
|
SendConsoleMessage(recipients, prefix $ nextLineRecord.contents, asSay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes `connectionService != none`, caller function must ensure that.
|
||||||
|
private final function SendConsoleMessage(
|
||||||
|
array<PlayerController> recipients,
|
||||||
|
string message,
|
||||||
|
bool asSay)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
for (i = 0; i < recipients.length; i += 1)
|
||||||
|
{
|
||||||
|
if (recipients[i] != none)
|
||||||
|
{
|
||||||
|
if (asSay) {
|
||||||
|
recipients[i].ClientMessage(message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
recipients[i].TeamMessage(none, message, 'AcediaConsole');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method for retrieving `PlayerController`s of recipients at the moment
|
||||||
|
// of the call
|
||||||
|
private final function array<PlayerController> GetRecipientsControllers()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local PlayerController nextRecipient;
|
||||||
|
local ConnectionService connectionService;
|
||||||
|
local array<PlayerController> recipients;
|
||||||
|
local array<ConnectionService.Connection> connections;
|
||||||
|
// No targets
|
||||||
|
if (targetType == CWTARGET_None) {
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
// Selected targets case
|
||||||
|
if (targetType != CWTARGET_All)
|
||||||
|
{
|
||||||
|
for (i = 0; i < outputTargets.length; i += 1)
|
||||||
|
{
|
||||||
|
nextRecipient = outputTargets[i].GetController();
|
||||||
|
if (nextRecipient != none) {
|
||||||
|
recipients[recipients.length] = nextRecipient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
// All players target case
|
||||||
|
connectionService =
|
||||||
|
ConnectionService(class'ConnectionService'.static.Require());
|
||||||
|
if (connectionService == none) {
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
connections = connectionService.GetActiveConnections();
|
||||||
|
for (i = 0; i < connections.length; i += 1)
|
||||||
|
{
|
||||||
|
if (connections[i].controllerReference != none) {
|
||||||
|
recipients[recipients.length] = connections[i].controllerReference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
NEWLINE_PREFIX = "| "
|
||||||
|
BROKENLINE_PREFIX = " "
|
||||||
|
INDENTATION = " "
|
||||||
|
}
|
||||||
126
kf_sources/AcediaCore/Classes/CoreGlobal.uc
Normal file
126
kf_sources/AcediaCore/Classes/CoreGlobal.uc
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* Base class for objects that will provide an access to a Acedia's client- and
|
||||||
|
* server-specific functionality by giving a reference to this object to all
|
||||||
|
* Acedia's objects and actors, emulating a global API namespace.
|
||||||
|
* Copyright 2022-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class CoreGlobal extends Object;
|
||||||
|
|
||||||
|
var protected bool initialized;
|
||||||
|
var protected class<AcediaAdapter> adapterClass;
|
||||||
|
|
||||||
|
var public SideEffectAPI sideEffects;
|
||||||
|
var public TimeAPI time;
|
||||||
|
var public DBAPI db;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition fatNoAdapterClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accessor to the generic `UnrealAPI`.
|
||||||
|
*/
|
||||||
|
public function UnrealAPI unreal_api()
|
||||||
|
{
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static function LevelCore GetLevelCore()
|
||||||
|
{
|
||||||
|
local LevelCore availableCore;
|
||||||
|
|
||||||
|
availableCore = class'ServerLevelCore'.static.GetInstance();
|
||||||
|
if (availableCore != none) {
|
||||||
|
return availableCore;
|
||||||
|
}
|
||||||
|
availableCore = class'ClientLevelCore'.static.GetInstance();
|
||||||
|
return availableCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static function CoreGlobal GetGenericInstance()
|
||||||
|
{
|
||||||
|
local ServerGlobal serverAPI;
|
||||||
|
local ClientGlobal clientAPI;
|
||||||
|
|
||||||
|
serverAPI = class'ServerGlobal'.static.GetInstance();
|
||||||
|
if (serverAPI != none && serverAPI.IsAvailable()) {
|
||||||
|
return serverAPI;
|
||||||
|
}
|
||||||
|
clientAPI = class'ClientGlobal'.static.GetInstance();
|
||||||
|
if (clientAPI != none && clientAPI.IsAvailable()) {
|
||||||
|
return clientAPI;
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method must perform initialization of the caller `...Global` instance.
|
||||||
|
*
|
||||||
|
* It must only be executed once (execution should be marked using
|
||||||
|
* `initialized` flag).
|
||||||
|
*/
|
||||||
|
protected function Initialize()
|
||||||
|
{
|
||||||
|
local MemoryAPI api;
|
||||||
|
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
if (adapterClass == none)
|
||||||
|
{
|
||||||
|
class'Global'.static.GetInstance().logger
|
||||||
|
.Auto(fatNoAdapterClass)
|
||||||
|
.ArgClass(self.class);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api = class'Global'.static.GetInstance().memory;
|
||||||
|
time = TimeAPI(api.Allocate(adapterClass.default.timeAPIClass));
|
||||||
|
db = DBAPI(api.Allocate(adapterClass.default.dbAPIClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks is caller `CoreGlobal` is available to be used.
|
||||||
|
*
|
||||||
|
* Server and client `CoreGlobal` instances are always created, so that they
|
||||||
|
* can be added to `AcediaObject`s and `AcediaActor`s at any time, even before
|
||||||
|
* they were initialized (whether they ever will be or not). This method
|
||||||
|
* allows one to check whether they were already initialized and can be used.
|
||||||
|
*
|
||||||
|
* @return `true` if caller `CoreGlobal` can be used and `false` otherwise.
|
||||||
|
*/
|
||||||
|
public function bool IsAvailable()
|
||||||
|
{
|
||||||
|
return initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes adapter class for the caller `...Global` instance.
|
||||||
|
*
|
||||||
|
* Must not do anything when caller `...Global` instance was already
|
||||||
|
* initialized or when passed adapter class is `none`.
|
||||||
|
*
|
||||||
|
* @param newAdapter New adapter class to use in the caller `...Global`
|
||||||
|
* instance.
|
||||||
|
* @return `true` if new adapter was set and `false` otherwise.
|
||||||
|
*/
|
||||||
|
public function bool SetAdapter(class<AcediaAdapter> newAdapter);
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
adapterClass = class'AcediaAdapter'
|
||||||
|
fatNoAdapterClass = (l=LOG_Fatal,m="`none` specified as an adapter for `%1` level core class. This should not have happened. AcediaCore cannot properly function.")
|
||||||
|
}
|
||||||
409
kf_sources/AcediaCore/Classes/DBAPI.uc
Normal file
409
kf_sources/AcediaCore/Classes/DBAPI.uc
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
/**
|
||||||
|
* API that provides methods for creating/destroying and managing available
|
||||||
|
* databases.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBAPI extends AcediaObject;
|
||||||
|
|
||||||
|
var private const class<Database> localDBClass;
|
||||||
|
|
||||||
|
// Store all already loaded databases to make sure we do not create two
|
||||||
|
// different `LocalDatabaseInstance` that are trying to make changes
|
||||||
|
// separately.
|
||||||
|
var private HashTable loadedLocalDatabases;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition infoLocalDatabaseCreated;
|
||||||
|
var private LoggerAPI.Definition infoLocalDatabaseDeleted;
|
||||||
|
var private LoggerAPI.Definition infoLocalDatabaseLoaded;
|
||||||
|
|
||||||
|
private final function CreateLocalDBMapIfMissing()
|
||||||
|
{
|
||||||
|
if (loadedLocalDatabases == none) {
|
||||||
|
loadedLocalDatabases = __().collections.EmptyHashTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads database based on the link.
|
||||||
|
*
|
||||||
|
* Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
|
||||||
|
* followed by the JSON pointer (possibly empty one) to the object inside it.
|
||||||
|
* "<type>" can be either "local" or "remote" and is necessary only when both
|
||||||
|
* local and remote database have the same name (which should be avoided).
|
||||||
|
* "<db_name>" refers to the database that we are expected
|
||||||
|
* to load, it has to consist of numbers and latin letters only.
|
||||||
|
*
|
||||||
|
* @param databaseLink Link from which to extract database's name.
|
||||||
|
* @return Database named "<db_name>" of type "<type>" from the `databaseLink`.
|
||||||
|
*/
|
||||||
|
public final function Database Load(BaseText databaseLink)
|
||||||
|
{
|
||||||
|
local Parser parser;
|
||||||
|
local Database result;
|
||||||
|
local MutableText databaseName;
|
||||||
|
|
||||||
|
if (databaseLink == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
parser = _.text.Parse(databaseLink);
|
||||||
|
// Only local DBs are supported for now!
|
||||||
|
// So just consume this prefix, if it's present.
|
||||||
|
parser.Match(P("[local]")).Confirm();
|
||||||
|
parser.R().MUntil(databaseName, _.text.GetCharacter(":")).MatchS(":");
|
||||||
|
if (!parser.Ok())
|
||||||
|
{
|
||||||
|
parser.FreeSelf();
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
result = LoadLocal(databaseName);
|
||||||
|
parser.FreeSelf();
|
||||||
|
databaseName.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts `MutableJSONPointer` from the database path, given by `databaseLink`.
|
||||||
|
*
|
||||||
|
* Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
|
||||||
|
* followed by the JSON pointer (possibly empty one) to the object inside it.
|
||||||
|
* "<type>" can be either "local" or "remote" and is necessary only when both
|
||||||
|
* local and remote database have the same name (which should be avoided).
|
||||||
|
* "<db_name>" refers to the database that we are expected
|
||||||
|
* to load, it has to consist of numbers and latin letters only.
|
||||||
|
* This method returns `MutableJSONPointer` that comes after type-name pair.
|
||||||
|
*
|
||||||
|
* @param Link from which to extract `MutableJSONPointer`.
|
||||||
|
* @return `MutableJSONPointer` from the database link.
|
||||||
|
* Guaranteed to not be `none` if provided argument `databaseLink`
|
||||||
|
* is not `none`.
|
||||||
|
*/
|
||||||
|
public final function MutableJsonPointer GetMutablePointer(BaseText databaseLink)
|
||||||
|
{
|
||||||
|
local int slashIndex;
|
||||||
|
local Text textPointer;
|
||||||
|
local MutableJsonPointer result;
|
||||||
|
|
||||||
|
if (databaseLink == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
slashIndex = databaseLink.IndexOf(P(":"));
|
||||||
|
if (slashIndex < 0) {
|
||||||
|
return MutableJsonPointer(_.memory.Allocate(class'MutableJsonPointer'));
|
||||||
|
}
|
||||||
|
textPointer = databaseLink.Copy(slashIndex + 1);
|
||||||
|
result = _.json.MutablePointer(textPointer);
|
||||||
|
textPointer.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts `JSONPointer` from the database path, given by `databaseLink`.
|
||||||
|
*
|
||||||
|
* Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
|
||||||
|
* followed by the JSON pointer (possibly empty one) to the object inside it.
|
||||||
|
* "<type>" can be either "local" or "remote" and is necessary only when both
|
||||||
|
* local and remote database have the same name (which should be avoided).
|
||||||
|
* "<db_name>" refers to the database that we are expected
|
||||||
|
* to load, it has to consist of numbers and latin letters only.
|
||||||
|
* This method returns `JSONPointer` that comes after type-name pair.
|
||||||
|
*
|
||||||
|
* @param Link from which to extract `JSONPointer`.
|
||||||
|
* @return `JSONPointer` from the database link.
|
||||||
|
* Guaranteed to not be `none` if provided argument `databaseLink`
|
||||||
|
* is not `none`.
|
||||||
|
*/
|
||||||
|
public final function JSONPointer GetPointer(BaseText databaseLink)
|
||||||
|
{
|
||||||
|
local int slashIndex;
|
||||||
|
local Text textPointer;
|
||||||
|
local JSONPointer result;
|
||||||
|
|
||||||
|
if (databaseLink == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
slashIndex = databaseLink.IndexOf(P(":"));
|
||||||
|
if (slashIndex < 0) {
|
||||||
|
return JSONPointer(_.memory.Allocate(class'JSONPointer'));
|
||||||
|
}
|
||||||
|
textPointer = databaseLink.Copy(slashIndex + 1);
|
||||||
|
result = _.json.Pointer(textPointer);
|
||||||
|
textPointer.FreeSelf();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a new `DBConnection` to the data referred to by the database link.
|
||||||
|
*
|
||||||
|
* Opened `DBConnection` doesn't automatically start a connection, so you
|
||||||
|
* need to call its `Connect()` method.
|
||||||
|
*
|
||||||
|
* @param databaseLink Database link to the data we want to connect to.
|
||||||
|
* @return Initialized `DBConnection` in case given link is valid and `none`
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public final function DBConnection OpenConnection(BaseText databaseLink)
|
||||||
|
{
|
||||||
|
local DBConnection result;
|
||||||
|
local Parser parser;
|
||||||
|
local Database databaseToConnect;
|
||||||
|
local JSONPointer locationToConnect;
|
||||||
|
local MutableText databaseName, textPointer;
|
||||||
|
|
||||||
|
if (databaseLink == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
parser = _.text.Parse(databaseLink);
|
||||||
|
// Only local DBs are supported for now!
|
||||||
|
// So just consume this prefix, if it's present.
|
||||||
|
parser.Match(P("[local]")).Confirm();
|
||||||
|
textPointer = parser
|
||||||
|
.R()
|
||||||
|
.MUntil(databaseName, _.text.GetCharacter(":"))
|
||||||
|
.MatchS(":")
|
||||||
|
.GetRemainderM();
|
||||||
|
if (parser.Ok())
|
||||||
|
{
|
||||||
|
databaseToConnect = LoadLocal(databaseName);
|
||||||
|
locationToConnect = _.json.Pointer(textPointer);
|
||||||
|
result = DBConnection(_.memory.Allocate(class'DBConnection'));
|
||||||
|
result.Initialize(databaseToConnect, locationToConnect);
|
||||||
|
_.memory.Free(databaseToConnect);
|
||||||
|
_.memory.Free(locationToConnect);
|
||||||
|
}
|
||||||
|
parser.FreeSelf();
|
||||||
|
_.memory.Free(databaseName);
|
||||||
|
_.memory.Free(textPointer);
|
||||||
|
if (result != none && !result.IsInitialized())
|
||||||
|
{
|
||||||
|
result.FreeSelf();
|
||||||
|
result = none;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates new local database with name `databaseName`.
|
||||||
|
*
|
||||||
|
* This method will fail if:
|
||||||
|
* 1. `databaseName` is `none` or empty;
|
||||||
|
* 2. Local database with name `databaseName` already exists.
|
||||||
|
*
|
||||||
|
* @param databaseName Name for the new database.
|
||||||
|
* @return Reference to created database. Returns `none` iff method failed.
|
||||||
|
*/
|
||||||
|
public final function LocalDatabaseInstance NewLocal(BaseText databaseName)
|
||||||
|
{
|
||||||
|
local DBRecord rootRecord;
|
||||||
|
local Text rootRecordName;
|
||||||
|
local Text databaseNameCopy;
|
||||||
|
local LocalDatabase newConfig;
|
||||||
|
local LocalDatabaseInstance newLocalDBInstance;
|
||||||
|
|
||||||
|
CreateLocalDBMapIfMissing();
|
||||||
|
if (databaseName == none) return none;
|
||||||
|
if (!databaseName.IsValidName()) return none;
|
||||||
|
newConfig = class'LocalDatabase'.static.Load(databaseName);
|
||||||
|
if (newConfig == none) return none;
|
||||||
|
if (newConfig.HasDefinedRoot()) return none;
|
||||||
|
if (loadedLocalDatabases.HasKey(databaseName)) return none;
|
||||||
|
|
||||||
|
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
|
||||||
|
databaseNameCopy = databaseName.Copy();
|
||||||
|
loadedLocalDatabases.SetItem(databaseNameCopy, newLocalDBInstance);
|
||||||
|
rootRecord = class'DBRecord'.static.NewRecord(databaseName);
|
||||||
|
rootRecordName = _.text.FromString(string(rootRecord.name));
|
||||||
|
newConfig.SetRootName(rootRecordName);
|
||||||
|
newConfig.Save();
|
||||||
|
newLocalDBInstance.Initialize(newConfig, rootRecord);
|
||||||
|
_.logger.Auto(infoLocalDatabaseCreated).Arg(databaseNameCopy);
|
||||||
|
_.memory.Free(rootRecordName);
|
||||||
|
return newLocalDBInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and returns local database with the name `databaseName`.
|
||||||
|
*
|
||||||
|
* If specified database is already loaded - simply returns it's reference
|
||||||
|
* (consequent calls to `LoadLocal()` will keep returning the same reference,
|
||||||
|
* unless database is deleted).
|
||||||
|
*
|
||||||
|
* @param databaseName Name of the database to load.
|
||||||
|
* @return Loaded local database. `none` if it does not exist.
|
||||||
|
*/
|
||||||
|
public final function LocalDatabaseInstance LoadLocal(BaseText databaseName)
|
||||||
|
{
|
||||||
|
local DBRecord rootRecord;
|
||||||
|
local Text rootRecordName;
|
||||||
|
local LocalDatabase newConfig;
|
||||||
|
local LocalDatabaseInstance newLocalDBInstance, result;
|
||||||
|
local Text dbKey;
|
||||||
|
|
||||||
|
if (databaseName == none) {
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
CreateLocalDBMapIfMissing();
|
||||||
|
dbKey = databaseName.Copy();
|
||||||
|
if (loadedLocalDatabases.HasKey(dbKey))
|
||||||
|
{
|
||||||
|
result = LocalDatabaseInstance(loadedLocalDatabases.GetItem(dbKey));
|
||||||
|
_.memory.Free(dbKey);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// No need to check `dbKey` for being valid,
|
||||||
|
// since `Load()` will just return `none` if it is not.
|
||||||
|
newConfig = class'LocalDatabase'.static.Load(dbKey);
|
||||||
|
if (newConfig == none) {
|
||||||
|
_.memory.Free(dbKey);
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
if (!newConfig.HasDefinedRoot() && !newConfig.ShouldCreateIfMissing()) {
|
||||||
|
_.memory.Free(dbKey);
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
|
||||||
|
loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance);
|
||||||
|
dbKey.FreeSelf();
|
||||||
|
if (newConfig.HasDefinedRoot())
|
||||||
|
{
|
||||||
|
rootRecordName = newConfig.GetRootName();
|
||||||
|
rootRecord = class'DBRecord'.static.LoadRecord(rootRecordName, dbKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rootRecord = class'DBRecord'.static.NewRecord(dbKey);
|
||||||
|
rootRecordName = _.text.FromString(string(rootRecord.name));
|
||||||
|
newConfig.SetRootName(rootRecordName);
|
||||||
|
newConfig.Save();
|
||||||
|
}
|
||||||
|
newLocalDBInstance.Initialize(newConfig, rootRecord);
|
||||||
|
_.logger.Auto(infoLocalDatabaseLoaded).Arg(dbKey);
|
||||||
|
_.memory.Free(rootRecordName);
|
||||||
|
return newLocalDBInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if local database with the name `databaseName` already exists.
|
||||||
|
*
|
||||||
|
* @param databaseName Name of the database to check.
|
||||||
|
* @return `true` if database with specified name exists and `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool ExistsLocal(BaseText databaseName)
|
||||||
|
{
|
||||||
|
local bool result;
|
||||||
|
local LocalDatabaseInstance instance;
|
||||||
|
|
||||||
|
instance = LoadLocal(databaseName);
|
||||||
|
result = (instance != none);
|
||||||
|
_.memory.Free(instance);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes local database with name `databaseName`.
|
||||||
|
*
|
||||||
|
* @param databaseName Name of the database to delete.
|
||||||
|
* @return `true` if database with specified name existed and was deleted and
|
||||||
|
* `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool DeleteLocal(BaseText databaseName)
|
||||||
|
{
|
||||||
|
local LocalDatabase localDatabaseConfig;
|
||||||
|
local LocalDatabaseInstance localDatabase;
|
||||||
|
local HashTable.Entry dbEntry;
|
||||||
|
|
||||||
|
if (databaseName == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CreateLocalDBMapIfMissing();
|
||||||
|
// To delete database we first need to load it
|
||||||
|
localDatabase = LoadLocal(databaseName);
|
||||||
|
if (localDatabase != none)
|
||||||
|
{
|
||||||
|
localDatabaseConfig = localDatabase.GetConfig();
|
||||||
|
localDatabase.WriteToDisk();
|
||||||
|
_.memory.Free(localDatabase);
|
||||||
|
}
|
||||||
|
dbEntry = loadedLocalDatabases.TakeEntry(databaseName);
|
||||||
|
// Delete `LocalDatabaseInstance` before erasing the package,
|
||||||
|
// to allow it to clean up safely
|
||||||
|
_.memory.Free(dbEntry.key);
|
||||||
|
_.memory.Free(dbEntry.value);
|
||||||
|
if (localDatabaseConfig != none)
|
||||||
|
{
|
||||||
|
EraseAllPackageData(localDatabaseConfig.GetPackageName());
|
||||||
|
localDatabaseConfig.DeleteSelf();
|
||||||
|
_.logger.Auto(infoLocalDatabaseDeleted).Arg(databaseName.Copy());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function EraseAllPackageData(BaseText packageToErase)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local string packageName;
|
||||||
|
local GameInfo game;
|
||||||
|
local DBRecord nextRecord;
|
||||||
|
local array<DBRecord> allRecords;
|
||||||
|
|
||||||
|
packageName = _.text.IntoString(packageToErase);
|
||||||
|
if (packageName == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
game = __level().unreal_api().GetGameType();
|
||||||
|
game.DeletePackage(packageName);
|
||||||
|
// Delete any leftover objects. This has to be done *after*
|
||||||
|
// `DeletePackage()` call, otherwise removed garbage can reappear.
|
||||||
|
// No clear idea why it works this way.
|
||||||
|
foreach game.AllDataObjects(class'DBRecord', nextRecord, packageName) {
|
||||||
|
allRecords[allRecords.length] = nextRecord;
|
||||||
|
}
|
||||||
|
for (i = 0; i < allRecords.length; i += 1)
|
||||||
|
{
|
||||||
|
game.DeleteDataObject( class'DBRecord', string(allRecords[i].name),
|
||||||
|
packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array of names of all available local databases.
|
||||||
|
*
|
||||||
|
* @return List of names of all local databases.
|
||||||
|
*/
|
||||||
|
public final function array<Text> ListLocal()
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<Text> dbNames;
|
||||||
|
local array<string> dbNamesAsStrings;
|
||||||
|
|
||||||
|
dbNamesAsStrings = GetPerObjectNames( "AcediaDB",
|
||||||
|
string(class'LocalDatabase'.name),
|
||||||
|
MaxInt);
|
||||||
|
for (i = 0; i < dbNamesAsStrings.length; i += 1) {
|
||||||
|
dbNames[dbNames.length] = _.text.FromString(dbNamesAsStrings[i]);
|
||||||
|
}
|
||||||
|
return dbNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
localDBClass = class'LocalDatabaseInstance'
|
||||||
|
infoLocalDatabaseCreated = (l=LOG_Info,m="Local database \"%1\" was created.")
|
||||||
|
infoLocalDatabaseLoaded = (l=LOG_Info,m="Local database \"%1\" was loaded.")
|
||||||
|
infoLocalDatabaseDeleted = (l=LOG_Info,m="Local database \"%1\" was deleted.")
|
||||||
|
}
|
||||||
1092
kf_sources/AcediaCore/Classes/DBCache.uc
Normal file
1092
kf_sources/AcediaCore/Classes/DBCache.uc
Normal file
File diff suppressed because it is too large
Load Diff
49
kf_sources/AcediaCore/Classes/DBCheckTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBCheckTask.uc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Variant of `DBTask` for `CheckDataType()` query.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBCheckTask extends DBTask;
|
||||||
|
|
||||||
|
var private Database.DataType queryTypeResponse;
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
Database.DataType type,
|
||||||
|
Database source,
|
||||||
|
int requestID) {}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
queryTypeResponse = JSON_Undefined;
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetDataType(Database.DataType type)
|
||||||
|
{
|
||||||
|
queryTypeResponse = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function CompleteSelf(Database source)
|
||||||
|
{
|
||||||
|
connect(GetResult(), queryTypeResponse, source, GetRequestID());
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
794
kf_sources/AcediaCore/Classes/DBConnection.uc
Normal file
794
kf_sources/AcediaCore/Classes/DBConnection.uc
Normal file
@ -0,0 +1,794 @@
|
|||||||
|
/**
|
||||||
|
* Auxiliary object for simplifying working with databases.
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBConnection extends AcediaObject
|
||||||
|
dependson(Database)
|
||||||
|
dependson(DBCache);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # `DBConnection`
|
||||||
|
*
|
||||||
|
* Auxiliary object for simplifying working with databases.
|
||||||
|
* `Database` class has a rather simple interface and there are several issues
|
||||||
|
* that constantly arise when trying to use it:
|
||||||
|
*
|
||||||
|
* 1. If one tries to read/write data from/to specific location in
|
||||||
|
* the database, then `JSONPointer` has to be kept in addition to
|
||||||
|
* the `Database` reference at all times;
|
||||||
|
* 2. One has to perform initial checks about whether database can even be
|
||||||
|
* connected to, if at desired location there is a proper data
|
||||||
|
* structure, etc.. If one also wants to start using data before
|
||||||
|
* database's response or after its failure, then the same work of
|
||||||
|
* duplication that data locally must be performed.
|
||||||
|
* 3. Instead of immediate operations, database operations are delayed and
|
||||||
|
* user has to handle their results asynchronously in separate methods.
|
||||||
|
*
|
||||||
|
* `DBConnection` takes care of these issues by providing you synchronous
|
||||||
|
* methods for accessing cached version of the data at the given location that
|
||||||
|
* is duplicated to the database as soon as possible (and even if its no longer
|
||||||
|
* possible in the case of a failure).
|
||||||
|
* `DBConnection` makes immediate changes on the local cache and reports
|
||||||
|
* about possible failures with database later through signals `OnEditResult()`
|
||||||
|
* (reports about success of writing operations) and `OnStateChanged()`
|
||||||
|
* (reports about state changes of connected database, including complete
|
||||||
|
* failures).
|
||||||
|
* The only reading of database's values occurs at the moment of connecting
|
||||||
|
* to it, after that all the data is read from the local cache.
|
||||||
|
* Possible `DBConnection` states include:
|
||||||
|
*
|
||||||
|
* * `DBCS_Idle` - database was created, but not yet connected;
|
||||||
|
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
|
||||||
|
* its result is still unknown;
|
||||||
|
* * `DBCS_Connected` - database is connected and properly working;
|
||||||
|
* * `DBCS_Disconnected` - database was manually disconnected and now
|
||||||
|
* operates solely on the local cache. Once disconnected `DBConnection`
|
||||||
|
* cannot be reconnected - create a new one instead.
|
||||||
|
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
|
||||||
|
* can be found out with `GetError()` method:
|
||||||
|
*
|
||||||
|
* * `FDE_None` - no error has yet occurred;
|
||||||
|
* * `FDE_CannotReadRootData` - root data couldn't be read from
|
||||||
|
* the database, most likely because of the invalid `JSONPointer`
|
||||||
|
* for the root value;
|
||||||
|
* * `FDE_UnexpectedRootData` - root data was read from the database,
|
||||||
|
* but has an unexpected format. `DBConnection` expects either
|
||||||
|
* JSON object or array (can be specified which one) and this error
|
||||||
|
* occurs if its not found at specified database's location;
|
||||||
|
* * `FDE_Unknown` - database returned `DBR_InvalidDatabase` result
|
||||||
|
* for one of the queries. This is likely to happen when database
|
||||||
|
* is damaged. More precise details depend on the implementation,
|
||||||
|
* but result boils down to database being unusable.
|
||||||
|
*
|
||||||
|
* ## Usage
|
||||||
|
*
|
||||||
|
* Usage is straightforward:
|
||||||
|
*
|
||||||
|
* 1. Initialize with appropriate database by calling `Initialize()`;
|
||||||
|
* 2. Start connecting to it by calling `Connect()`;
|
||||||
|
* 3. Use `ReadDataByJSON()`/`WriteDataByJSON()` to read/write into
|
||||||
|
* the connected database;
|
||||||
|
*
|
||||||
|
* You can use it transparently even if database connection fails, but if you
|
||||||
|
* need to handle such failure - connect to the `OnStateChanged()` signal for
|
||||||
|
* tracking `DBCS_FaultyDatabase` state and to `OnEditResult()` for tracking
|
||||||
|
* success of writing operations.
|
||||||
|
*
|
||||||
|
* ## Implementation
|
||||||
|
*
|
||||||
|
* The brunt of work is done by `DBCache` and most of the logic in this
|
||||||
|
* class is for tracking state of the connection to the database and then
|
||||||
|
* reporting these changes through its own signals.
|
||||||
|
* The most notable hidden functionality is tracking requests by ID -
|
||||||
|
* `DBConnection` makes each request with unique ID and then stores them inside
|
||||||
|
* `requestIDs` (and in case of write requests - along with corresponding
|
||||||
|
* `JSONPointer` inside `queuedPointers`). This is necessary because:
|
||||||
|
*
|
||||||
|
* 1. Even if `DBConnection` gets reallocated - as far as UnrealScript is
|
||||||
|
* concerned it is still the same object, so the responses we no longer
|
||||||
|
* care about will still arrive. Keeping track of the IDs that interest
|
||||||
|
* us inside `requestIDs` allows us to filter out responses we no
|
||||||
|
* longer care about;
|
||||||
|
* 2. Tracking corresponding (to the IDs) `queuedPointers` also allow us
|
||||||
|
* to know responses to which writing requests we've received.
|
||||||
|
*
|
||||||
|
* ## Remarks
|
||||||
|
*
|
||||||
|
* Currently `DBConnection` doesn't support important feature of *incrementing*
|
||||||
|
* data that allows several sources to safely change the same value
|
||||||
|
* asynchronously. We're skipping on it right now to save time as its not
|
||||||
|
* really currently needed, however it will be added in the future.
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum DBConnectionState
|
||||||
|
{
|
||||||
|
// `DBConnection` was created, but didn't yet attempt to connect
|
||||||
|
// to database
|
||||||
|
DBCS_Idle,
|
||||||
|
// `DBConnection` is currently connecting
|
||||||
|
DBCS_Connecting,
|
||||||
|
// `DBConnection` has already connected without errors
|
||||||
|
DBCS_Connected,
|
||||||
|
// `DBConnection` was manually disconnected
|
||||||
|
DBCS_Disconnected,
|
||||||
|
// `DBConnection` was disconnected because of the database error,
|
||||||
|
// @see `FaultyDatabaseError` for more.
|
||||||
|
DBCS_FaultyDatabase
|
||||||
|
};
|
||||||
|
// Current connection state
|
||||||
|
var private DBConnectionState currentState;
|
||||||
|
|
||||||
|
enum FaultyDatabaseError
|
||||||
|
{
|
||||||
|
// No error has occurred yet
|
||||||
|
FDE_None,
|
||||||
|
// Root data isn't available
|
||||||
|
FDE_CannotReadRootData,
|
||||||
|
// Root data has incorrect format
|
||||||
|
FDE_UnexpectedRootData,
|
||||||
|
// Some internal error in database has occurred
|
||||||
|
FDE_Unknown
|
||||||
|
};
|
||||||
|
// Reason for why current state is in `DBCS_FaultyDatabase` state;
|
||||||
|
// `FDE_None` if `DBConnection` is in any other state.
|
||||||
|
var private FaultyDatabaseError dbFailureReason;
|
||||||
|
// Keeps track whether root value read from the database was of the correct
|
||||||
|
// type. Only relevant after database tried connecting (i.e. it is in states
|
||||||
|
// `DBCS_Connected`, `DBCS_Disconnected` or `DBCS_FaultyDatabase`).
|
||||||
|
// This variable helps us determine whether error should be
|
||||||
|
// `FDE_CannotReadRootData` or `FDE_UnexpectedRootData`.
|
||||||
|
var private bool rootIsOfExpectedType;
|
||||||
|
|
||||||
|
// `Database` + `JSONPointer` combo that point at the data we want to
|
||||||
|
// connect to
|
||||||
|
var private Database dbInstance;
|
||||||
|
var private JSONPointer rootPointer;
|
||||||
|
// Local, cached version of that data
|
||||||
|
var private DBCache localCache;
|
||||||
|
|
||||||
|
// This is basically an array of (`int`, `JSONPointer`) pairs for tracking
|
||||||
|
// database requests of interest
|
||||||
|
var private array<int> requestIDs;
|
||||||
|
var private array<JSONPointer> queuedPointers;
|
||||||
|
// Next usable ID. `DBConnection` is expected to always use unique IDs.
|
||||||
|
var private int nextRequestID;
|
||||||
|
|
||||||
|
|
||||||
|
var private DBConnection_StateChanged_Signal onStateChangedSignal;
|
||||||
|
var private DBConnection_EditResult_Signal onEditResultSignal;
|
||||||
|
|
||||||
|
var private LoggerAPI.Definition errDoubleInitialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that will be emitted whenever `DBConnection` changes state.
|
||||||
|
* List of the available states:
|
||||||
|
*
|
||||||
|
* * `DBCS_Idle` - database was created, but not yet connected;
|
||||||
|
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
|
||||||
|
* its result is still unknown;
|
||||||
|
* * `DBCS_Connected` - database is connected and properly working;
|
||||||
|
* * `DBCS_Disconnected` - database was manually disconnected and now
|
||||||
|
* operates solely on the local cache. Once disconnected `DBConnection`
|
||||||
|
* cannot be reconnected - create a new one instead.
|
||||||
|
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
|
||||||
|
* can be found out with `GetError()` method.
|
||||||
|
*
|
||||||
|
* This method *is not* called when `DBConnection` is deallocated.
|
||||||
|
*
|
||||||
|
* [Signature]
|
||||||
|
* void <slot>(
|
||||||
|
* DBConnection instance,
|
||||||
|
* DBConnectionState oldState,
|
||||||
|
* DBConnectionState newState)
|
||||||
|
*
|
||||||
|
* @param instance Instance of the `DBConnection` that has changed state.
|
||||||
|
* @param oldState State it was previously in.
|
||||||
|
* @param oldState New state.
|
||||||
|
*/
|
||||||
|
/* SIGNAL */
|
||||||
|
public final function DBConnection_StateChanged_Slot OnStateChanged(
|
||||||
|
AcediaObject receiver)
|
||||||
|
{
|
||||||
|
return DBConnection_StateChanged_Slot(onStateChangedSignal
|
||||||
|
.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that will be emitted whenever `DBConnection` receives response from
|
||||||
|
* connected database about success of writing operation.
|
||||||
|
*
|
||||||
|
* Responses to old requests can still be received even if database got
|
||||||
|
* disconnected.
|
||||||
|
*
|
||||||
|
* Any emissions of this signal when the database's state is `DBCS_Connecting`
|
||||||
|
* correspond to reapplying edits made prior connection was established.
|
||||||
|
*
|
||||||
|
* [Signature]
|
||||||
|
* void <slot>(JSONPointer editLocation, bool isSuccessful)
|
||||||
|
*
|
||||||
|
* @param editLocation Location of the writing operation this is
|
||||||
|
* a response to.
|
||||||
|
* @param isSuccessful Whether writing operation ended in the success.
|
||||||
|
*/
|
||||||
|
/* SIGNAL */
|
||||||
|
public final function DBConnection_EditResult_Slot OnEditResult(
|
||||||
|
AcediaObject receiver)
|
||||||
|
{
|
||||||
|
return DBConnection_EditResult_Slot(onEditResultSignal.NewSlot(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
localCache = DBCache(_.memory.Allocate(class'DBCache'));
|
||||||
|
onStateChangedSignal = DBConnection_StateChanged_Signal(
|
||||||
|
_.memory.Allocate(class'DBConnection_StateChanged_Signal'));
|
||||||
|
onEditResultSignal = DBConnection_EditResult_Signal(
|
||||||
|
_.memory.Allocate(class'DBConnection_EditResult_Signal'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
rootIsOfExpectedType = false;
|
||||||
|
currentState = DBCS_Idle;
|
||||||
|
_.memory.Free(dbInstance);
|
||||||
|
_.memory.Free(rootPointer);
|
||||||
|
_.memory.Free(localCache);
|
||||||
|
dbInstance = none;
|
||||||
|
rootPointer = none;
|
||||||
|
localCache = none;
|
||||||
|
_.memory.FreeMany(queuedPointers);
|
||||||
|
queuedPointers.length = 0;
|
||||||
|
requestIDs.length = 0;
|
||||||
|
// Free signals
|
||||||
|
_.memory.Free(onStateChangedSignal);
|
||||||
|
_.memory.Free(onEditResultSignal);
|
||||||
|
onStateChangedSignal = none;
|
||||||
|
onEditResultSignal = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes `DBConnection` with database and location to which it must be
|
||||||
|
* connected.
|
||||||
|
*
|
||||||
|
* For the initialization to be successful `DBConnection` must not yet be
|
||||||
|
* initialized and `initDatabase` be not `none`.
|
||||||
|
*
|
||||||
|
* To check whether caller `DBConnection` is initialized
|
||||||
|
* @see `IsInitialized()`.
|
||||||
|
*
|
||||||
|
* @param initDatabase Database with data we want to connect to.
|
||||||
|
* @param initRootPointer Location of said data in the given database.
|
||||||
|
* If `none` is specified, uses root object of the database.
|
||||||
|
* @return `true` if initialization was successful and `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool Initialize(
|
||||||
|
Database initDatabase,
|
||||||
|
optional BaseJSONPointer initRootPointer)
|
||||||
|
{
|
||||||
|
if (IsInitialized()) return false;
|
||||||
|
if (initDatabase == none) return false;
|
||||||
|
if (!initDatabase.IsAllocated()) return false;
|
||||||
|
|
||||||
|
dbInstance = initDatabase;
|
||||||
|
dbInstance.NewRef();
|
||||||
|
if (initRootPointer != none) {
|
||||||
|
rootPointer = initRootPointer.Copy();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rootPointer = _.json.Pointer();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads data from the `DBConnection` at the location defined by the given
|
||||||
|
* `JSONPointer`.
|
||||||
|
*
|
||||||
|
* If data was initialized with non-empty location for the root data, then
|
||||||
|
* actual returned data's location in the database is defined by appending
|
||||||
|
* given `pointer` to that root pointer.
|
||||||
|
*
|
||||||
|
* Data is actually always read from the local cache and, therefore, we can
|
||||||
|
* read data we've written via `DBConnection` even without actually connecting
|
||||||
|
* to the database.
|
||||||
|
*
|
||||||
|
* @param pointer Location from which to read the data.
|
||||||
|
* @return Data recorded for the given `JSONPointer`. `none` if it is missing.
|
||||||
|
*/
|
||||||
|
public final function AcediaObject ReadDataByJSON(BaseJSONPointer pointer)
|
||||||
|
{
|
||||||
|
return localCache.Read(pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes given data into the `DBConnection` at the location defined by
|
||||||
|
* the given `JSONPointer`.
|
||||||
|
*
|
||||||
|
* If data was initialized with non-empty location for the root data, then
|
||||||
|
* actual location for writing data in the database is defined by appending
|
||||||
|
* given `pointer` to that root pointer.
|
||||||
|
*
|
||||||
|
* Data is actually always also written into the local cache, even when
|
||||||
|
* there is no connection to the database. Once connection is made - all valid
|
||||||
|
* changes will be duplicated into it.
|
||||||
|
* Success of failure of actually making changes into the database can be
|
||||||
|
* tracked with `OnEditResult()` signal.
|
||||||
|
*
|
||||||
|
* This operation also returns immediate indication of whether it has
|
||||||
|
* failed *locally*. This can happen when trying to perform operation
|
||||||
|
* impossible for the local cache. For example, we cannot write any data at
|
||||||
|
* location "/a/b/c" for the JSON object "{"a":45.6}".
|
||||||
|
* If operation ended in failure locally, then change to database won't
|
||||||
|
* even be attempted.
|
||||||
|
*
|
||||||
|
* @param pointer Location into which to write the data.
|
||||||
|
* @param data Data to write into the connection.
|
||||||
|
* @return `true` on success and `false` on failure. `true` is required for
|
||||||
|
* the writing database request to be made.
|
||||||
|
*/
|
||||||
|
public final function bool WriteDataByJSON(
|
||||||
|
BaseJSONPointer pointer,
|
||||||
|
AcediaObject data)
|
||||||
|
{
|
||||||
|
if (pointer == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (localCache.Write(pointer, data))
|
||||||
|
{
|
||||||
|
ModifyDataInDatabase(pointer, data, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments given data into the `DBConnection` at the location defined by
|
||||||
|
* the given `JSONPointer`.
|
||||||
|
*
|
||||||
|
* If data was initialized with non-empty location for the root data, then
|
||||||
|
* actual location for incrementing data in the database is defined by
|
||||||
|
* appending given `pointer` to that root pointer.
|
||||||
|
*
|
||||||
|
* Data is actually always also incremented into the local cache, even when
|
||||||
|
* there is no connection to the database. Once connection is made - all valid
|
||||||
|
* changes will be duplicated into it.
|
||||||
|
* Success of failure of actually making changes into the database can be
|
||||||
|
* tracked with `OnEditResult()` signal.
|
||||||
|
*
|
||||||
|
* This operation also returns immediate indication of whether it has
|
||||||
|
* failed *locally*. This can happen when trying to perform operation
|
||||||
|
* impossible for the local cache. For example, we cannot increment any data at
|
||||||
|
* location "/a/b/c" for the JSON object "{"a":45.6}".
|
||||||
|
* If operation ended in failure locally, then change to database won't
|
||||||
|
* even be attempted.
|
||||||
|
*
|
||||||
|
* @param pointer Location at which to increment the data.
|
||||||
|
* @param data Data with which to increment value inside the connection.
|
||||||
|
* @return `true` on success and `false` on failure. `true` is required for
|
||||||
|
* the incrementing database request to be made.
|
||||||
|
*/
|
||||||
|
public final function bool IncrementDataByJSON(
|
||||||
|
BaseJSONPointer pointer,
|
||||||
|
AcediaObject data)
|
||||||
|
{
|
||||||
|
if (pointer == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (localCache.Increment(pointer, data))
|
||||||
|
{
|
||||||
|
ModifyDataInDatabase(pointer, data, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes data from the `DBConnection` at the location defined by the given
|
||||||
|
* `JSONPointer`.
|
||||||
|
*
|
||||||
|
* If data was initialized with non-empty location for the root data, then
|
||||||
|
* actual location at which to remove data in the database is defined by
|
||||||
|
* appending given `pointer` to that root pointer.
|
||||||
|
*
|
||||||
|
* Data is actually always also removed from the local cache, even when
|
||||||
|
* there is no connection to the database. Once connection is made - all valid
|
||||||
|
* changes will be duplicated into it.
|
||||||
|
* Success of failure of actually making changes into the database can be
|
||||||
|
* tracked with `OnEditResult()` signal.
|
||||||
|
*
|
||||||
|
* This operation also returns immediate indication of whether it has
|
||||||
|
* failed *locally*.
|
||||||
|
* If operation ended in failure locally, then change to database won't
|
||||||
|
* even be attempted.
|
||||||
|
*
|
||||||
|
* @param pointer Location at which to remove data.
|
||||||
|
* @return `true` on success and `false` on failure. `true` is required for
|
||||||
|
* the removal database request to be made.
|
||||||
|
*/
|
||||||
|
public final function bool RemoveDataByJSON(BaseJSONPointer pointer)
|
||||||
|
{
|
||||||
|
if (pointer == none) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (localCache.Remove(pointer))
|
||||||
|
{
|
||||||
|
RemoveDataInDatabase(pointer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function ModifyDataInDatabase(
|
||||||
|
BaseJSONPointer pointer,
|
||||||
|
AcediaObject data,
|
||||||
|
bool increment)
|
||||||
|
{
|
||||||
|
local MutableJSONPointer dataPointer;
|
||||||
|
|
||||||
|
if (currentState != DBCS_Connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataPointer = rootPointer.MutableCopy();
|
||||||
|
dataPointer.Append(pointer);
|
||||||
|
// `dataPointer` is consumed by `RegisterNextRequestID()` method
|
||||||
|
if (increment)
|
||||||
|
{
|
||||||
|
dbInstance
|
||||||
|
.IncrementData(
|
||||||
|
dataPointer,
|
||||||
|
data,
|
||||||
|
RegisterNextRequestID(/*take*/ dataPointer.Copy()))
|
||||||
|
.connect = EditDataHandler;
|
||||||
|
_.memory.Free(dataPointer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dbInstance
|
||||||
|
.WriteData(dataPointer, data, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
|
||||||
|
.connect = EditDataHandler;
|
||||||
|
_.memory.Free(dataPointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function RemoveDataInDatabase(BaseJSONPointer pointer)
|
||||||
|
{
|
||||||
|
local MutableJSONPointer dataPointer;
|
||||||
|
|
||||||
|
if (currentState != DBCS_Connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataPointer = rootPointer.MutableCopy();
|
||||||
|
dataPointer.Append(pointer);
|
||||||
|
// `dataPointer` is consumed by `RegisterNextRequestID()` method
|
||||||
|
dbInstance
|
||||||
|
.RemoveData(dataPointer, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
|
||||||
|
.connect = EditDataHandler;
|
||||||
|
_.memory.Free(dataPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks caller `DBConnection` was successfully initialized.
|
||||||
|
*
|
||||||
|
* @return `true` if caller `DBConnection` was initialized and `false`
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool IsInitialized()
|
||||||
|
{
|
||||||
|
return (dbInstance != none);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current state of the connection of `DBConnection` to the database
|
||||||
|
* it was initialized with.
|
||||||
|
*
|
||||||
|
* @see `OnStateChanged()` for more information about connection states.
|
||||||
|
* @return Current connection state.
|
||||||
|
*/
|
||||||
|
public final function DBConnectionState GetConnectionState()
|
||||||
|
{
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether caller `DBConnection` is currently connected without errors
|
||||||
|
* to the database it was initialized with.
|
||||||
|
*
|
||||||
|
* @return `true` if caller `DBConnection` is connected to the database and
|
||||||
|
* `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool IsConnected()
|
||||||
|
{
|
||||||
|
return (currentState == DBCS_Connected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an error has occurred with connection to the database.
|
||||||
|
*
|
||||||
|
* `DBConnection` can get disconnected from database manually and without
|
||||||
|
* any errors, so, if you simply want to check whether connection exists,
|
||||||
|
* @see `IsConnected()` or @see `GetConnectionState()`.
|
||||||
|
* To obtain more detailed information @see `GetError()`.
|
||||||
|
*
|
||||||
|
* @return `true` if there were no error thus far and `false` otherwise.
|
||||||
|
*/
|
||||||
|
public final function bool IsOk()
|
||||||
|
{
|
||||||
|
return (dbFailureReason == FDE_None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns error that has occurred during connection.
|
||||||
|
*
|
||||||
|
* @return Error that has occurred during connection to the database,
|
||||||
|
* `FDE_None` if there was no errors.
|
||||||
|
*/
|
||||||
|
public final function FaultyDatabaseError GetError()
|
||||||
|
{
|
||||||
|
return dbFailureReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function ChangeState(DBConnectionState newState)
|
||||||
|
{
|
||||||
|
local DBConnectionState oldState;
|
||||||
|
|
||||||
|
oldState = currentState;
|
||||||
|
currentState = newState;
|
||||||
|
onStateChangedSignal.Emit(self, oldState, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts connection to the database caller `DBConnection` was initialized
|
||||||
|
* with. Result isn't immediate and can be tracked with `OnStateChanged()`
|
||||||
|
* signal.
|
||||||
|
*
|
||||||
|
* Connection checks whether data by the initialization address can be read and
|
||||||
|
* has proper type (by default JSON object, but JSON array can be used
|
||||||
|
* instead).
|
||||||
|
*
|
||||||
|
* Whether connection is successfully established isn't known at the moment
|
||||||
|
* this function returns. User `OnStateChanged()` to track that.
|
||||||
|
*
|
||||||
|
* @param expectArray Set this to `true` if the expected root value is
|
||||||
|
* JSON array.
|
||||||
|
*/
|
||||||
|
public final function Connect(optional bool expectArray)
|
||||||
|
{
|
||||||
|
local Collection incrementObject;
|
||||||
|
|
||||||
|
if (!IsInitialized()) return;
|
||||||
|
if (currentState != DBCS_Idle) return;
|
||||||
|
|
||||||
|
if (expectArray) {
|
||||||
|
incrementObject = _.collections.EmptyArrayList();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
incrementObject = _.collections.EmptyHashTable();
|
||||||
|
}
|
||||||
|
dbInstance.IncrementData(
|
||||||
|
rootPointer,
|
||||||
|
incrementObject,
|
||||||
|
RegisterNextRequestID()).connect = IncrementCheckHandler;
|
||||||
|
incrementObject.FreeSelf();
|
||||||
|
// Copy of the `rootPointer` is consumed by `RegisterNextRequestID()`
|
||||||
|
// method
|
||||||
|
dbInstance.ReadData(rootPointer,, RegisterNextRequestID(rootPointer.Copy()))
|
||||||
|
.connect = InitialLoadingHandler;
|
||||||
|
ChangeState(DBCS_Connecting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects `DBConnection` from its database, preventing its further
|
||||||
|
* updates.
|
||||||
|
*
|
||||||
|
* Database can only be disconnected if connection was at least initialized
|
||||||
|
* (state isn't `DBCS_Idle`) and no error has yet occurred (state isn't
|
||||||
|
* `DBCS_FaultyDatabase`).
|
||||||
|
*
|
||||||
|
* @return `true` if `DBConnection` was disconnected from the database and
|
||||||
|
* `false` otherwise (including if it already was disconnected).
|
||||||
|
*/
|
||||||
|
public final function bool Disconnect()
|
||||||
|
{
|
||||||
|
if ( currentState != DBCS_FaultyDatabase
|
||||||
|
&& currentState != DBCS_Idle
|
||||||
|
&& currentState != DBCS_Disconnected)
|
||||||
|
{
|
||||||
|
ChangeState(DBCS_Disconnected);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function int RegisterNextRequestID(
|
||||||
|
optional /*take*/ JSONPointer relativePointer)
|
||||||
|
{
|
||||||
|
if (relativePointer != none) {
|
||||||
|
queuedPointers[queuedPointers.length] = relativePointer;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
queuedPointers[queuedPointers.length] = _.json.Pointer();
|
||||||
|
}
|
||||||
|
requestIDs[requestIDs.length] = nextRequestID;
|
||||||
|
nextRequestID += 1;
|
||||||
|
return (nextRequestID - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function JSONPointer FetchRequestPointer(int requestID)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local JSONPointer result;
|
||||||
|
|
||||||
|
while (i < requestIDs.length)
|
||||||
|
{
|
||||||
|
if (requestIDs[i] < requestID)
|
||||||
|
{
|
||||||
|
// We receive all requests in order, so if `requestID` is higher
|
||||||
|
// than IDs of some other requests - it means that they are older,
|
||||||
|
// lost requests
|
||||||
|
_.memory.Free(queuedPointers[i]);
|
||||||
|
queuedPointers.Remove(i, 1);
|
||||||
|
requestIDs.Remove(i, 1);
|
||||||
|
}
|
||||||
|
if (requestIDs[i] == requestID)
|
||||||
|
{
|
||||||
|
result = queuedPointers[i];
|
||||||
|
queuedPointers.Remove(i, 1);
|
||||||
|
requestIDs.Remove(i, 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function bool FetchIfRequestStillValid(int requestID)
|
||||||
|
{
|
||||||
|
local JSONPointer result;
|
||||||
|
|
||||||
|
result = FetchRequestPointer(requestID);
|
||||||
|
if (result != none)
|
||||||
|
{
|
||||||
|
_.memory.Free(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function IncrementCheckHandler(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
Database source,
|
||||||
|
int requestID)
|
||||||
|
{
|
||||||
|
if (!FetchIfRequestStillValid(requestID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If we could successfully increment value with appropriate JSON value,
|
||||||
|
// then its type is correct
|
||||||
|
rootIsOfExpectedType = (result == DBR_Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function InitialLoadingHandler(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
/*take*/ AcediaObject data,
|
||||||
|
Database source,
|
||||||
|
int requestID)
|
||||||
|
{
|
||||||
|
local int i;
|
||||||
|
local array<DBCache.PendingEdit> completedEdits;
|
||||||
|
|
||||||
|
if (!FetchIfRequestStillValid(requestID))
|
||||||
|
{
|
||||||
|
_.memory.Free(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (HandleInitializationError(result))
|
||||||
|
{
|
||||||
|
_.memory.Free(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completedEdits = localCache.SetRealData(data);
|
||||||
|
for (i = 0; i < completedEdits.length; i += 1)
|
||||||
|
{
|
||||||
|
if (completedEdits[i].successful)
|
||||||
|
{
|
||||||
|
if (completedEdits[i].type == DBCET_Remove) {
|
||||||
|
RemoveDataInDatabase(completedEdits[i].location);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ModifyDataInDatabase(
|
||||||
|
completedEdits[i].location,
|
||||||
|
completedEdits[i].data,
|
||||||
|
completedEdits[i].type == DBCET_Increment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
onEditResultSignal.Emit(completedEdits[i].location, false);
|
||||||
|
}
|
||||||
|
_.memory.Free(completedEdits[i].location);
|
||||||
|
_.memory.Free(completedEdits[i].data);
|
||||||
|
}
|
||||||
|
_.memory.Free(data);
|
||||||
|
ChangeState(DBCS_Connected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return `true` if further initialization must be stopped.
|
||||||
|
private final function bool HandleInitializationError(
|
||||||
|
Database.DBQueryResult result)
|
||||||
|
{
|
||||||
|
// Get disconnected before even response has even arrived
|
||||||
|
if (currentState == DBCS_Disconnected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentState == DBCS_Connected)
|
||||||
|
{
|
||||||
|
_.logger.Auto(errDoubleInitialization).Arg(rootPointer.ToText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (result == DBR_InvalidDatabase)
|
||||||
|
{
|
||||||
|
dbFailureReason = FDE_Unknown;
|
||||||
|
ChangeState(DBCS_FaultyDatabase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (result != DBR_Success)
|
||||||
|
{
|
||||||
|
dbFailureReason = FDE_CannotReadRootData;
|
||||||
|
ChangeState(DBCS_FaultyDatabase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!rootIsOfExpectedType)
|
||||||
|
{
|
||||||
|
dbFailureReason = FDE_UnexpectedRootData;
|
||||||
|
ChangeState(DBCS_FaultyDatabase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final function EditDataHandler(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
Database source,
|
||||||
|
int requestID)
|
||||||
|
{
|
||||||
|
local JSONPointer relatedPointer;
|
||||||
|
|
||||||
|
relatedPointer = FetchRequestPointer(requestID);
|
||||||
|
if (relatedPointer == none) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result == DBR_InvalidDatabase)
|
||||||
|
{
|
||||||
|
dbFailureReason = FDE_Unknown;
|
||||||
|
ChangeState(DBCS_FaultyDatabase);
|
||||||
|
relatedPointer.FreeSelf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result == DBR_Success) {
|
||||||
|
onEditResultSignal.Emit(relatedPointer, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
onEditResultSignal.Emit(relatedPointer, false);
|
||||||
|
}
|
||||||
|
relatedPointer.FreeSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
errDoubleInitialization = (l=LOG_Error,m="`DBConnection` connected to \"%1\" was double-initialized. This SHOULD NOT happen. Please report this bug.")
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Signal class for `DBConnections`'s `OnEditResult()` signal.
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBConnection_EditResult_Signal extends Signal
|
||||||
|
dependson(DBConnection);
|
||||||
|
|
||||||
|
public final function Emit(JSONPointer editLocation, bool isSuccessful)
|
||||||
|
{
|
||||||
|
local Slot nextSlot;
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none)
|
||||||
|
{
|
||||||
|
DBConnection_EditResult_Slot(nextSlot)
|
||||||
|
.connect(editLocation, isSuccessful);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
relatedSlotClass = class'DBConnection_EditResult_Slot'
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Slot class for `DBConnections`'s `OnEditResult()` signal.
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBConnection_EditResult_Slot extends Slot
|
||||||
|
dependson(DBConnection);
|
||||||
|
|
||||||
|
delegate connect(JSONPointer editLocation, bool isSuccessful)
|
||||||
|
{
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Signal class for `DBConnections`'s `OnStatusChanged()` signal.
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBConnection_StateChanged_Signal extends Signal
|
||||||
|
dependson(DBConnection);
|
||||||
|
|
||||||
|
public final function Emit(
|
||||||
|
DBConnection instance,
|
||||||
|
DBConnection.DBConnectionState oldState,
|
||||||
|
DBConnection.DBConnectionState newState)
|
||||||
|
{
|
||||||
|
local Slot nextSlot;
|
||||||
|
StartIterating();
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
while (nextSlot != none)
|
||||||
|
{
|
||||||
|
DBConnection_StateChanged_Slot(nextSlot)
|
||||||
|
.connect(instance, oldState, newState);
|
||||||
|
nextSlot = GetNextSlot();
|
||||||
|
}
|
||||||
|
CleanEmptySlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
relatedSlotClass = class'DBConnection_StateChanged_Slot'
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Slot class for `DBConnections`'s `OnStatusChanged()` signal.
|
||||||
|
* Copyright 2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBConnection_StateChanged_Slot extends Slot
|
||||||
|
dependson(DBConnection);
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
DBConnection instance,
|
||||||
|
DBConnection.DBConnectionState oldState,
|
||||||
|
DBConnection.DBConnectionState newState)
|
||||||
|
{
|
||||||
|
DummyCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Constructor()
|
||||||
|
{
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
40
kf_sources/AcediaCore/Classes/DBIncrementTask.uc
Normal file
40
kf_sources/AcediaCore/Classes/DBIncrementTask.uc
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Variant of `DBTask` for `IncrementData()` query.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBIncrementTask extends DBTask;
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
Database source,
|
||||||
|
int requestID) {}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function CompleteSelf(Database source)
|
||||||
|
{
|
||||||
|
connect(GetResult(), source, GetRequestID());
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
49
kf_sources/AcediaCore/Classes/DBKeysTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBKeysTask.uc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Variant of `DBTask` for `GetDataKeys()` query.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBKeysTask extends DBTask;
|
||||||
|
|
||||||
|
var private ArrayList queryKeysResponse;
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
/*take*/ ArrayList keys,
|
||||||
|
Database source,
|
||||||
|
int requestID) {}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
queryKeysResponse = none;
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetDataKeys(/* take */ ArrayList keys)
|
||||||
|
{
|
||||||
|
queryKeysResponse = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function CompleteSelf(Database source)
|
||||||
|
{
|
||||||
|
connect(GetResult(), queryKeysResponse, source, GetRequestID());
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
49
kf_sources/AcediaCore/Classes/DBReadTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBReadTask.uc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Variant of `DBTask` for `ReadData()` query.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBReadTask extends DBTask;
|
||||||
|
|
||||||
|
var private AcediaObject queryDataResponse;
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
/*take*/ AcediaObject data,
|
||||||
|
Database source,
|
||||||
|
int requestID) {}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
queryDataResponse = none;
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetReadData(AcediaObject data)
|
||||||
|
{
|
||||||
|
queryDataResponse = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function CompleteSelf(Database source)
|
||||||
|
{
|
||||||
|
connect(GetResult(), queryDataResponse, source, GetRequestID());
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
1199
kf_sources/AcediaCore/Classes/DBRecord.uc
Normal file
1199
kf_sources/AcediaCore/Classes/DBRecord.uc
Normal file
File diff suppressed because it is too large
Load Diff
40
kf_sources/AcediaCore/Classes/DBRemoveTask.uc
Normal file
40
kf_sources/AcediaCore/Classes/DBRemoveTask.uc
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Variant of `DBTask` for `RemoveData()` query.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBRemoveTask extends DBTask;
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
Database source,
|
||||||
|
int requestID) {}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function CompleteSelf(Database source)
|
||||||
|
{
|
||||||
|
connect(GetResult(), source, GetRequestID());
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
49
kf_sources/AcediaCore/Classes/DBSizeTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBSizeTask.uc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Variant of `DBTask` for `GetDataSize()` query.
|
||||||
|
* Copyright 2021-2023 Anton Tarasenko
|
||||||
|
*------------------------------------------------------------------------------
|
||||||
|
* This file is part of Acedia.
|
||||||
|
*
|
||||||
|
* Acedia is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Acedia is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
class DBSizeTask extends DBTask;
|
||||||
|
|
||||||
|
var private int querySizeResponse;
|
||||||
|
|
||||||
|
delegate connect(
|
||||||
|
Database.DBQueryResult result,
|
||||||
|
int size,
|
||||||
|
Database source,
|
||||||
|
int requestID) {}
|
||||||
|
|
||||||
|
protected function Finalizer()
|
||||||
|
{
|
||||||
|
super.Finalizer();
|
||||||
|
querySizeResponse = 0;
|
||||||
|
connect = none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetDataSize(int size)
|
||||||
|
{
|
||||||
|
querySizeResponse = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function CompleteSelf(Database source)
|
||||||
|
{
|
||||||
|
connect(GetResult(), querySizeResponse, source, GetRequestID());
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultproperties
|
||||||
|
{
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user