From edfbc51f1da31da2bb5ac09b2e8c2f1e61a91b19 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Sat, 23 Mar 2024 17:11:28 +0100 Subject: [PATCH] Fully show the auto splitter settings (#52) I randomly came across the `obs_source_update_properties` function in the documentation which allows us to dynamically rebuild the settings GUI whenever necessary. This allows us to properly show all the auto splitter settings and update them automatically when necessary. Unfortunately the function is a little buggy in OBS and can cause internal race conditions that crash OBS. It's probably rather rare that this happens, so we can probably move forward with this implementation regardless for now. There is as far as I can tell no way to work around these issues. The issue is tracked here: https://github.com/obsproject/obs-studio/issues/10423 --- .github/workflows/before_deploy.sh | 2 + .github/workflows/build_shared.sh | 2 + Cargo.lock | 293 +++++--- Cargo.toml | 10 +- obs/src/lib.rs | 55 ++ src/auto_splitters.rs | 15 +- src/ffi.rs | 24 + src/ffi_types.rs | 5 + src/lib.rs | 1031 ++++++++++++++++------------ 9 files changed, 906 insertions(+), 531 deletions(-) diff --git a/.github/workflows/before_deploy.sh b/.github/workflows/before_deploy.sh index e81fc15..52f1952 100755 --- a/.github/workflows/before_deploy.sh +++ b/.github/workflows/before_deploy.sh @@ -1,3 +1,5 @@ +#!/bin/bash + set -ex main() { diff --git a/.github/workflows/build_shared.sh b/.github/workflows/build_shared.sh index c40231d..2cd94f4 100755 --- a/.github/workflows/build_shared.sh +++ b/.github/workflows/build_shared.sh @@ -1,3 +1,5 @@ +#!/bin/bash + set -ex main() { diff --git a/Cargo.lock b/Cargo.lock index cb05354..3abf06d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -73,9 +73,9 @@ checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" [[package]] name = "arc-swap" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3d0060af21e8d11a926981cc00c6c1541aa91dd64b9f881985c3da1094425f" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "arrayref" @@ -91,9 +91,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" dependencies = [ "proc-macro2", "quote", @@ -108,9 +108,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -152,7 +152,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -174,9 +174,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitvec" @@ -230,9 +230,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cap-fs-ext" @@ -320,6 +320,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "clang-sys" version = "1.7.0" @@ -599,15 +605,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" -[[package]] -name = "encoding_rs" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -725,6 +722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -759,6 +757,7 @@ checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -816,9 +815,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" dependencies = [ "bytes", "fnv", @@ -866,9 +865,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "http" -version = "0.2.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -877,12 +876,24 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", + "futures-core", "http", + "http-body", "pin-project-lite", ] @@ -892,48 +903,61 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" -version = "0.14.28" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http", "hyper", + "hyper-util", "rustls", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -989,9 +1013,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6107a25f04af48ceeb4093eebc9b405ee5a1813a0bab5ecf1805d3eabb3337" +checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c" dependencies = [ "byteorder", "thiserror", @@ -999,9 +1023,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1145,7 +1169,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", "redox_syscall", ] @@ -1159,7 +1183,7 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "livesplit-auto-splitting" version = "0.1.0" -source = "git+https://github.com/LiveSplit/livesplit-core#ef37e67adecc46ffe003851f0e97b8a3b1da7842" +source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" dependencies = [ "anyhow", "arc-swap", @@ -1181,7 +1205,7 @@ dependencies = [ [[package]] name = "livesplit-core" version = "0.13.0" -source = "git+https://github.com/LiveSplit/livesplit-core#ef37e67adecc46ffe003851f0e97b8a3b1da7842" +source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" dependencies = [ "base64-simd", "bytemuck", @@ -1217,14 +1241,14 @@ dependencies = [ [[package]] name = "livesplit-hotkey" version = "0.7.0" -source = "git+https://github.com/LiveSplit/livesplit-core#ef37e67adecc46ffe003851f0e97b8a3b1da7842" +source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "crossbeam-channel", "evdev", "mio", - "nix 0.27.1", + "nix 0.28.0", "promising-future", "serde", "windows-sys 0.52.0", @@ -1234,7 +1258,7 @@ dependencies = [ [[package]] name = "livesplit-title-abbreviations" version = "0.3.0" -source = "git+https://github.com/LiveSplit/livesplit-core#ef37e67adecc46ffe003851f0e97b8a3b1da7842" +source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" dependencies = [ "unicase", ] @@ -1320,6 +1344,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1362,12 +1396,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", + "cfg_aliases", "libc", ] @@ -1447,6 +1482,7 @@ dependencies = [ "anyhow", "livesplit-core", "log", + "mime_guess", "obs", "open", "percent-encoding", @@ -1505,6 +1541,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1716,9 +1772,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1745,20 +1801,22 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.26" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" +checksum = "e333b1eb9fe677f6893a9efcb0d277a2d3edd83f358a236b657c32301dc6e5f6" dependencies = [ "base64", "bytes", - "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-rustls", + "hyper-util", "ipnet", "js-sys", "log", @@ -1768,7 +1826,8 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-native-certs", - "rustls-pemfile", + "rustls-pemfile 1.0.4", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -1813,11 +1872,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "itoa", "libc", @@ -1828,24 +1887,27 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 2.1.1", + "rustls-pki-types", "schannel", "security-framework", ] @@ -1859,13 +1921,30 @@ dependencies = [ "base64", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" + [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -1901,16 +1980,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" version = "2.9.2" @@ -2061,9 +2130,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snafu" @@ -2120,11 +2189,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" -version = "2.0.52" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -2188,7 +2263,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0682e006dd35771e392a6623ac180999a9a854b1d4a6c12fb2e804941c2b1f58" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cap-fs-ext", "cap-std", "fd-lock", @@ -2322,6 +2397,7 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "pin-project-lite", "socket2", "windows-sys 0.48.0", @@ -2329,11 +2405,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", + "rustls-pki-types", "tokio", ] @@ -2351,6 +2428,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -2553,7 +2652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec11da24eed0ca98c3e071cf9186051b51b6436db21a7613498a9191d6f35a" dependencies = [ "anyhow", - "bitflags 2.4.2", + "bitflags 2.5.0", "cap-rand", "cap-std", "io-extras", @@ -2896,7 +2995,7 @@ checksum = "a5530d063ee9ccb1d503fed91e3d509419f43733a05fcc99c9f7aa3482703189" dependencies = [ "anyhow", "async-trait", - "bitflags 2.4.2", + "bitflags 2.5.0", "thiserror", "tracing", "wasmtime", @@ -3119,7 +3218,7 @@ version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "windows-sys 0.52.0", ] @@ -3175,6 +3274,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 54b1036..a0421ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ name = "obs-livesplit-one" version = "0.1.0" authors = ["Christopher Serr "] -edition = "2018" +edition = "2021" +rust-version = "1.77" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -17,7 +18,7 @@ livesplit-core = { git = "https://github.com/LiveSplit/livesplit-core", features "software-rendering", "font-loading", ] } -log = { version = "0.4.6", features = ["serde"] } +log = { version = "0.4.6", features = ["serde", "release_max_level_info"] } serde = "1.0.188" serde_derive = "1.0.188" serde_json = "1.0.105" @@ -30,10 +31,13 @@ quick-xml = { version = "0.31.0", features = [ "serialize", "overlapped-lists", ], optional = true } -reqwest = { version = "0.11.18", features = [ +reqwest = { version = "0.12.1", features = [ "blocking", + "http2", + "macos-system-configuration", "rustls-tls-native-roots", ], default-features = false, optional = true } +mime_guess = "2.0.4" [target.'cfg(not(any(target_os = "macos", windows)))'.dependencies] obs = { path = "obs" } diff --git a/obs/src/lib.rs b/obs/src/lib.rs index ae07127..60dc020 100644 --- a/obs/src/lib.rs +++ b/obs/src/lib.rs @@ -101,6 +101,31 @@ pub extern "C" fn obs_data_get_bool(_data: *mut obs_data_t, _name: *const c_char panic!() } +#[no_mangle] +pub extern "C" fn obs_data_set_default_string( + _data: *mut obs_data_t, + _name: *const c_char, + _val: *const c_char, +) { + panic!() +} + +#[no_mangle] +pub extern "C" fn obs_data_erase(_data: *mut obs_data_t, _name: *const c_char) { + panic!() +} + +#[no_mangle] +pub extern "C" fn obs_properties_add_group( + _props: *mut obs_properties_t, + _name: *const c_char, + _description: *const c_char, + _ty: obs_group_type, + _group: *mut obs_properties_t, +) -> *mut obs_property_t { + panic!() +} + #[no_mangle] pub extern "C" fn obs_properties_add_int( _props: *mut obs_properties_t, @@ -211,6 +236,7 @@ pub extern "C" fn obs_properties_add_text( ) -> *mut obs_property_t { panic!() } + #[no_mangle] pub extern "C" fn obs_property_set_modified_callback2( _prop: *mut obs_property_t, @@ -219,6 +245,7 @@ pub extern "C" fn obs_property_set_modified_callback2( ) { panic!() } + #[no_mangle] pub extern "C" fn obs_property_set_description( _prop: *mut obs_property_t, @@ -226,14 +253,25 @@ pub extern "C" fn obs_property_set_description( ) { panic!() } + +#[no_mangle] +pub extern "C" fn obs_property_set_long_description( + _prop: *mut obs_property_t, + _long_description: *const c_char, +) { + panic!() +} + #[no_mangle] pub extern "C" fn obs_property_set_enabled(_prop: *mut obs_property_t, _enabled: bool) { panic!() } + #[no_mangle] pub extern "C" fn obs_property_set_visible(_prop: *mut obs_property_t, _visible: bool) { panic!() } + #[no_mangle] pub extern "C" fn obs_properties_get( _props: *mut obs_properties_t, @@ -241,6 +279,7 @@ pub extern "C" fn obs_properties_get( ) -> *mut obs_property_t { panic!() } + #[no_mangle] pub extern "C" fn obs_module_get_config_path( _module: *mut obs_module_t, @@ -248,6 +287,7 @@ pub extern "C" fn obs_module_get_config_path( ) -> *const c_char { panic!() } + #[no_mangle] pub extern "C" fn obs_properties_add_list( _props: *mut obs_properties_t, @@ -258,6 +298,7 @@ pub extern "C" fn obs_properties_add_list( ) -> *mut obs_property_t { panic!() } + #[no_mangle] pub extern "C" fn obs_properties_add_editable_list( _props: *mut obs_properties_t, @@ -269,6 +310,7 @@ pub extern "C" fn obs_properties_add_editable_list( ) -> *mut obs_property_t { panic!() } + #[no_mangle] pub extern "C" fn obs_property_list_add_string( _prop: *mut obs_property_t, @@ -277,10 +319,12 @@ pub extern "C" fn obs_property_list_add_string( ) -> size_t { panic!() } + #[no_mangle] pub extern "C" fn obs_data_set_bool(_data: *mut obs_data_t, _name: *const c_char, _val: bool) { panic!() } + #[no_mangle] pub extern "C" fn obs_data_set_string( _data: *mut obs_data_t, @@ -289,27 +333,38 @@ pub extern "C" fn obs_data_set_string( ) { panic!() } + #[no_mangle] pub extern "C" fn obs_data_get_array(_data: *mut obs_data_t, _name: *const c_char) -> *mut c_void { panic!() } + #[no_mangle] pub extern "C" fn obs_data_array_count(_array: *mut c_void) -> size_t { panic!() } + #[no_mangle] pub extern "C" fn obs_data_array_item(_array: *mut c_void, _idx: size_t) -> *mut obs_data_t { panic!() } + #[no_mangle] pub extern "C" fn obs_data_array_release(_array: *mut c_void) { panic!() } + #[no_mangle] pub extern "C" fn obs_data_release(_data: *mut obs_data_t) { panic!() } + #[no_mangle] pub extern "C" fn obs_data_get_json(_data: *mut obs_data_t) -> *const c_char { panic!() } + +#[no_mangle] +pub extern "C" fn obs_source_update_properties(_source: *mut obs_source_t) { + panic!() +} diff --git a/src/auto_splitters.rs b/src/auto_splitters.rs index c5d6f97..d612ff6 100644 --- a/src/auto_splitters.rs +++ b/src/auto_splitters.rs @@ -1,6 +1,6 @@ use anyhow::{format_err, Context, Error, Result}; use livesplit_core::util::PopulateString; -use log::{error, info}; +use log::{error, info, warn}; use quick_xml::de; use reqwest::{blocking::Client, Url}; use serde_derive::Deserialize; @@ -28,7 +28,7 @@ pub fn get_module_config_path() -> &'static PathBuf { unsafe { let config_path_ptr = obs_module_get_config_path( crate::OBS_MODULE_POINTER.load(atomic::Ordering::Relaxed), - cstr!(""), + cstr!(c""), ); if let Ok(config_path) = CStr::from_ptr(config_path_ptr).to_str() { buffer.push(config_path); @@ -131,7 +131,11 @@ impl List { impl Downloader { pub fn new() -> Self { Self { - client: Client::new(), + client: Client::builder() + .use_rustls_tls() + .http2_prior_knowledge() + .build() + .unwrap(), } } @@ -142,7 +146,10 @@ impl Downloader { }; let from_file_error = match get_list_from_file(folder) { - Ok(list) => return Ok(list), + Ok(list) => { + warn!("Failed downloading the auto splitters list from GitHub. Using the cached version. Error: {from_github_error:?}"); + return Ok(list); + } Err(e) => e, }; diff --git a/src/ffi.rs b/src/ffi.rs index 9d87929..abd1fd7 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -68,6 +68,22 @@ extern "C" { description: *const c_char, ) -> *mut obs_property_t; pub fn obs_data_get_bool(data: *mut obs_data_t, name: *const c_char) -> bool; + #[cfg(feature = "auto-splitting")] + pub fn obs_data_set_default_string( + data: *mut obs_data_t, + name: *const c_char, + val: *const c_char, + ); + #[cfg(feature = "auto-splitting")] + pub fn obs_data_erase(data: *mut obs_data_t, name: *const c_char); + #[cfg(feature = "auto-splitting")] + pub fn obs_properties_add_group( + props: *mut obs_properties_t, + name: *const c_char, + description: *const c_char, + ty: obs_group_type, + group: *mut obs_properties_t, + ) -> *mut obs_property_t; pub fn obs_properties_add_text( props: *mut obs_properties_t, name: *const c_char, @@ -137,6 +153,11 @@ extern "C" { #[cfg(feature = "auto-splitting")] pub fn obs_property_set_description(prop: *mut obs_property_t, description: *const c_char); #[cfg(feature = "auto-splitting")] + pub fn obs_property_set_long_description( + prop: *mut obs_property_t, + long_description: *const c_char, + ); + #[cfg(feature = "auto-splitting")] pub fn obs_property_set_enabled(prop: *mut obs_property_t, enabled: bool); pub fn obs_property_set_visible(prop: *mut obs_property_t, visible: bool); pub fn obs_properties_get( @@ -159,4 +180,7 @@ extern "C" { pub fn obs_data_array_release(array: *mut c_void); pub fn obs_data_release(data: *mut obs_data_t); pub fn obs_data_get_json(data: *mut obs_data_t) -> *const c_char; + + #[cfg(feature = "auto-splitting")] + pub fn obs_source_update_properties(source: *mut obs_source_t); } diff --git a/src/ffi_types.rs b/src/ffi_types.rs index 97ac9d5..6884fef 100644 --- a/src/ffi_types.rs +++ b/src/ffi_types.rs @@ -63,6 +63,11 @@ pub const OBS_EDITABLE_LIST_TYPE_STRINGS: obs_editable_list_type = 0; pub const OBS_EDITABLE_LIST_TYPE_FILES: obs_editable_list_type = 1; pub const OBS_EDITABLE_LIST_TYPE_FILES_AND_URLS: obs_editable_list_type = 2; +pub type obs_group_type = u32; +pub const OBS_COMBO_INVALID: obs_group_type = 0; +pub const OBS_GROUP_NORMAL: obs_group_type = 1; +pub const OBS_GROUP_CHECKABLE: obs_group_type = 2; + pub type obs_data_t = obs_data; #[repr(C)] #[derive(Debug, Copy, Clone)] diff --git a/src/lib.rs b/src/lib.rs index 3f97ddd..8ed81a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,20 +59,24 @@ use serde_json::from_str; #[cfg(feature = "auto-splitting")] use { self::ffi::{ - obs_data_set_bool, obs_data_set_string, obs_properties_add_list, - obs_property_list_add_string, obs_property_set_description, obs_property_set_enabled, - OBS_COMBO_FORMAT_STRING, OBS_COMBO_TYPE_LIST, OBS_TEXT_INFO, + obs_data_erase, obs_data_set_bool, obs_data_set_default_string, obs_data_set_string, + obs_properties_add_group, obs_properties_add_list, obs_property_list_add_string, + obs_property_set_description, obs_property_set_enabled, obs_property_set_long_description, + obs_source_update_properties, OBS_COMBO_FORMAT_STRING, OBS_COMBO_TYPE_LIST, + OBS_GROUP_NORMAL, OBS_TEXT_INFO, + }, + livesplit_core::auto_splitting::{ + self, + settings::{self, FileFilter, Value, Widget, WidgetKind}, + wasi_path, }, - livesplit_core::auto_splitting, - livesplit_core::auto_splitting::settings::{Value, Widget, WidgetKind}, - livesplit_core::auto_splitting::wasi_path, log::error, std::{ffi::CString, sync::atomic::AtomicBool}, }; macro_rules! cstr { ($f:literal) => { - concat!($f, '\0').as_ptr().cast::() + std::ffi::CStr::as_ptr($f) }; } @@ -129,9 +133,23 @@ struct State { height: u32, activated: bool, #[cfg(feature = "auto-splitting")] - auto_splitter_settings: Arc>, + auto_splitter_widgets: Arc>, + #[cfg(feature = "auto-splitting")] + auto_splitter_map: settings::Map, #[cfg(feature = "auto-splitting")] obs_settings: *mut obs_data_t, + #[cfg(feature = "auto-splitting")] + source: *mut obs_source_t, +} + +impl Drop for State { + fn drop(&mut self) { + unsafe { + obs_enter_graphics(); + gs_texture_destroy(self.texture); + obs_leave_graphics(); + } + } } struct Settings { @@ -176,7 +194,7 @@ fn log(level: Level, target: &str, args: &fmt::Arguments<'_>) { Level::Debug | Level::Trace => LOG_DEBUG, }; unsafe { - blog(level as _, cstr!("%s"), str.as_ptr()); + blog(level as _, cstr!(c"%s"), str.as_ptr()); } } @@ -316,6 +334,7 @@ impl State { width, height, }: Settings, + _source: *mut obs_source_t, #[cfg(feature = "auto-splitting")] obs_settings: *mut obs_data_t, ) -> Self { debug!("Loading settings."); @@ -353,9 +372,13 @@ impl State { height, activated: false, #[cfg(feature = "auto-splitting")] - auto_splitter_settings: Arc::>::default(), + auto_splitter_widgets: Arc::default(), + #[cfg(feature = "auto-splitting")] + auto_splitter_map: settings::Map::new(), #[cfg(feature = "auto-splitting")] obs_settings, + #[cfg(feature = "auto-splitting")] + source: _source, } } @@ -377,11 +400,35 @@ impl State { ); self.image_cache.collect(); + + #[cfg(feature = "auto-splitting")] + { + let mut needs_properties_update = false; + + if let Some(auto_splitter_widgets) = self.global_timer.auto_splitter.settings_widgets() + { + if !Arc::ptr_eq(&self.auto_splitter_widgets, &auto_splitter_widgets) { + self.auto_splitter_widgets = auto_splitter_widgets; + needs_properties_update = true; + } + } + + if let Some(auto_splitter_map) = self.global_timer.auto_splitter.settings_map() { + if !self.auto_splitter_map.is_unchanged(&auto_splitter_map) { + self.auto_splitter_map = auto_splitter_map; + needs_properties_update = true; + } + } + + if needs_properties_update { + obs_source_update_properties(self.source); + } + } } } unsafe extern "C" fn get_name(_: *mut c_void) -> *const c_char { - cstr!("LiveSplit One") + cstr!(c"LiveSplit One") } unsafe extern "C" fn split( @@ -394,7 +441,7 @@ unsafe extern "C" fn split( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -412,7 +459,7 @@ unsafe extern "C" fn reset( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -434,7 +481,7 @@ unsafe extern "C" fn undo( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -452,7 +499,7 @@ unsafe extern "C" fn skip( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -470,7 +517,7 @@ unsafe extern "C" fn pause( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -493,7 +540,7 @@ unsafe extern "C" fn undo_all_pauses( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -511,7 +558,7 @@ unsafe extern "C" fn previous_comparison( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -534,7 +581,7 @@ unsafe extern "C" fn next_comparison( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -557,7 +604,7 @@ unsafe extern "C" fn toggle_timing_method( return; } - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.activated { return; } @@ -571,79 +618,82 @@ unsafe extern "C" fn toggle_timing_method( } unsafe extern "C" fn create(settings: *mut obs_data_t, source: *mut obs_source_t) -> *mut c_void { - #[cfg(feature = "auto-splitting")] - let data = Box::into_raw(Box::new(State::new(parse_settings(settings), settings))).cast(); - #[cfg(not(feature = "auto-splitting"))] - let data = Box::into_raw(Box::new(State::new(parse_settings(settings)))).cast(); + let data = Box::into_raw(Box::new(Mutex::new(State::new( + parse_settings(settings), + source, + #[cfg(feature = "auto-splitting")] + settings, + )))) + .cast(); obs_hotkey_register_source( source, - cstr!("hotkey_split"), - cstr!("Split"), + cstr!(c"hotkey_split"), + cstr!(c"Split"), Some(split), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_reset"), - cstr!("Reset"), + cstr!(c"hotkey_reset"), + cstr!(c"Reset"), Some(reset), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_undo"), - cstr!("Undo Split"), + cstr!(c"hotkey_undo"), + cstr!(c"Undo Split"), Some(undo), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_skip"), - cstr!("Skip Split"), + cstr!(c"hotkey_skip"), + cstr!(c"Skip Split"), Some(skip), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_pause"), - cstr!("Pause"), + cstr!(c"hotkey_pause"), + cstr!(c"Pause"), Some(pause), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_undo_all_pauses"), - cstr!("Undo All Pauses"), + cstr!(c"hotkey_undo_all_pauses"), + cstr!(c"Undo All Pauses"), Some(undo_all_pauses), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_previous_comparison"), - cstr!("Previous Comparison"), + cstr!(c"hotkey_previous_comparison"), + cstr!(c"Previous Comparison"), Some(previous_comparison), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_next_comparison"), - cstr!("Next Comparison"), + cstr!(c"hotkey_next_comparison"), + cstr!(c"Next Comparison"), Some(next_comparison), data, ); obs_hotkey_register_source( source, - cstr!("hotkey_toggle_timing_method"), - cstr!("Toggle Timing Method"), + cstr!(c"hotkey_toggle_timing_method"), + cstr!(c"Toggle Timing Method"), Some(toggle_timing_method), data, ); @@ -652,34 +702,31 @@ unsafe extern "C" fn create(settings: *mut obs_data_t, source: *mut obs_source_t } unsafe extern "C" fn destroy(data: *mut c_void) { - let state: Box = Box::from_raw(data.cast()); - obs_enter_graphics(); - gs_texture_destroy(state.texture); - obs_leave_graphics(); + drop(Box::>::from_raw(data.cast())); } unsafe extern "C" fn get_width(data: *mut c_void) -> u32 { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.width } unsafe extern "C" fn get_height(data: *mut c_void) -> u32 { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.height } unsafe extern "C" fn video_render(data: *mut c_void, _: *mut gs_effect_t) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.render(); let effect = obs_get_base_effect(OBS_EFFECT_PREMULTIPLIED_ALPHA); - let tech = gs_effect_get_technique(effect, cstr!("Draw")); + let tech = gs_effect_get_technique(effect, cstr!(c"Draw")); gs_technique_begin(tech); gs_technique_begin_pass(tech, 0); gs_effect_set_texture( - gs_effect_get_param_by_name(effect, cstr!("image")), + gs_effect_get_param_by_name(effect, cstr!(c"image")), state.texture, ); gs_draw_sprite(state.texture, 0, 0, 0); @@ -694,7 +741,7 @@ unsafe extern "C" fn mouse_wheel( _: c_int, y_delta: c_int, ) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); match y_delta.cmp(&0) { Ordering::Less => state.layout.scroll_down(), Ordering::Equal => {} @@ -707,7 +754,7 @@ unsafe extern "C" fn save_splits( _: *mut obs_property_t, data: *mut c_void, ) -> bool { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); save_splits_file(state) } @@ -717,7 +764,7 @@ unsafe extern "C" fn use_game_arguments_modified( _prop: *mut obs_property_t, settings: *mut obs_data_t, ) -> bool { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let use_game_arguments = obs_data_get_bool(settings, SETTINGS_USE_GAME_ARGUMENTS); @@ -742,7 +789,7 @@ unsafe extern "C" fn start_game_clicked( _prop: *mut obs_property_t, data: *mut c_void, ) -> bool { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if !state.game_path.exists() { warn!("No path provided to start a game."); @@ -820,232 +867,6 @@ unsafe extern "C" fn start_game_clicked( false } -#[cfg(feature = "auto-splitting")] -const DEFAULT_AUTO_SPLITTER_LIST_SETTING: *const c_char = cstr!("obs-livesplit-one-default-choice"); -#[cfg(feature = "auto-splitting")] -const DEFAULT_AUTO_SPLITTER_SETTING_TOOLTIP: *const c_char = cstr!("Waiting for setting selection"); - -#[cfg(feature = "auto-splitting")] -unsafe extern "C" fn settings_list_modified( - data: *mut c_void, - props: *mut obs_properties_t, - _prop: *mut obs_property_t, - settings: *mut obs_data_t, -) -> bool { - let default_setting_string = CStr::from_ptr(DEFAULT_AUTO_SPLITTER_LIST_SETTING); - let list_setting_string = CStr::from_ptr(obs_data_get_string( - settings, - SETTINGS_AUTO_SPLITTER_SETTINGS_LIST, - )); - - let tooltip_property = obs_properties_get(props, SETTINGS_AUTO_SPLITTER_SETTINGS_TOOLTIP); - let enable_property = obs_properties_get(props, SETTINGS_AUTO_SPLITTER_SETTINGS_ENABLE); - let file_select_property = - obs_properties_get(props, SETTINGS_AUTO_SPLITTER_SETTINGS_FILE_SELECT); - - macro_rules! reset_auto_splitter_setting_ui { - () => { - obs_property_set_description(tooltip_property, DEFAULT_AUTO_SPLITTER_SETTING_TOOLTIP); - obs_property_set_enabled(enable_property, false); - obs_property_set_enabled(file_select_property, false); - obs_property_set_visible(enable_property, false); - obs_property_set_visible(file_select_property, false); - return true; - }; - } - - if list_setting_string == default_setting_string { - reset_auto_splitter_setting_ui!(); - } - - let state: &mut State = &mut *data.cast(); - - let list_setting_string = list_setting_string.to_str().unwrap_or_default(); - - let user_setting = state - .auto_splitter_settings - .iter() - .find(|x| x.key.as_ref() == list_setting_string); - - if let Some(user_setting) = user_setting { - match user_setting.kind { - WidgetKind::Title { heading_level: _ } => { - obs_property_set_enabled(enable_property, false); - obs_property_set_enabled(file_select_property, false); - obs_property_set_visible(enable_property, false); - obs_property_set_visible(file_select_property, false); - } - WidgetKind::Bool { - default_value: default, - } => { - match state - .global_timer - .auto_splitter - .settings_map() - .unwrap_or_default() - .get(user_setting.key.as_ref()) - { - Some(Value::Bool(value)) => { - obs_property_set_enabled(enable_property, true); - obs_property_set_enabled(file_select_property, false); - obs_property_set_visible(enable_property, true); - obs_property_set_visible(file_select_property, false); - obs_data_set_bool(settings, SETTINGS_AUTO_SPLITTER_SETTINGS_ENABLE, *value); - } - _ => { - obs_property_set_enabled(enable_property, true); - obs_property_set_enabled(file_select_property, false); - obs_property_set_visible(enable_property, true); - obs_property_set_visible(file_select_property, false); - obs_data_set_bool( - settings, - SETTINGS_AUTO_SPLITTER_SETTINGS_ENABLE, - default, - ); - } - } - } - WidgetKind::Choice { .. } => { - warn!("Unimplemented setting type Choice"); - } - WidgetKind::FileSelect { .. } => { - obs_property_set_enabled(enable_property, false); - obs_property_set_enabled(file_select_property, true); - obs_property_set_visible(enable_property, false); - obs_property_set_visible(file_select_property, true); - // Needs to outlive the pointer. - let path_cs; - let path_ptr = match state - .global_timer - .auto_splitter - .settings_map() - .unwrap_or_default() - .get(user_setting.key.as_ref()) - { - Some(Value::String(value)) => { - path_cs = wasi_path::to_native(value, true) - .filter(|p| p.exists()) - .and_then(|p| CString::new(p.as_os_str().as_encoded_bytes()).ok()) - .unwrap_or_default(); - - path_cs.as_ptr() - } - _ => ptr::null(), - }; - obs_data_set_string( - settings, - SETTINGS_AUTO_SPLITTER_SETTINGS_FILE_SELECT, - path_ptr, - ); - } - } - - match &user_setting.tooltip { - Some(tooltip) => { - let tooltip = CString::new(tooltip.to_string()).unwrap_or_default(); - obs_property_set_description(tooltip_property, tooltip.as_ptr()); - } - None => { - obs_property_set_description(tooltip_property, cstr!("No tooltip to show")); - } - } - - true - } else { - reset_auto_splitter_setting_ui!(); - } -} - -#[cfg(feature = "auto-splitting")] -unsafe extern "C" fn settings_enable_modified( - data: *mut c_void, - _props: *mut obs_properties_t, - _prop: *mut obs_property_t, - settings: *mut obs_data_t, -) -> bool { - let default_setting_string = CStr::from_ptr(DEFAULT_AUTO_SPLITTER_LIST_SETTING); - let list_setting_string = CStr::from_ptr(obs_data_get_string( - settings, - SETTINGS_AUTO_SPLITTER_SETTINGS_LIST, - )); - - if list_setting_string == default_setting_string { - return false; - } - - let state: &mut State = &mut *data.cast(); - - let value = obs_data_get_bool(settings, SETTINGS_AUTO_SPLITTER_SETTINGS_ENABLE); - - let setting_key = match list_setting_string.to_str() { - Ok(value) => value, - Err(_) => { - warn!("Tried to set invalid setting key"); - return false; - } - }; - - match state.global_timer.auto_splitter.settings_map() { - Some(mut map) => { - map.insert(Arc::from(setting_key), Value::Bool(value)); - state.global_timer.auto_splitter.set_settings_map(map); - } - None => { - warn!("The settings map could not be loaded"); - return false; - } - }; - - true -} - -#[cfg(feature = "auto-splitting")] -unsafe extern "C" fn settings_file_select_modified( - data: *mut c_void, - _props: *mut obs_properties_t, - _prop: *mut obs_property_t, - settings: *mut obs_data_t, -) -> bool { - let default_setting_string = CStr::from_ptr(DEFAULT_AUTO_SPLITTER_LIST_SETTING); - let list_setting_string = CStr::from_ptr(obs_data_get_string( - settings, - SETTINGS_AUTO_SPLITTER_SETTINGS_LIST, - )); - - if list_setting_string == default_setting_string { - return false; - } - - let state: &mut State = &mut *data.cast(); - - let value = obs_data_get_string(settings, SETTINGS_AUTO_SPLITTER_SETTINGS_FILE_SELECT); - - let setting_key = match list_setting_string.to_str() { - Ok(value) => value, - Err(_) => { - warn!("Tried to set invalid setting key"); - return false; - } - }; - - match state.global_timer.auto_splitter.settings_map() { - Some(mut map) => { - let value_str = CStr::from_ptr(value).to_string_lossy(); - let Some(wasi_str) = wasi_path::from_native(Path::new(value_str.as_ref())) else { - warn!("Tried to set invalid setting value"); - return false; - }; - map.insert(Arc::from(setting_key), Value::String(Arc::from(wasi_str))); - state.global_timer.auto_splitter.set_settings_map(map); - } - None => { - warn!("The settings map could not be loaded"); - return false; - } - }; - true -} - #[cfg(feature = "auto-splitting")] unsafe extern "C" fn use_local_auto_splitter_modified( data: *mut c_void, @@ -1053,7 +874,7 @@ unsafe extern "C" fn use_local_auto_splitter_modified( _prop: *mut obs_property_t, settings: *mut obs_data_t, ) -> bool { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let use_local_auto_splitter = obs_data_get_bool(settings, SETTINGS_LOCAL_AUTO_SPLITTER); @@ -1074,7 +895,7 @@ unsafe extern "C" fn use_local_auto_splitter_modified( obs_property_set_visible(local_auto_splitter_path, use_local_auto_splitter); - obs_property_set_description(auto_splitter_activate, cstr!("Activate")); + obs_property_set_description(auto_splitter_activate, cstr!(c"Activate")); update_auto_splitter_ui( auto_splitter_info, @@ -1095,7 +916,7 @@ unsafe extern "C" fn splits_path_modified( let splits_path = CStr::from_ptr(obs_data_get_string(settings, SETTINGS_SPLITS_PATH).cast()); let splits_path = PathBuf::from(splits_path.to_string_lossy().into_owned()); - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); handle_splits_path_change(state, splits_path); @@ -1131,7 +952,7 @@ unsafe fn update_auto_splitter_ui( obs_property_set_enabled(activate_button, false); obs_property_set_description( info_text, - cstr!("This game's auto splitter is incompatible with LiveSplit One."), + cstr!(c"This game's auto splitter is incompatible with LiveSplit One."), ); } else { obs_property_set_enabled(activate_button, true); @@ -1149,7 +970,7 @@ unsafe fn update_auto_splitter_ui( obs_property_set_enabled(website_button, false); obs_property_set_description( info_text, - cstr!("No auto splitter available for this game."), + cstr!(c"No auto splitter available for this game."), ); } } @@ -1186,7 +1007,7 @@ unsafe extern "C" fn auto_splitter_activate_clicked( prop: *mut obs_property_t, data: *mut c_void, ) -> bool { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state .global_timer @@ -1229,9 +1050,9 @@ unsafe fn auto_splitter_update_activation_label( obs_property_set_description( activate_button_prop, if !is_active { - cstr!("Activate") + cstr!(c"Activate") } else { - cstr!("Deactivate") + cstr!(c"Deactivate") }, ); } @@ -1242,7 +1063,7 @@ unsafe extern "C" fn auto_splitter_open_website( _prop: *mut obs_property_t, data: *mut c_void, ) -> bool { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let website = auto_splitters::get_list() .get_website_for_game(state.global_timer.timer.read().unwrap().run().game_name()); @@ -1266,7 +1087,7 @@ unsafe extern "C" fn auto_splitter_open_website( } unsafe extern "C" fn media_get_state(data: *mut c_void) -> obs_media_state { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let phase = state.global_timer.timer.read().unwrap().current_phase(); match phase { TimerPhase::NotRunning => OBS_MEDIA_STATE_STOPPED, @@ -1277,7 +1098,7 @@ unsafe extern "C" fn media_get_state(data: *mut c_void) -> obs_media_state { } unsafe extern "C" fn media_play_pause(data: *mut c_void, pause: bool) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let mut timer = state.global_timer.timer.write().unwrap(); match timer.current_phase() { TimerPhase::NotRunning => { @@ -1300,7 +1121,7 @@ unsafe extern "C" fn media_play_pause(data: *mut c_void, pause: bool) { } unsafe extern "C" fn media_restart(data: *mut c_void) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); if state.auto_save { save_splits_file(state); } @@ -1310,7 +1131,7 @@ unsafe extern "C" fn media_restart(data: *mut c_void) { } unsafe extern "C" fn media_stop(data: *mut c_void) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.global_timer.timer.write().unwrap().reset(true); if state.auto_save { save_splits_file(state); @@ -1318,17 +1139,17 @@ unsafe extern "C" fn media_stop(data: *mut c_void) { } unsafe extern "C" fn media_next(data: *mut c_void) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.global_timer.timer.write().unwrap().split(); } unsafe extern "C" fn media_previous(data: *mut c_void) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.global_timer.timer.write().unwrap().undo_split(); } unsafe extern "C" fn media_get_time(data: *mut c_void) -> i64 { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let timer = state.global_timer.timer.read().unwrap(); let time = timer.snapshot().current_time()[timer.current_timing_method()].unwrap_or_default(); let (secs, nanos) = time.to_seconds_and_subsec_nanoseconds(); @@ -1336,7 +1157,7 @@ unsafe extern "C" fn media_get_time(data: *mut c_void) -> i64 { } unsafe extern "C" fn media_get_duration(data: *mut c_void) -> i64 { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let timer = state.global_timer.timer.read().unwrap(); let time = timer .run() @@ -1349,80 +1170,67 @@ unsafe extern "C" fn media_get_duration(data: *mut c_void) -> i64 { secs * 1000 + (nanos / 1_000_000) as i64 } -const SETTINGS_WIDTH: *const c_char = cstr!("width"); -const SETTINGS_HEIGHT: *const c_char = cstr!("height"); -const SETTINGS_USE_GAME_ARGUMENTS: *const c_char = cstr!("game_use_arguments"); -const SETTINGS_GAME_PATH: *const c_char = cstr!("game_path"); -const SETTINGS_GAME_ARGUMENTS: *const c_char = cstr!("game_arguments"); -const SETTINGS_GAME_WORKING_DIRECTORY: *const c_char = cstr!("game_working_directory"); -const SETTINGS_GAME_ENVIRONMENT_LIST: *const c_char = cstr!("game_environment_list"); -const SETTINGS_START_GAME: *const c_char = cstr!("start_game"); -const SETTINGS_SPLITS_PATH: *const c_char = cstr!("splits_path"); -const SETTINGS_AUTO_SAVE: *const c_char = cstr!("auto_save"); +const SETTINGS_WIDTH: *const c_char = cstr!(c"width"); +const SETTINGS_HEIGHT: *const c_char = cstr!(c"height"); +const SETTINGS_USE_GAME_ARGUMENTS: *const c_char = cstr!(c"game_use_arguments"); +const SETTINGS_GAME_PATH: *const c_char = cstr!(c"game_path"); +const SETTINGS_GAME_ARGUMENTS: *const c_char = cstr!(c"game_arguments"); +const SETTINGS_GAME_WORKING_DIRECTORY: *const c_char = cstr!(c"game_working_directory"); +const SETTINGS_GAME_ENVIRONMENT_LIST: *const c_char = cstr!(c"game_environment_list"); +const SETTINGS_START_GAME: *const c_char = cstr!(c"start_game"); +const SETTINGS_SPLITS_PATH: *const c_char = cstr!(c"splits_path"); +const SETTINGS_AUTO_SAVE: *const c_char = cstr!(c"auto_save"); #[cfg(feature = "auto-splitting")] -const SETTINGS_LOCAL_AUTO_SPLITTER: *const c_char = cstr!("local_auto_splitter"); +const SETTINGS_LOCAL_AUTO_SPLITTER: *const c_char = cstr!(c"local_auto_splitter"); #[cfg(feature = "auto-splitting")] -const SETTINGS_LOCAL_AUTO_SPLITTER_PATH: *const c_char = cstr!("local_auto_splitter_path"); +const SETTINGS_LOCAL_AUTO_SPLITTER_PATH: *const c_char = cstr!(c"local_auto_splitter_path"); #[cfg(feature = "auto-splitting")] -const SETTINGS_AUTO_SPLITTER_INFO: *const c_char = cstr!("auto_splitter_info"); +const SETTINGS_AUTO_SPLITTER_INFO: *const c_char = cstr!(c"auto_splitter_info"); #[cfg(feature = "auto-splitting")] -const SETTINGS_AUTO_SPLITTER_ACTIVATE: *const c_char = cstr!("auto_splitter_activate"); +const SETTINGS_AUTO_SPLITTER_ACTIVATE: *const c_char = cstr!(c"auto_splitter_activate"); #[cfg(feature = "auto-splitting")] -const SETTINGS_AUTO_SPLITTER_WEBSITE: *const c_char = cstr!("auto_splitter_website"); -#[cfg(feature = "auto-splitting")] -const SETTINGS_AUTO_SPLITTER_SETTINGS_LIST: *const c_char = cstr!("auto_splitter_settings_list"); -#[cfg(feature = "auto-splitting")] -const SETTINGS_AUTO_SPLITTER_SETTINGS_TOOLTIP: *const c_char = - cstr!("auto_splitter_settings_tooltip"); -#[cfg(feature = "auto-splitting")] -const SETTINGS_AUTO_SPLITTER_SETTINGS_ENABLE: *const c_char = - cstr!("auto_splitter_settings_enable"); -#[cfg(feature = "auto-splitting")] -const SETTINGS_AUTO_SPLITTER_SETTINGS_FILE_SELECT: *const c_char = - cstr!("auto_splitter_settings_file_select"); -const SETTINGS_LAYOUT_PATH: *const c_char = cstr!("layout_path"); -const SETTINGS_SAVE_SPLITS: *const c_char = cstr!("save_splits"); +const SETTINGS_AUTO_SPLITTER_WEBSITE: *const c_char = cstr!(c"auto_splitter_website"); +const SETTINGS_LAYOUT_PATH: *const c_char = cstr!(c"layout_path"); +const SETTINGS_SAVE_SPLITS: *const c_char = cstr!(c"save_splits"); unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t { - debug!("We are getting the properties."); - let props = obs_properties_create(); - obs_properties_add_int(props, SETTINGS_WIDTH, cstr!("Width"), 10, 8200, 10); - obs_properties_add_int(props, SETTINGS_HEIGHT, cstr!("Height"), 10, 8200, 10); + obs_properties_add_int(props, SETTINGS_WIDTH, cstr!(c"Width"), 10, 8200, 10); + obs_properties_add_int(props, SETTINGS_HEIGHT, cstr!(c"Height"), 10, 8200, 10); let splits_path = obs_properties_add_path( props, SETTINGS_SPLITS_PATH, - cstr!("Splits"), + cstr!(c"Splits"), OBS_PATH_FILE, - cstr!("LiveSplit Splits (*.lss)"), + cstr!(c"LiveSplit Splits (*.lss)"), ptr::null(), ); obs_properties_add_bool( props, SETTINGS_AUTO_SAVE, - cstr!("Automatically save splits file on reset"), + cstr!(c"Automatically save splits file on reset"), ); obs_properties_add_button( props, SETTINGS_SAVE_SPLITS, - cstr!("Save Splits"), + cstr!(c"Save Splits"), Some(save_splits), ); obs_properties_add_path( props, SETTINGS_LAYOUT_PATH, - cstr!("Layout"), + cstr!(c"Layout"), OBS_PATH_FILE, - cstr!("LiveSplit Layouts (*.lsl *.ls1l)"), + cstr!(c"LiveSplit Layouts (*.lsl *.ls1l)"), ptr::null(), ); let use_game_arguments = obs_properties_add_bool( props, SETTINGS_USE_GAME_ARGUMENTS, - cstr!("Advanced start game options"), + cstr!(c"Advanced start game options"), ); obs_property_set_modified_callback2( @@ -1434,35 +1242,35 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t obs_properties_add_path( props, SETTINGS_GAME_PATH, - cstr!("Game path"), + cstr!(c"Game Path"), OBS_PATH_FILE, - cstr!("Executable files (*)"), + cstr!(c"Executable files (*)"), ptr::null(), ); let game_arguments = obs_properties_add_text( props, SETTINGS_GAME_ARGUMENTS, - cstr!("Game arguments"), + cstr!(c"Game Arguments"), OBS_TEXT_DEFAULT, ); let game_working_directory = obs_properties_add_path( props, SETTINGS_GAME_WORKING_DIRECTORY, - cstr!("Working directory"), + cstr!(c"Working Directory"), OBS_PATH_DIRECTORY, - cstr!("Directories"), + cstr!(c"Directories"), ptr::null(), ); let game_env_list = obs_properties_add_editable_list( props, SETTINGS_GAME_ENVIRONMENT_LIST, - cstr!("Game environment variables (KEY=VALUE)"), + cstr!(c"Game Environment Variables (KEY=VALUE)"), OBS_EDITABLE_LIST_TYPE_STRINGS, ptr::null(), ptr::null(), ); - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let uses_game_arguments = state.use_game_arguments; obs_property_set_visible(game_arguments, uses_game_arguments); @@ -1472,7 +1280,7 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t obs_properties_add_button( props, SETTINGS_START_GAME, - cstr!("Start game"), + cstr!(c"Start Game"), Some(start_game_clicked), ); @@ -1483,7 +1291,7 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t let use_local_auto_splitter = obs_properties_add_bool( props, SETTINGS_LOCAL_AUTO_SPLITTER, - cstr!("Use local auto splitter"), + cstr!(c"Use local auto splitter"), ); obs_property_set_modified_callback2( @@ -1495,16 +1303,16 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t let local_auto_splitter_path = obs_properties_add_path( props, SETTINGS_LOCAL_AUTO_SPLITTER_PATH, - cstr!("Local Auto Splitter file"), + cstr!(c"Local Auto Splitter File"), OBS_PATH_FILE, - cstr!("LiveSplit One Auto Splitter (*.wasm)"), + cstr!(c"LiveSplit One Auto Splitter (*.wasm)"), ptr::null(), ); let info_text = obs_properties_add_text( props, SETTINGS_AUTO_SPLITTER_INFO, - cstr!("No splits loaded"), + cstr!(c"No splits loaded"), OBS_TEXT_INFO, ); @@ -1513,8 +1321,8 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t .auto_splitter_is_enabled .load(atomic::Ordering::Relaxed) { - true => cstr!("Deactivate"), - false => cstr!("Activate"), + true => cstr!(c"Deactivate"), + false => cstr!(c"Activate"), }; let activate_button = obs_properties_add_button( @@ -1527,7 +1335,7 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t let website_button = obs_properties_add_button( props, SETTINGS_AUTO_SPLITTER_WEBSITE, - cstr!("Website"), + cstr!(c"Website"), Some(auto_splitter_open_website), ); @@ -1553,87 +1361,188 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t return props; } - let settings_list = obs_properties_add_list( - props, - SETTINGS_AUTO_SPLITTER_SETTINGS_LIST, - cstr!("Custom auto splitter settings"), - OBS_COMBO_TYPE_LIST, - OBS_COMBO_FORMAT_STRING, - ); + let auto_splitter_properties = obs_properties_create(); - obs_properties_add_text( - props, - SETTINGS_AUTO_SPLITTER_SETTINGS_TOOLTIP, - DEFAULT_AUTO_SPLITTER_SETTING_TOOLTIP, - OBS_TEXT_INFO, - ); + let mut parents = vec![auto_splitter_properties]; - let settings_enable = obs_properties_add_bool( - props, - SETTINGS_AUTO_SPLITTER_SETTINGS_ENABLE, - cstr!("Enable"), - ); + for widget in state.auto_splitter_widgets.iter() { + let widget_description = CString::new(widget.description.as_ref()); - let settings_file_select = obs_properties_add_path( - props, - SETTINGS_AUTO_SPLITTER_SETTINGS_FILE_SELECT, - cstr!("Select a file"), - OBS_PATH_FILE, - cstr!("All files (*.*)"), - ptr::null(), - ); + let setting_key = CString::from_vec_with_nul( + format!("auto_splitter_setting_{}\0", widget.key).into(), + ); - obs_property_list_add_string( - settings_list, - cstr!("Select a setting to change"), - DEFAULT_AUTO_SPLITTER_LIST_SETTING, - ); + if let (Ok(setting_key), Ok(widget_description)) = (setting_key, widget_description) { + match &widget.kind { + WidgetKind::Bool { default_value } => { + let property = obs_properties_add_bool( + *parents.last().unwrap(), + setting_key.as_ptr(), + widget_description.as_ptr(), + ); - obs_data_set_string( - state.obs_settings, - SETTINGS_AUTO_SPLITTER_SETTINGS_LIST, - DEFAULT_AUTO_SPLITTER_LIST_SETTING, - ); - obs_data_set_bool( - state.obs_settings, - SETTINGS_AUTO_SPLITTER_SETTINGS_ENABLE, - false, - ); + if let Some(tooltip) = widget + .tooltip + .as_ref() + .and_then(|t| CString::new(t.as_bytes()).ok()) + { + obs_property_set_long_description(property, tooltip.as_ptr()); + } + + if let Some(value) = state + .auto_splitter_map + .get(&widget.key) + .and_then(|v| v.to_bool()) + { + obs_data_set_bool(state.obs_settings, setting_key.as_ptr(), value); + } else { + obs_data_erase(state.obs_settings, setting_key.as_ptr()); + } + + obs_data_set_default_bool( + state.obs_settings, + setting_key.as_ptr(), + *default_value, + ); + } + WidgetKind::Title { heading_level } => { + parents.truncate(*heading_level as usize + 1); + + let auto_splitter_properties = obs_properties_create(); + let property = obs_properties_add_group( + *parents.last().unwrap(), + setting_key.as_ptr(), + widget_description.as_ptr(), + OBS_GROUP_NORMAL, + auto_splitter_properties, + ); - obs_property_set_modified_callback2(settings_list, Some(settings_list_modified), data); - obs_property_set_modified_callback2(settings_enable, Some(settings_enable_modified), data); - obs_property_set_modified_callback2( - settings_file_select, - Some(settings_file_select_modified), - data, - ); + if let Some(tooltip) = widget + .tooltip + .as_ref() + .and_then(|t| CString::new(t.as_bytes()).ok()) + { + obs_property_set_long_description(property, tooltip.as_ptr()); + } - let auto_splitter_settings = state.global_timer.auto_splitter.settings_widgets(); - - if let Some(auto_splitter_settings) = auto_splitter_settings { - state.auto_splitter_settings = auto_splitter_settings; - - for setting in state.auto_splitter_settings.iter() { - let setting_description = CString::new(setting.description.as_ref()); - - let setting_key = CString::new(setting.key.as_ref()); - - if let (Ok(setting_key), Ok(setting_description)) = - (setting_key, setting_description) - { - obs_property_list_add_string( - settings_list, - setting_description.as_ptr(), - setting_key.as_ptr(), - ); - } else { - warn!( - "Couldn't add invalid setting to the settings list ({}: {})", - setting.key, setting.description - ); + parents.push(auto_splitter_properties); + } + WidgetKind::Choice { + default_option_key, + options, + } => { + let property = obs_properties_add_list( + *parents.last().unwrap(), + setting_key.as_ptr(), + widget_description.as_ptr(), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING, + ); + + if let Some(tooltip) = widget + .tooltip + .as_ref() + .and_then(|t| CString::new(t.as_bytes()).ok()) + { + obs_property_set_long_description(property, tooltip.as_ptr()); + } + + for option in &**options { + let option_key = + CString::from_vec_with_nul(format!("{}\0", option.key).into()); + let option_description = CString::from_vec_with_nul( + format!("{}\0", option.description).into(), + ); + + if let (Ok(option_key), Ok(option_description)) = + (option_key, option_description) + { + obs_property_list_add_string( + property, + option_description.as_ptr(), + option_key.as_ptr(), + ); + } + } + + if let Some(value) = state + .auto_splitter_map + .get(&widget.key) + .and_then(|v| v.as_string()) + { + if let Ok(value) = + CString::from_vec_with_nul(format!("{}\0", value).into()) + { + obs_data_set_string( + state.obs_settings, + setting_key.as_ptr(), + value.as_ptr(), + ); + } + } else { + obs_data_erase(state.obs_settings, setting_key.as_ptr()); + } + + if let Ok(default_option_key) = + CString::from_vec_with_nul(format!("{}\0", default_option_key).into()) + { + obs_data_set_default_string( + state.obs_settings, + setting_key.as_ptr(), + default_option_key.as_ptr(), + ); + } + } + WidgetKind::FileSelect { filters } => { + let mut filter_buf = Vec::new(); + build_filter(filters, &mut filter_buf); + filter_buf.push(b'\0'); + + let property = obs_properties_add_path( + *parents.last().unwrap(), + setting_key.as_ptr(), + widget_description.as_ptr(), + OBS_PATH_FILE, + filter_buf.as_ptr().cast(), + ptr::null(), + ); + + if let Some(tooltip) = widget + .tooltip + .as_ref() + .and_then(|t| CString::new(t.as_bytes()).ok()) + { + obs_property_set_long_description(property, tooltip.as_ptr()); + } + + if let Some(value) = state + .auto_splitter_map + .get(&widget.key) + .and_then(|v| v.as_string()) + .and_then(|s| wasi_path::to_native(s, true)) + .filter(|p| p.exists()) + .and_then(|p| CString::new(p.as_os_str().as_encoded_bytes()).ok()) + { + obs_data_set_string( + state.obs_settings, + setting_key.as_ptr(), + value.as_ptr(), + ); + } else { + obs_data_erase(state.obs_settings, setting_key.as_ptr()); + } + } } } } + + obs_properties_add_group( + props, + cstr!(c"auto_splitter_settings_group"), + cstr!(c"Auto Splitter Settings"), + OBS_GROUP_NORMAL, + auto_splitter_properties, + ); } props @@ -1646,12 +1555,12 @@ unsafe extern "C" fn get_defaults(settings: *mut obs_data_t) { } unsafe extern "C" fn activate(data: *mut c_void) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.activated = true; } unsafe extern "C" fn deactivate(data: *mut c_void) { - let state: &mut State = &mut *data.cast(); + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); state.activated = false; } @@ -1661,11 +1570,10 @@ fn default_run() -> (Run, bool) { (run, false) } -unsafe extern "C" fn update(data: *mut c_void, settings: *mut obs_data_t) { - debug!("Reloading settings."); +unsafe extern "C" fn update(data: *mut c_void, settings_obj: *mut obs_data_t) { + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - let state: &mut State = &mut *data.cast(); - let settings = parse_settings(settings); + let settings = parse_settings(settings_obj); handle_splits_path_change(state, settings.splits_path); @@ -1691,6 +1599,69 @@ unsafe extern "C" fn update(data: *mut c_void, settings: *mut obs_data_t) { auto_splitter_load(&state.global_timer, local_auto_splitter.clone()); } } + + loop { + let Some(original) = state.global_timer.auto_splitter.settings_map() else { + break; + }; + let mut map = original.clone(); + + for widget in state.auto_splitter_widgets.iter() { + let key = &widget.key; + let Ok(data_key) = CString::new(format!("auto_splitter_setting_{}", key)) else { + continue; + }; + + match &widget.kind { + WidgetKind::Title { .. } => {} + WidgetKind::Bool { default_value } => { + let value = obs_data_get_bool(settings_obj, data_key.as_ptr()); + if value != *default_value { + map.insert(key.clone(), Value::Bool(value)); + } else { + map.remove(key); + } + } + WidgetKind::Choice { + default_option_key, .. + } => { + if let Some(value) = + CStr::from_ptr(obs_data_get_string(settings_obj, data_key.as_ptr())) + .to_str() + .ok() + .filter(|v| *v != &**default_option_key) + { + map.insert(key.clone(), Value::String(Arc::from(value))); + } else { + map.remove(key); + } + } + WidgetKind::FileSelect { .. } => { + if let Some(value) = + CStr::from_ptr(obs_data_get_string(settings_obj, data_key.as_ptr())) + .to_str() + .ok() + .filter(|v| !v.is_empty()) + .and_then(|s| wasi_path::from_native(Path::new(s))) + { + map.insert(key.clone(), Value::String(Arc::from(value))); + } else { + map.remove(key); + } + } + } + } + + if state + .global_timer + .auto_splitter + .set_settings_map_if_unchanged(&original, map.clone()) + != Some(false) + { + state.auto_splitter_map = map; + break; + } + } } if state.width != settings.width || state.height != settings.height { @@ -1768,7 +1739,7 @@ impl Log for ObsLog { #[no_mangle] pub extern "C" fn obs_module_load() -> bool { static SOURCE_INFO: UnsafeMultiThread = UnsafeMultiThread(obs_source_info { - id: cstr!("livesplit-one"), + id: cstr!(c"livesplit-one"), type_: OBS_SOURCE_TYPE_INPUT, output_flags: OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW @@ -1836,3 +1807,203 @@ pub extern "C" fn obs_module_load() -> bool { true } + +#[cfg(feature = "auto-splitting")] +fn build_filter(filters: &[FileFilter], output: &mut Vec) { + for filter in filters.iter() { + match filter { + FileFilter::Name { + description, + pattern, + } => { + if pattern.contains(";;") { + continue; + } + if !output.is_empty() { + output.extend_from_slice(b";;"); + } + match &description { + Some(description) => { + output.extend( + description + .trim() + .split(";;") + .flat_map(|s| s.bytes()) + .filter(|b| *b != b'(' && *b != b')'), + ); + output.extend_from_slice(b" ("); + } + None => { + let mime = pattern.split(' ').find_map(|pat| { + let (name, ext) = pat.rsplit_once('.')?; + if name != "*" { + return None; + } + if ext.contains('*') { + return None; + } + mime_guess::from_ext(ext).first() + }); + if let Some(mime) = mime { + append_mime_desc( + mime.type_().as_str(), + mime.subtype().as_str(), + output, + ); + } else { + let mut ext_count = 0; + + let only_contains_extensions = pattern.split(' ').all(|pat| { + ext_count += 1; + let Some((name, ext)) = pat.rsplit_once('.') else { + return false; + }; + name == "*" && !ext.contains('*') + }); + + if only_contains_extensions { + let mut char_buf = [0; 4]; + + for (i, ext) in pattern + .split(' ') + .filter_map(|pat| { + let (_, ext) = pat.rsplit_once('.')?; + Some(ext) + }) + .enumerate() + { + if i != 0 { + output.extend_from_slice(if i + 1 != ext_count { + b", " + } else { + b" or " + }); + } + + for c in ext + .chars() + .flat_map(|c| c.to_uppercase()) + .filter(|c| *c != '(' && *c != ')') + { + output.extend_from_slice( + c.encode_utf8(&mut char_buf).as_bytes(), + ); + } + } + + output.extend_from_slice(b" files ("); + } else { + output.extend( + pattern.trim().bytes().filter(|&c| c != b'(' && c != b')'), + ); + output.extend_from_slice(b" ("); + } + } + } + } + + for (i, pattern) in pattern.split(' ').enumerate() { + if i != 0 { + output.push(b' '); + } + output.extend_from_slice(pattern.as_bytes()); + } + } + FileFilter::MimeType(mime_type) => { + let Some((top, sub)) = mime_type.split_once('/') else { + continue; + }; + if top == "*" { + continue; + } + let Some(extensions) = mime_guess::get_extensions(top, sub) else { + continue; + }; + + if !output.is_empty() { + output.extend_from_slice(b";;"); + } + + append_mime_desc(top, sub, output); + + for (i, extension) in extensions.iter().enumerate() { + if i != 0 { + output.push(b' '); + } + output.extend_from_slice(b"*."); + output.extend_from_slice(extension.as_bytes()); + } + } + } + + output.push(b')'); + } + + if !output.is_empty() { + output.extend_from_slice(b";;"); + } + output.extend_from_slice(b"All files (*.*)"); +} + +#[cfg(feature = "auto-splitting")] +fn append_mime_desc(top: &str, sub: &str, output: &mut Vec) { + let mut char_buf = [0; 4]; + + if sub != "*" { + // Strip vendor and x- prefixes + + let sub = sub + .strip_prefix("vnd.") + .unwrap_or(sub) + .strip_prefix("x-") + .unwrap_or(sub); + + // Capitalize the first letter + + let mut chars = sub.chars(); + if let Some(c) = chars + .by_ref() + .map(|c| match c { + '-' | '.' | '+' | '|' | '(' | ')' => ' ', + _ => c, + }) + .next() + { + for c in c.to_uppercase() { + output.extend_from_slice(c.encode_utf8(&mut char_buf).as_bytes()); + } + } + + // Only capitalize chunks of the rest that are 4 characters or less as a + // heuristic to detect acronyms + + let rem = chars.as_str(); + for (i, piece) in rem.split(&['-', '.', '+', '|', ' ', '(', ')']).enumerate() { + if i != 0 { + output.push(b' '); + } + if piece.len() <= 4 - (i == 0) as usize { + for c in piece.chars() { + for c in c.to_uppercase() { + output.extend_from_slice(c.encode_utf8(&mut char_buf).as_bytes()); + } + } + } else { + output.extend_from_slice(piece.as_bytes()); + } + } + + output.push(b' '); + } + + let mut chars = top.chars(); + if sub == "*" { + if let Some(c) = chars.by_ref().find(|c| *c != '(' && *c != ')') { + for c in c.to_uppercase() { + output.extend_from_slice(c.encode_utf8(&mut char_buf).as_bytes()); + } + } + } + output.extend(chars.as_str().bytes().filter(|b| *b != b'(' && *b != b')')); + output.extend_from_slice(if top == "image" { b"s (" } else { b" files (" }); +}