From fcfa55d0aa9b1e3a8f82bfc6fbd698974afbd65d Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 6 May 2026 23:53:01 +0200 Subject: [PATCH] record raw events everywhere --- record-daemon/Cargo.lock | 693 +++++++-------- record-daemon/src/ipc/protocol.rs | 6 +- record-daemon/src/lqp/client.rs | 90 +- record-daemon/src/lqp/events.rs | 752 ++++------------ record-daemon/src/lqp/mod.rs | 11 +- record-daemon/src/lqp/websocket.rs | 1040 +++-------------------- record-daemon/src/main.rs | 79 +- record-daemon/src/state/machine.rs | 53 +- record-daemon/src/timeline/mapper.rs | 16 +- record-daemon/src/timeline/mod.rs | 80 +- record-daemon/src/timeline/store.rs | 20 +- tauri-app/src/components/GameReview.vue | 14 +- tauri-app/src/types/timeline.ts | 37 +- 13 files changed, 848 insertions(+), 2043 deletions(-) diff --git a/record-daemon/Cargo.lock b/record-daemon/Cargo.lock index c2647e3..0a7e6ef 100644 --- a/record-daemon/Cargo.lock +++ b/record-daemon/Cargo.lock @@ -149,9 +149,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -187,7 +187,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools", @@ -224,9 +224,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -278,9 +278,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.57" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -288,12 +288,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cexpr" version = "0.6.0" @@ -358,9 +352,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -380,9 +374,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -407,9 +401,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -485,9 +479,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -578,9 +572,9 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deranged" @@ -659,15 +653,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", ] [[package]] name = "display-info" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bce305c30b9819766d4cb345932806d98bc8f142fce5269eb3cbc41f6e3e375" +checksum = "9e0aca670967c2528799e316f9f97913efcc034867614d55681dd41a1c2f7830" dependencies = [ "fxhash", "log", @@ -757,29 +751,15 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -1042,7 +1022,7 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "futures-channel", "futures-core", "futures-executor", @@ -1118,9 +1098,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1157,9 +1137,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -1266,21 +1246,20 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.13", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1302,15 +1281,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", - "rustls 0.23.37", - "rustls-pki-types", + "rustls 0.23.40", "tokio", "tokio-rustls 0.26.4", "tower-service", @@ -1328,7 +1306,7 @@ dependencies = [ "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", @@ -1367,12 +1345,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1380,9 +1359,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1393,9 +1372,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1407,15 +1386,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1427,15 +1406,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1465,9 +1444,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1489,12 +1468,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1505,16 +1484,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1538,25 +1507,52 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if 1.0.4", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -1570,10 +1566,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if 1.0.4", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1602,9 +1600,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1650,7 +1648,7 @@ dependencies = [ "libloading 0.9.0", "libobs", "log", - "reqwest 0.13.2", + "reqwest 0.13.3", "semver", "serde", "sevenz-rust", @@ -1729,14 +1727,14 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", ] [[package]] @@ -1747,9 +1745,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1829,9 +1827,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -1879,9 +1877,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-derive" @@ -1918,7 +1916,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -1939,7 +1937,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -1950,7 +1948,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -1961,7 +1959,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "dispatch2", "libc", @@ -1974,7 +1972,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "dispatch2", "libc", @@ -2000,7 +1998,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2012,7 +2010,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2031,7 +2029,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -2044,7 +2042,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", ] @@ -2055,7 +2053,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -2066,7 +2064,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -2142,17 +2140,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -2166,7 +2158,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -2175,9 +2167,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2239,9 +2231,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "quick-error" @@ -2260,9 +2252,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.2" +version = "0.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" dependencies = [ "memchr", ] @@ -2279,7 +2271,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.37", + "rustls 0.23.40", "socket2 0.6.3", "thiserror 2.0.18", "tokio", @@ -2297,10 +2289,10 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -2346,9 +2338,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2357,9 +2349,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2434,7 +2426,7 @@ dependencies = [ "tokio-test", "tokio-tungstenite", "tokio-util", - "toml 1.0.7+spec-1.1.0", + "toml", "tracing", "tracing-subscriber", "uuid", @@ -2448,16 +2440,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -2554,21 +2546,21 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.13", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-util", "js-sys", "log", @@ -2576,7 +2568,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -2622,9 +2614,18 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] name = "rustix" @@ -2632,7 +2633,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -2667,14 +2668,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -2702,9 +2703,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2712,19 +2713,19 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", "once_cell", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.13", "security-framework", "security-framework-sys", "webpki-root-certs", @@ -2760,9 +2761,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -2822,7 +2823,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2841,9 +2842,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2890,9 +2891,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -2965,9 +2966,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", @@ -2997,9 +2998,25 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" @@ -3019,7 +3036,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cursor-icon", "libc", "log", @@ -3130,7 +3147,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -3157,14 +3174,14 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.7" +version = "7.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 0.9.12+spec-1.1.0", + "toml", "version-compare", ] @@ -3282,9 +3299,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3307,9 +3324,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -3324,9 +3341,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3360,7 +3377,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.37", + "rustls 0.23.40", "tokio", ] @@ -3417,78 +3434,54 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml" -version = "1.0.7+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime 1.0.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.0.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.5+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", - "toml_datetime 1.0.1+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -3507,20 +3500,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "a28f0d049ccfaa566e14e9663d304d8577427b368cb4710a20528690287a738b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-util", "http 1.4.0", "http-body 1.0.1", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3614,7 +3607,7 @@ dependencies = [ "http 1.4.0", "httparse", "log", - "rand 0.8.5", + "rand 0.8.6", "rustls 0.22.4", "rustls-pki-types", "sha1", @@ -3625,9 +3618,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" @@ -3679,9 +3672,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -3744,11 +3737,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -3757,14 +3750,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -3775,23 +3768,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if 1.0.4", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3799,9 +3788,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -3812,9 +3801,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -3860,7 +3849,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -3868,9 +3857,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", @@ -3881,11 +3870,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "rustix", "wayland-backend", "wayland-scanner", @@ -3897,16 +3886,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ "rustix", "wayland-client", @@ -3915,11 +3904,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.11" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -3931,7 +3920,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3940,11 +3929,11 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3953,11 +3942,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3966,29 +3955,29 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.9" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml 0.39.2", + "quick-xml 0.39.3", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "pkg-config", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -4006,9 +3995,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -4025,14 +4014,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -4204,15 +4193,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -4249,21 +4229,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -4321,12 +4286,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4345,12 +4304,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4369,12 +4322,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4405,12 +4352,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4429,12 +4370,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4453,12 +4388,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4477,12 +4406,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4503,15 +4426,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -4545,6 +4462,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -4594,7 +4517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -4626,9 +4549,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x11rb" @@ -4672,9 +4595,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4683,9 +4606,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4695,18 +4618,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -4715,18 +4638,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4742,9 +4665,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4753,9 +4676,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4764,9 +4687,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -4787,9 +4710,9 @@ checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-jpeg" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] diff --git a/record-daemon/src/ipc/protocol.rs b/record-daemon/src/ipc/protocol.rs index 4d93575..141f94d 100644 --- a/record-daemon/src/ipc/protocol.rs +++ b/record-daemon/src/ipc/protocol.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::config::Settings; -use crate::lqp::GameEvent; use crate::recording::RecordingResult; use crate::state::DaemonStatus; use crate::timeline::RecordingMetadata; @@ -153,7 +152,10 @@ pub enum IpcNotification { }, /// Game event received. - GameEvent { event: Box }, + GameEvent { + event_type: String, + raw_data: serde_json::Value, + }, /// Daemon status changed. StatusChanged { status: DaemonStatus }, diff --git a/record-daemon/src/lqp/client.rs b/record-daemon/src/lqp/client.rs index 5d83355..bc66c49 100644 --- a/record-daemon/src/lqp/client.rs +++ b/record-daemon/src/lqp/client.rs @@ -11,7 +11,7 @@ use tracing::{debug, error, info, trace, warn}; use super::auth::LockfileCredentials; use super::endpoints; -use super::events::GameEvent; +use super::events::EVENT_TYPE_GAME_END; use super::state::{ClientState, GameflowPhase}; use super::tls::create_insecure_tls_config; use super::websocket::{parse_live_client_event, parse_websocket_message, ParsedEvent}; @@ -186,23 +186,35 @@ impl LqpClient { } if let Some(parsed) = parse_websocket_message(&text) { // Update state based on event - Self::update_state_from_event(&state, &parsed.event).await; + Self::update_state_from_event(&state, &parsed).await; // Check for duplicate GameStart events - if let GameEvent::GameStart(ref info) = parsed.event { + if parsed.event_type == super::events::EVENT_TYPE_GAME_START { + let game_id = parsed + .raw_data + .get("gameId") + .or_else(|| { + parsed + .raw_data + .get("gameData") + .and_then(|gd| gd.get("gameId")) + }) + .and_then(|v| v.as_u64()) + .unwrap_or(0); + let mut last_game_id = last_emitted_game_id.write().await; - if *last_game_id == Some(info.game_id) { + if *last_game_id == Some(game_id) && game_id != 0 { info!( "Skipping duplicate GameStart event for game_id={}", - info.game_id + game_id ); continue; } - *last_game_id = Some(info.game_id); + *last_game_id = Some(game_id); } // Reset last_emitted_game_id on GameEnd to allow new game starts - if let GameEvent::GameEnd(_) = &parsed.event { + if parsed.event_type == EVENT_TYPE_GAME_END { *last_emitted_game_id.write().await = None; } @@ -219,23 +231,35 @@ impl LqpClient { if !text.is_empty() { if let Some(parsed) = parse_websocket_message(&text) { // Update state based on event - Self::update_state_from_event(&state, &parsed.event).await; + Self::update_state_from_event(&state, &parsed).await; // Check for duplicate GameStart events - if let GameEvent::GameStart(ref info) = parsed.event { + if parsed.event_type == super::events::EVENT_TYPE_GAME_START { + let game_id = parsed + .raw_data + .get("gameId") + .or_else(|| { + parsed + .raw_data + .get("gameData") + .and_then(|gd| gd.get("gameId")) + }) + .and_then(|v| v.as_u64()) + .unwrap_or(0); + let mut last_game_id = last_emitted_game_id.write().await; - if *last_game_id == Some(info.game_id) { + if *last_game_id == Some(game_id) && game_id != 0 { info!( "Skipping duplicate GameStart event for game_id={}", - info.game_id + game_id ); continue; } - *last_game_id = Some(info.game_id); + *last_game_id = Some(game_id); } // Reset last_emitted_game_id on GameEnd to allow new game starts - if let GameEvent::GameEnd(_) = &parsed.event { + if parsed.event_type == EVENT_TYPE_GAME_END { *last_emitted_game_id.write().await = None; } @@ -277,19 +301,36 @@ impl LqpClient { Ok(()) } - /// Update internal state from a game event. - async fn update_state_from_event(state: &Arc>, event: &GameEvent) { + /// Update internal state from a parsed event. + async fn update_state_from_event(state: &Arc>, parsed: &ParsedEvent) { let mut state = state.write().await; - match event { - GameEvent::GameStart(info) => { + match parsed.event_type.as_str() { + super::events::EVENT_TYPE_GAME_START => { state.phase = GameflowPhase::InProgress; - state.game_id = Some(info.game_id); - state.champion = info.champion.clone(); + state.game_id = parsed + .raw_data + .get("gameId") + .or_else(|| { + parsed + .raw_data + .get("gameData") + .and_then(|gd| gd.get("gameId")) + }) + .and_then(|v| v.as_u64()); + // Champion is not extracted here — raw session data has it } - GameEvent::GameEnd(_) => { + super::events::EVENT_TYPE_GAME_END => { state.phase = GameflowPhase::EndOfGame; } + super::events::EVENT_TYPE_PHASE_CHANGE => { + let phase_str = parsed + .raw_data + .as_str() + .or_else(|| parsed.raw_data.get("phase").and_then(|v| v.as_str())) + .unwrap_or(""); + state.phase = GameflowPhase::from(phase_str); + } _ => {} } } @@ -497,6 +538,7 @@ impl LqpClient { Ok(events) => { // The response has an "Events" key containing the array let events_array = events.get("Events").and_then(|e| e.as_array()); + if let Some(events_array) = events_array { let event_count = events_array.len(); if event_count > 0 { @@ -522,10 +564,12 @@ impl LqpClient { // Parse and broadcast the event if let Some(parsed) = parse_live_client_event(event) { - info!("Parsed live client event: {:?}", parsed.event); + info!( + "Parsed live client event: type={}", + parsed.event_type + ); // Update state based on event - Self::update_state_from_event(&state, &parsed.event) - .await; + Self::update_state_from_event(&state, &parsed).await; // Broadcast event if event_sender.send(parsed).is_err() { diff --git a/record-daemon/src/lqp/events.rs b/record-daemon/src/lqp/events.rs index 9773497..ede4798 100644 --- a/record-daemon/src/lqp/events.rs +++ b/record-daemon/src/lqp/events.rs @@ -1,596 +1,141 @@ -//! Game event types from the League Client API. +//! Event type constants and metadata structures for the League Client API. //! -//! These events are received via WebSocket subscription to LQP endpoints. +//! The record-daemon stores raw API JSON data only — no typed event parsing. +//! Event types are simple string constants derived from the URI, used for +//! classification and state machine transitions. The tauri-app parses the +//! raw data it needs on the frontend side. -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -/// A game event received from the League Client. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "eventType", rename_all = "camelCase")] -pub enum GameEvent { - /// Match found in queue. - #[serde(rename = "lcu-match-found")] - MatchFound(MatchInfo), - - /// Champion select phase started. - #[serde(rename = "lcu-champ-select-start")] - ChampSelectStart(ChampSelectStartInfo), - - /// Player picked a champion. - #[serde(rename = "lcu-champion-pick")] - ChampionPick(ChampionPickInfo), - - /// Game has started. - #[serde(rename = "lcu-game-start")] - GameStart(Box), - - /// Player killed an enemy. - #[serde(rename = "lcu-kill")] - Kill(KillEvent), - - /// Player died. - #[serde(rename = "lcu-death")] - Death(DeathEvent), - - /// Objective was taken. - #[serde(rename = "lcu-objective")] - Objective(ObjectiveEvent), - - /// In-game stats update. - #[serde(rename = "lcu-stats-update")] - StatsUpdate(InGameStats), - - /// Game has ended. - #[serde(rename = "lcu-game-end")] - GameEnd(GameEndInfo), - - /// Gameflow phase changed. - #[serde(rename = "lcu-phase-change")] - PhaseChange(PhaseChangeInfo), - - /// LP changed after a ranked game. - #[serde(rename = "lcu-lp-change")] - LpChange(LpChangeInfo), - - /// Unknown event type. - #[serde(other)] - Unknown, -} - -/// Phase change event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PhaseChangeInfo { - /// The new phase. - pub phase: String, - - /// Timestamp when phase changed. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// LP change event data after a ranked game. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LpChangeInfo { - /// Queue type (RANKED_SOLO_5x5, RANKED_FLEX_SR, etc.). - pub queue_type: String, - - /// LP change amount (positive for gain, negative for loss). - pub lp_change: i32, - - /// LP before the game. - pub lp_before: i32, - - /// LP after the game. - pub lp_after: i32, - - /// Current tier (IRON, BRONZE, SILVER, GOLD, PLATINUM, DIAMOND, MASTER, GRANDMASTER, CHALLENGER). - pub tier: String, - - /// Current division (I, II, III, IV). - #[serde(default)] - pub division: Option, - - /// Current league points. - #[serde(default)] - pub league_points: Option, - - /// Whether the player is in a promotional series. - #[serde(default)] - pub in_promos: bool, - - /// Promotional series progress (e.g., "W:2 L:1"). - #[serde(default)] - pub promo_progress: Option, - - /// Number of wins in promo series. - #[serde(default)] - pub promo_wins: Option, - - /// Number of losses in promo series. - #[serde(default)] - pub promo_losses: Option, - - /// Total games played in this queue. - #[serde(default)] - pub total_games: Option, - - /// Total wins in this queue. - #[serde(default)] - pub total_wins: Option, - - /// Total losses in this queue. - #[serde(default)] - pub total_losses: Option, - - /// Timestamp when LP change occurred. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Match found event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MatchInfo { - /// Match ID. - #[serde(default)] - pub match_id: Option, - - /// Queue type (ranked, normal, aram, etc.). - pub queue_type: String, - - /// Queue ID (numeric identifier). - #[serde(default)] - pub queue_id: Option, - - /// Map name. - pub map: String, - - /// Game mode. - pub game_mode: String, - - /// Timestamp when match was found. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Champion select start event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChampSelectStartInfo { - /// Session ID. - #[serde(default)] - pub session_id: Option, - - /// Team (100 = blue, 200 = red). - #[serde(default)] - pub team: Option, - - /// Timestamp when champ select started. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Champion pick event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChampionPickInfo { - /// Player's summoner name. - pub summoner_name: String, - - /// Champion ID. - pub champion_id: u32, - - /// Champion name. - pub champion_name: String, - - /// Whether this is the local player's pick. - #[serde(default)] - pub is_local_player: bool, - - /// Skin ID selected. - #[serde(default)] - pub skin_id: Option, - - /// Skin name. - #[serde(default)] - pub skin_name: Option, - - /// Timestamp when champion was picked. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} +// ============================================================================ +// Event Type Constants +// ============================================================================ +/// Match found in queue. +pub const EVENT_TYPE_MATCH_FOUND: &str = "match_found"; +/// Champion select phase started. +pub const EVENT_TYPE_CHAMP_SELECT_START: &str = "champ_select_start"; +/// Player picked a champion. +pub const EVENT_TYPE_CHAMPION_PICK: &str = "champion_pick"; +/// Game has started. +pub const EVENT_TYPE_GAME_START: &str = "game_start"; +/// Player killed an enemy. +pub const EVENT_TYPE_KILL: &str = "kill"; +/// Player died. +pub const EVENT_TYPE_DEATH: &str = "death"; +/// Objective was taken. +pub const EVENT_TYPE_OBJECTIVE: &str = "objective"; /// In-game stats update. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InGameStats { - /// Current kills. - pub kills: u32, +pub const EVENT_TYPE_STATS_UPDATE: &str = "stats_update"; +/// Game has ended. +pub const EVENT_TYPE_GAME_END: &str = "game_end"; +/// Gameflow phase changed. +pub const EVENT_TYPE_PHASE_CHANGE: &str = "phase_change"; +/// LP changed after a ranked game. +pub const EVENT_TYPE_LP_CHANGE: &str = "lp_change"; +/// Unknown / unclassified event. +pub const EVENT_TYPE_UNKNOWN: &str = "unknown"; - /// Current deaths. - pub deaths: u32, - - /// Current assists. - pub assists: u32, - - /// Current creep score. - #[serde(default)] - pub creep_score: u32, - - /// Current gold. - #[serde(default)] - pub gold: u32, - - /// Current level. - #[serde(default)] - pub level: u32, - - /// Game time in seconds. - #[serde(default)] - pub game_time: f64, - - /// Timestamp of the stats update. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Game start event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GameStartInfo { - /// Game ID. - pub game_id: u64, - - /// Server address. - #[serde(default)] - pub server: Option, - - /// Player's champion name. - #[serde(default)] - pub champion: Option, - - /// Player's summoner name. - #[serde(default)] - pub summoner_name: Option, - - /// Team (100 = blue, 200 = red). - #[serde(default)] - pub team: Option, - - /// Queue type (ranked, normal, aram, etc.). - #[serde(default)] - pub queue_type: Option, - - /// Queue ID. - #[serde(default)] - pub queue_id: Option, - - /// Game mode. - #[serde(default)] - pub game_mode: Option, - - /// Map name. - #[serde(default, rename = "map")] - pub map_name: Option, - - /// Full gameflow session data (if available). - #[serde(default)] - pub session: Option, - - /// Game start timestamp. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Kill event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct KillEvent { - /// Killer summoner name. - pub killer: String, - - /// Killer champion name. - #[serde(default)] - pub killer_champion: Option, - - /// Victim summoner name. - pub victim: String, - - /// Victim champion name. - #[serde(default)] - pub victim_champion: Option, - - /// Whether this was a solo kill. - #[serde(default)] - pub solo_kill: bool, - - /// Number of assists. - #[serde(default)] - pub assists: u32, - - /// Kill position on map. - #[serde(default)] - pub position: Option, - - /// Game time when kill occurred. - #[serde(default)] - pub game_time: Option, - - /// Real timestamp. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Death event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeathEvent { - /// Killer summoner name (if killed by champion). - #[serde(default)] - pub killer: Option, - - /// Killer champion name. - #[serde(default)] - pub killer_champion: Option, - - /// Death cause (champion, minion, tower, etc.). - pub cause: String, - - /// Death position on map. - #[serde(default)] - pub position: Option, - - /// Game time when death occurred. - #[serde(default)] - pub game_time: Option, - - /// Real timestamp. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Objective event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ObjectiveEvent { - /// Type of objective. - pub objective_type: ObjectiveType, - - /// Team that took the objective (100 = blue, 200 = red). - pub team: u32, - - /// Whether the player participated. - #[serde(default)] - pub participated: bool, - - /// Game time when objective was taken. - #[serde(default)] - pub game_time: Option, - - /// Real timestamp. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Type of objective. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ObjectiveType { - Dragon, - Herald, - Baron, - Tower, - Inhibitor, - Nexus, - RiftHerald, - ElderDragon, -} - -/// 2D position on the map. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Position { - pub x: f32, - pub y: f32, -} - -/// Game end event data. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GameEndInfo { - /// Game ID. - pub game_id: u64, - - /// Whether the player's team won. - pub victory: bool, - - /// Game duration in seconds. - pub duration: f64, - - /// Player's stats. - #[serde(default)] - pub stats: Option, - - /// End timestamp. - #[serde(default = "Utc::now")] - pub timestamp: DateTime, -} - -/// Player statistics at game end. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PlayerStats { - /// Kills. - pub kills: u32, - - /// Deaths. - pub deaths: u32, - - /// Assists. - pub assists: u32, - - /// Total gold earned. - pub gold_earned: u32, - - /// Total damage dealt. - pub damage_dealt: u64, - - /// Total damage taken. - pub damage_taken: u64, - - /// Minions killed (CS). - pub minions_killed: u32, - - /// Vision score. - #[serde(default)] - pub vision_score: f64, -} - -/// Raw event data from LQP WebSocket. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RawEvent { - /// Event type URI. - #[serde(rename = "uri")] - pub uri: String, - - /// Event data. - pub data: EventData, -} - -/// Event data payload. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum EventData { - /// Game event. - GameEvent(Box), - - /// Raw JSON value. - Raw(serde_json::Value), -} - -impl GameEvent { - /// Parse a game event from raw WebSocket data. - pub fn from_json(value: &serde_json::Value) -> Option { - match serde_json::from_value(value.clone()) { - Ok(event) => Some(event), - Err(e) => { - // Log the parsing error for debugging - tracing::warn!("Failed to parse game event: {}. Data: {:?}", e, value); - None - } +/// Generate a brief human-readable description from raw event data. +/// +/// This is used for logging and timeline display. It extracts minimal +/// information from the raw JSON — no full parsing required. +pub fn describe_event(event_type: &str, raw_data: &serde_json::Value) -> String { + match event_type { + EVENT_TYPE_MATCH_FOUND => { + let queue = raw_data + .get("queueType") + .or_else(|| { + raw_data + .get("gameData") + .and_then(|gd| gd.get("queue")) + .and_then(|q| q.get("name")) + }) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + format!("Match found: {}", queue) } - } - - /// Check if this event is relevant for recording. - pub fn is_relevant(&self) -> bool { - !matches!(self, GameEvent::Unknown) - } - - /// Get a human-readable description of the event. - pub fn description(&self) -> String { - match self { - GameEvent::MatchFound(info) => { - format!("Match found: {} ({})", info.game_mode, info.queue_type) - } - GameEvent::ChampSelectStart(info) => { - format!("Champion select started (team: {:?})", info.team) - } - GameEvent::ChampionPick(pick) => { - format!("{} picked {}", pick.summoner_name, pick.champion_name) - } - GameEvent::GameStart(info) => { - format!("Game started: ID {}", info.game_id) - } - GameEvent::Kill(kill) => { - format!("{} killed {}", kill.killer, kill.victim) - } - GameEvent::Death(death) => { - format!("Player died ({})", death.cause) - } - GameEvent::Objective(obj) => { - let team = if obj.team == 100 { "Blue" } else { "Red" }; - format!("{} took {:?}", team, obj.objective_type) - } - GameEvent::StatsUpdate(stats) => { - format!( - "Stats: {}/{}/{} CS: {} Gold: {}", - stats.kills, stats.deaths, stats.assists, stats.creep_score, stats.gold - ) - } - GameEvent::GameEnd(end) => { - let result = if end.victory { "Victory" } else { "Defeat" }; - format!("Game ended: {} ({:.1}s)", result, end.duration) - } - GameEvent::PhaseChange(info) => { - format!("Phase changed to: {}", info.phase) - } - GameEvent::LpChange(lp) => { - let sign = if lp.lp_change >= 0 { "+" } else { "" }; - format!( - "LP Change: {}{} LP ({} {} {} -> {} LP)", - sign, - lp.lp_change, - lp.tier, - lp.division.as_deref().unwrap_or(""), - lp.queue_type, - lp.lp_after - ) - } - GameEvent::Unknown => "Unknown event".to_string(), + EVENT_TYPE_CHAMP_SELECT_START => "Champion select started".to_string(), + EVENT_TYPE_CHAMPION_PICK => { + let champ = raw_data + .get("championName") + .or_else(|| raw_data.get("championName")) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + format!("Champion picked: {}", champ) } - } - - /// Get the event type name for categorization. - pub fn event_type_name(&self) -> &'static str { - match self { - GameEvent::MatchFound(_) => "match_found", - GameEvent::ChampSelectStart(_) => "champ_select_start", - GameEvent::ChampionPick(_) => "champion_pick", - GameEvent::GameStart(_) => "game_start", - GameEvent::Kill(_) => "kill", - GameEvent::Death(_) => "death", - GameEvent::Objective(_) => "objective", - GameEvent::StatsUpdate(_) => "stats_update", - GameEvent::GameEnd(_) => "game_end", - GameEvent::PhaseChange(_) => "phase_change", - GameEvent::LpChange(_) => "lp_change", - GameEvent::Unknown => "unknown", + EVENT_TYPE_GAME_START => { + let game_id = raw_data + .get("gameId") + .or_else(|| raw_data.get("gameData").and_then(|gd| gd.get("gameId"))) + .and_then(|v| v.as_u64()) + .unwrap_or(0); + format!("Game started: ID {}", game_id) } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_kill_event() { - let json = serde_json::json!({ - "eventType": "lcu-kill", - "killer": "Player1", - "killerChampion": "Ahri", - "victim": "Player2", - "victimChampion": "Lux", - "soloKill": true, - "assists": 0 - }); - - let event: GameEvent = serde_json::from_value(json).unwrap(); - if let GameEvent::Kill(kill) = event { - assert_eq!(kill.killer, "Player1"); - assert!(kill.solo_kill); - } else { - panic!("Expected Kill event"); + EVENT_TYPE_KILL => { + let killer = raw_data + .get("KillerName") + .or_else(|| raw_data.get("killer")) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + let victim = raw_data + .get("VictimName") + .or_else(|| raw_data.get("victim")) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + format!("{} killed {}", killer, victim) } - } - - #[test] - fn test_objective_type_deserialization() { - let json = serde_json::json!("dragon"); - let obj: ObjectiveType = serde_json::from_value(json).unwrap(); - assert_eq!(obj, ObjectiveType::Dragon); + EVENT_TYPE_DEATH => { + let cause = raw_data + .get("DeathCause") + .or_else(|| raw_data.get("cause")) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + format!("Player died ({})", cause) + } + EVENT_TYPE_OBJECTIVE => { + let obj_type = raw_data + .get("EventName") + .or_else(|| raw_data.get("objectiveType")) + .and_then(|v| v.as_str()) + .unwrap_or("objective"); + let team = raw_data + .get("Team") + .or_else(|| raw_data.get("team")) + .and_then(|v| v.as_u64()) + .map(|t| if t == 100 { "Blue" } else { "Red" }) + .unwrap_or("Unknown"); + format!("{} took {}", team, obj_type) + } + EVENT_TYPE_STATS_UPDATE => "Stats update".to_string(), + EVENT_TYPE_GAME_END => { + let victory = raw_data + .get("victory") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let result = if victory { "Victory" } else { "Defeat" }; + format!("Game ended: {}", result) + } + EVENT_TYPE_PHASE_CHANGE => { + let phase = raw_data + .as_str() + .or_else(|| raw_data.get("phase").and_then(|v| v.as_str())) + .unwrap_or("Unknown"); + format!("Phase changed to: {}", phase) + } + EVENT_TYPE_LP_CHANGE => { + let lp_change = raw_data + .get("lpChange") + .and_then(|v| v.as_i64()) + .unwrap_or(0); + let tier = raw_data + .get("tier") + .and_then(|v| v.as_str()) + .unwrap_or("UNRANKED"); + let sign = if lp_change >= 0 { "+" } else { "" }; + format!("LP Change: {}{} LP ({})", sign, lp_change, tier) + } + _ => "Unknown event".to_string(), } } @@ -868,3 +413,46 @@ impl GameflowSession { self.queue.as_ref().map(|q| q.id) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_event_type_constants() { + assert_eq!(EVENT_TYPE_GAME_START, "game_start"); + assert_eq!(EVENT_TYPE_LP_CHANGE, "lp_change"); + assert_eq!(EVENT_TYPE_KILL, "kill"); + } + + #[test] + fn test_describe_event_lp_change() { + let raw = serde_json::json!({ + "lpChange": 22, + "lpAfter": 75, + "tier": "GOLD", + "queueType": "RANKED_SOLO_5x5" + }); + let desc = describe_event(EVENT_TYPE_LP_CHANGE, &raw); + assert!(desc.contains("+22")); + assert!(desc.contains("GOLD")); + } + + #[test] + fn test_describe_event_kill() { + let raw = serde_json::json!({ + "KillerName": "Player1", + "VictimName": "Player2" + }); + let desc = describe_event(EVENT_TYPE_KILL, &raw); + assert_eq!(desc, "Player1 killed Player2"); + } + + #[test] + fn test_describe_event_phase_change() { + // Phase change raw_data is just the phase string + let raw = serde_json::json!("InProgress"); + let desc = describe_event(EVENT_TYPE_PHASE_CHANGE, &raw); + assert_eq!(desc, "Phase changed to: InProgress"); + } +} diff --git a/record-daemon/src/lqp/mod.rs b/record-daemon/src/lqp/mod.rs index 5494398..08f14d5 100644 --- a/record-daemon/src/lqp/mod.rs +++ b/record-daemon/src/lqp/mod.rs @@ -20,12 +20,13 @@ pub use endpoints::{ MATCH_HISTORY, RUNE_PAGES, SESSION, SUBSCRIBE_ENDPOINTS, SUMMONER, }; pub use events::{ - ChampSelectStartInfo, ChampionPickInfo, DeathEvent, EventData, GameEndInfo, GameEvent, - GameStartInfo, GameflowSession, InGameStats, ItemBuild, ItemInfo, KillEvent, MatchInfo, - ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity, - QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember, + describe_event, GameflowSession, ItemBuild, ItemInfo, PlayerChampionSelection, + PlayerGameMetadata, PlayerIdentity, QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember, + EVENT_TYPE_CHAMPION_PICK, EVENT_TYPE_CHAMP_SELECT_START, EVENT_TYPE_DEATH, EVENT_TYPE_GAME_END, + EVENT_TYPE_GAME_START, EVENT_TYPE_KILL, EVENT_TYPE_LP_CHANGE, EVENT_TYPE_MATCH_FOUND, + EVENT_TYPE_OBJECTIVE, EVENT_TYPE_PHASE_CHANGE, EVENT_TYPE_STATS_UPDATE, EVENT_TYPE_UNKNOWN, }; pub use state::{ClientState, GameflowPhase}; pub use websocket::{ - parse_event_from_uri, parse_live_client_event, parse_websocket_message, ParsedEvent, + classify_event_from_uri, parse_live_client_event, parse_websocket_message, ParsedEvent, }; diff --git a/record-daemon/src/lqp/websocket.rs b/record-daemon/src/lqp/websocket.rs index e4fbffa..fcb072a 100644 --- a/record-daemon/src/lqp/websocket.rs +++ b/record-daemon/src/lqp/websocket.rs @@ -1,18 +1,23 @@ //! WebSocket event parsing for LQP client. //! //! Handles parsing of WebSocket messages from the League Client -//! and converting them to game events. +//! and classifying them by event type based on URI. +//! All raw JSON data is preserved as-is — no field extraction or remapping. use tracing::{debug, info, warn}; -use super::events::{GameEvent, GameflowSession}; +use super::events::{ + describe_event, EVENT_TYPE_CHAMPION_PICK, EVENT_TYPE_CHAMP_SELECT_START, EVENT_TYPE_DEATH, + EVENT_TYPE_GAME_END, EVENT_TYPE_GAME_START, EVENT_TYPE_KILL, EVENT_TYPE_LP_CHANGE, + EVENT_TYPE_OBJECTIVE, EVENT_TYPE_PHASE_CHANGE, EVENT_TYPE_UNKNOWN, +}; /// Parsed event with raw data preserved. #[derive(Debug, Clone)] pub struct ParsedEvent { - /// The parsed game event. - pub event: GameEvent, - /// The raw JSON data from the API. + /// Event type string derived from URI (e.g. "game_start", "lp_change"). + pub event_type: String, + /// The raw JSON data from the API — stored as-is, no remapping. pub raw_data: serde_json::Value, /// The URI of the endpoint that triggered this event. pub uri: String, @@ -40,41 +45,26 @@ pub fn parse_websocket_message(text: &str) -> Option { let event_data = arr.get(2)?; if callback == "OnJsonApiEvent" { - // Try to parse as RawEvent for type-safe access - if let Ok(raw_event) = - serde_json::from_value::(event_data.clone()) - { - let event_type = event_data - .get("eventType") - .and_then(|t| t.as_str()) - .unwrap_or("Update"); - let raw_data = - serde_json::to_value(raw_event.data.clone()).unwrap_or_default(); - let uri = raw_event.uri.clone(); - if let Some(event) = parse_event_from_uri(&uri, event_type, &raw_data) { - return Some(ParsedEvent { - event, - raw_data, - uri, - }); - } - } - - // Fallback to manual extraction let uri = event_data.get("uri")?.as_str()?.to_string(); let data = event_data.get("data")?.clone(); - let event_type = event_data + let event_action = event_data .get("eventType") .and_then(|t| t.as_str()) .unwrap_or("Update"); - if let Some(event) = parse_event_from_uri(&uri, event_type, &data) { - return Some(ParsedEvent { - event, - raw_data: data, - uri, - }); - } + let event_type = classify_event_from_uri(&uri, event_action, &data); + let description = describe_event(&event_type, &data); + + info!( + "Event classified: type={}, uri={}, desc={}", + event_type, uri, description + ); + + return Some(ParsedEvent { + event_type, + raw_data: data, + uri, + }); } else { debug!("Unknown callback: {}", callback); } @@ -95,211 +85,78 @@ pub fn parse_websocket_message(text: &str) -> Option { None } -/// Parse an event based on the URI. -pub fn parse_event_from_uri( - uri: &str, - event_type: &str, - data: &serde_json::Value, -) -> Option { - info!("Parsing event from URI: {} (type: {})", uri, event_type); - +/// Classify an event based on the URI. +/// +/// Returns an event type string. The raw data is NOT modified — +/// all original API fields are preserved as-is. +pub fn classify_event_from_uri(uri: &str, _event_action: &str, data: &serde_json::Value) -> String { // Handle gameflow phase changes if uri == "/lol-gameflow/v1/gameflow-phase" { - return parse_gameflow_phase_event(data); + return EVENT_TYPE_PHASE_CHANGE.to_string(); } // Handle gameflow session updates if uri == "/lol-gameflow/v1/session" { - return parse_gameflow_session_event(data); + if let Some(phase) = data.get("phase").and_then(|p| p.as_str()) { + if phase == "InProgress" { + return EVENT_TYPE_GAME_START.to_string(); + } + return EVENT_TYPE_PHASE_CHANGE.to_string(); + } + return EVENT_TYPE_UNKNOWN.to_string(); } // Handle game events (kills, deaths, objectives) if uri == "/lol-game-events/v1/game-events" { - info!("Game event received: {:?}", data); - return parse_game_event(data); + return classify_game_event(data); } // Handle ready check if uri == "/lol-matchmaking/v1/ready-check" { - info!("Ready check event: {:?}", data); - return None; + debug!("Ready check event — not recorded"); + return EVENT_TYPE_UNKNOWN.to_string(); } // Handle champion select if uri == "/lol-champ-select/v1/session" { - return parse_champion_select_event(data); + return classify_champion_select_event(data); } - // Handle end-of-game stats block (contains actual game results) + // Handle end-of-game stats block if uri == "/lol-end-of-game/v1/eog-stats-block" { - return parse_end_of_game_stats(data); + return EVENT_TYPE_GAME_END.to_string(); } // Handle LP change notifications if uri == "/lol-ranked/v1/current-lp-change-notification" { - return parse_lp_change_notification(data); + return EVENT_TYPE_LP_CHANGE.to_string(); } // Handle ranked stats updates (with UUID suffix) if uri.starts_with("/lol-ranked/v1/ranked-stats/") { - return parse_ranked_stats_event(data); + return EVENT_TYPE_LP_CHANGE.to_string(); } // Handle lobby if uri.starts_with("/lol-lobby") { debug!("Lobby event: {}", uri); - return None; + return EVENT_TYPE_UNKNOWN.to_string(); } debug!("Unhandled URI: {}", uri); - None + EVENT_TYPE_UNKNOWN.to_string() } -/// Parse gameflow phase change event. -fn parse_gameflow_phase_event(data: &serde_json::Value) -> Option { - let phase = data.as_str()?; - info!("Gameflow phase changed to: {}", phase); - - // Only trigger GameEnd on EndOfGame phase (not WaitingForStats or PreEndOfGame) - // This ensures we wait for the stats to be available - if phase == "EndOfGame" { - info!("Game end phase detected: {}", phase); - // Generate a GameEnd event for timeline recording - return Some( - GameEvent::from_json(&serde_json::json!({ - "eventType": "lcu-game-end", - "gameId": 0, // Will be filled from state if available - "victory": false, // Will be updated from end-of-game stats - "duration": 0.0 - })) - .unwrap_or(GameEvent::Unknown), - ); - } - - // Update internal state based on phase - Some( - GameEvent::from_json(&serde_json::json!({ - "eventType": "lcu-phase-change", - "phase": phase - })) - .unwrap_or(GameEvent::Unknown), - ) -} - -/// Parse gameflow session event. -fn parse_gameflow_session_event(data: &serde_json::Value) -> Option { - if let Some(phase) = data.get("phase").and_then(|p| p.as_str()) { - info!("Gameflow session phase: {}", phase); - - // Check for game start - if phase == "InProgress" { - return parse_game_start_event(data); - } - - return Some( - GameEvent::from_json(&serde_json::json!({ - "eventType": "lcu-phase-change", - "phase": phase - })) - .unwrap_or(GameEvent::Unknown), - ); - } - None -} - -/// Parse game start event from session data. -/// -/// The session data contains all player information. The reading software should -/// use the puuid from RecordingMetadata to find the local player's data in the session. -fn parse_game_start_event(data: &serde_json::Value) -> Option { - info!("Game is now in progress!"); - - // Try to parse the gameData into a GameflowSession struct - let session: Option = data - .get("gameData") - .and_then(|gd| serde_json::from_value(gd.clone()).ok()); - - if let Some(ref session) = session { - debug!( - "Parsed GameflowSession: game_id={}, queue={:?}", - session.game_id, - session.queue_name() - ); - } else { - debug!("Failed to parse gameData as GameflowSession, falling back to manual extraction"); - } - - // Extract game_id - prefer from parsed session, fallback to manual extraction - let game_id = session.as_ref().map(|s| s.game_id).unwrap_or_else(|| { - data.get("gameData") - .and_then(|gd| gd.get("gameId")) - .and_then(|id| id.as_u64()) - .unwrap_or(0) - }); - - // Extract queue info (this is the same for all players) - let queue_type = session - .as_ref() - .and_then(|s| s.queue_name()) - .map(|s| s.to_string()) - .or_else(|| { - data.get("gameData") - .and_then(|gd| gd.get("queue")) - .and_then(|q| q.get("name")) - .and_then(|n| n.as_str()) - .map(|s| s.to_string()) - }); - - let queue_id = session.as_ref().and_then(|s| s.queue_id()); - - let game_mode = session - .as_ref() - .and_then(|s| s.game_mode()) - .map(|s| s.to_string()) - .or_else(|| { - data.get("gameData") - .and_then(|gd| gd.get("queue")) - .and_then(|q| q.get("gameMode")) - .and_then(|m| m.as_str()) - .map(|s| s.to_string()) - }); - - info!( - "Extracted game metadata: game_id={}, queue={:?}, queue_id={:?}, mode={:?}", - game_id, queue_type, queue_id, game_mode - ); - - // Note: Player-specific data (champion, team, summoner_name) is NOT extracted here. - // The full session is stored in the event, and the reading software should use - // the puuid from RecordingMetadata to find the local player's data in the session. - // This allows the same recording to be analyzed for any player by their puuid. - - Some( - GameEvent::from_json(&serde_json::json!({ - "eventType": "lcu-game-start", - "gameId": game_id, - "queueType": queue_type, - "queueId": queue_id, - "gameMode": game_mode, - "session": session - })) - .unwrap_or(GameEvent::Unknown), - ) -} - -/// Parse champion select event. -fn parse_champion_select_event(data: &serde_json::Value) -> Option { - info!("Champion select event: {:?}", data); - - // Check if we're in champion select phase +/// Classify a champion select event based on the data. +fn classify_champion_select_event(data: &serde_json::Value) -> String { + // Check if we're in a pick phase and a champion has been selected if let Some(timers) = data.get("timers") { if let Some(phase) = timers.get("phase").and_then(|p| p.as_str()) { if phase == "BAN_PICK" || phase == "FINALIZATION" { - // Extract local player's champion + // Check if local player has picked a champion if let Some(local_player_cell_id) = data.get("localPlayerCellId").and_then(|id| id.as_i64()) { - // Check both teams for the local player for team_key in &["myTeam", "theirTeam"] { if let Some(team) = data.get(team_key).and_then(|t| t.as_array()) { for member in team { @@ -310,22 +167,7 @@ fn parse_champion_select_event(data: &serde_json::Value) -> Option { member.get("championId").and_then(|id| id.as_u64()) { if champion_id > 0 { - let champion_name = member - .get("championName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown") - .to_string(); - - return Some( - GameEvent::from_json(&serde_json::json!({ - "eventType": "lcu-champion-pick", - "summonerName": "LocalPlayer", - "championId": champion_id, - "championName": champion_name, - "isLocalPlayer": true - })) - .unwrap_or(GameEvent::Unknown), - ); + return EVENT_TYPE_CHAMPION_PICK.to_string(); } } } @@ -336,496 +178,27 @@ fn parse_champion_select_event(data: &serde_json::Value) -> Option { } } } - None + EVENT_TYPE_CHAMP_SELECT_START.to_string() } -/// Parse end-of-game stats. -fn parse_end_of_game_stats(data: &serde_json::Value) -> Option { - info!("End-of-game stats received: {:?}", data); - - // Extract game ID and duration - let game_id = data.get("gameId").and_then(|id| id.as_u64()).unwrap_or(0); - let game_duration = data - .get("gameLength") - .and_then(|d| d.as_f64()) - .unwrap_or(0.0); - - // Get local player data - prefer localPlayer field, fallback to teams[0].players[0] - let local_player = data.get("localPlayer"); - - // Extract victory status from local player's stats (WIN: 1 means victory) - let victory = local_player - .and_then(|p| p.get("stats")) - .and_then(|s| s.get("WIN")) - .and_then(|w| w.as_u64()) - .map(|w| w == 1) - .or_else(|| { - // Fallback: check if player's team is winning team - data.get("teams") - .and_then(|teams| teams.as_array()) - .and_then(|t| { - t.iter().find_map(|team| { - if team.get("isPlayerTeam").and_then(|p| p.as_bool()) == Some(true) { - team.get("isWinningTeam").and_then(|w| w.as_bool()) - } else { - None - } - }) - }) - }) - .unwrap_or(false); - - // Extract player stats - stats use UPPERCASE keys - let mut kills = 0u32; - let mut deaths = 0u32; - let mut assists = 0u32; - let mut creep_score = 0u32; - let mut gold_earned = 0u32; - let mut damage_dealt = 0u64; - let mut damage_taken = 0u64; - let mut vision_score = 0.0; - - if let Some(stats_obj) = local_player.and_then(|p| p.get("stats")) { - kills = stats_obj - .get("CHAMPIONS_KILLED") - .and_then(|k| k.as_u64()) - .unwrap_or(0) as u32; - deaths = stats_obj - .get("NUM_DEATHS") - .and_then(|d| d.as_u64()) - .unwrap_or(0) as u32; - assists = stats_obj - .get("ASSISTS") - .and_then(|a| a.as_u64()) - .unwrap_or(0) as u32; - creep_score = stats_obj - .get("MINIONS_KILLED") - .and_then(|cs| cs.as_u64()) - .unwrap_or(0) as u32; - gold_earned = stats_obj - .get("GOLD_EARNED") - .and_then(|g| g.as_u64()) - .unwrap_or(0) as u32; - damage_dealt = stats_obj - .get("TOTAL_DAMAGE_DEALT_TO_CHAMPIONS") - .and_then(|d| d.as_u64()) - .unwrap_or(0); - damage_taken = stats_obj - .get("TOTAL_DAMAGE_TAKEN") - .and_then(|d| d.as_u64()) - .unwrap_or(0); - vision_score = stats_obj - .get("VISION_SCORE") - .and_then(|v| v.as_f64()) - .unwrap_or(0.0); - } - - info!("Extracted game end stats: kills={}, deaths={}, assists={}, cs={}, gold={}, damage_dealt={}, damage_taken={}, vision={}, victory={}", - kills, deaths, assists, creep_score, gold_earned, damage_dealt, damage_taken, vision_score, victory); - - // Generate a GameEnd event with actual stats - // Note: PlayerStats uses camelCase due to serde rename_all - let event_json = serde_json::json!({ - "eventType": "lcu-game-end", - "gameId": game_id, - "victory": victory, - "duration": game_duration, - "stats": { - "kills": kills, - "deaths": deaths, - "assists": assists, - "minionsKilled": creep_score, - "goldEarned": gold_earned, - "damageDealt": damage_dealt, - "damageTaken": damage_taken, - "visionScore": vision_score - } - }); - info!("Generating GameEnd event from eog-stats: {:?}", event_json); - - match GameEvent::from_json(&event_json) { - Some(event) => { - info!("Successfully parsed GameEnd event"); - Some(event) - } - None => { - warn!("Failed to parse GameEnd event, returning Unknown"); - Some(GameEvent::Unknown) - } - } -} - -/// Parse LP change notification event. -/// -/// This is the primary source for LP changes after a ranked game. -/// The notification contains the LP delta and current rank info. -fn parse_lp_change_notification(data: &serde_json::Value) -> Option { - info!("LP change notification received: {:?}", data); - - // Extract queue type - let queue_type = data - .get("queueType") - .and_then(|q| q.as_str()) - .unwrap_or("UNKNOWN") - .to_string(); - - // Extract LP change amount - let lp_change = data.get("lpChange").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32; - - // Extract LP before and after - let lp_before = data.get("lpBefore").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32; - - let lp_after = data.get("lpAfter").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32; - - // Extract tier and division - let tier = data - .get("tier") - .and_then(|t| t.as_str()) - .unwrap_or("UNRANKED") - .to_string(); - - let division = data - .get("division") - .and_then(|d| d.as_str()) - .map(|s| s.to_string()); - - // Check for promotional series - let in_promos = data.get("miniSeries").is_some(); - - let promo_progress = if in_promos { - data.get("miniSeries") - .and_then(|ms| ms.get("progress")) - .and_then(|p| p.as_str()) - .map(|s| s.to_string()) - } else { - None - }; - - let promo_wins = data - .get("miniSeries") - .and_then(|ms| ms.get("wins")) - .and_then(|w| w.as_u64()) - .map(|w| w as u32); - - let promo_losses = data - .get("miniSeries") - .and_then(|ms| ms.get("losses")) - .and_then(|l| l.as_u64()) - .map(|l| l as u32); - - // Extract total games stats if available - let total_wins = data.get("wins").and_then(|w| w.as_u64()).unwrap_or(0) as u32; - - let total_losses = data.get("losses").and_then(|l| l.as_u64()).unwrap_or(0) as u32; - - let total_games = total_wins + total_losses; - - info!( - "LP change notification: {} {} LP change: {} ({} -> {})", - queue_type, tier, lp_change, lp_before, lp_after - ); - - let event_json = serde_json::json!({ - "eventType": "lcu-lp-change", - "queueType": queue_type, - "lpChange": lp_change, - "lpBefore": lp_before, - "lpAfter": lp_after, - "tier": tier, - "division": division, - "leaguePoints": lp_after, - "inPromos": in_promos, - "promoProgress": promo_progress, - "promoWins": promo_wins, - "promoLosses": promo_losses, - "totalGames": total_games, - "totalWins": total_wins, - "totalLosses": total_losses - }); - - GameEvent::from_json(&event_json) -} - -/// Parse ranked stats event for LP changes. -/// -/// The ranked stats endpoint provides updates when LP changes occur. -/// We extract the queue-specific data and emit an LpChange event. -fn parse_ranked_stats_event(data: &serde_json::Value) -> Option { - info!("Ranked stats event received: {:?}", data); - - // The ranked stats data contains queue-specific stats - // We look for RANKED_SOLO_5x5 and RANKED_FLEX_SR queues - let queues = data.get("queues")?.as_array()?; - - for queue_data in queues { - let queue_type = queue_data - .get("queueType") - .and_then(|q| q.as_str()) - .unwrap_or(""); - - // Only process ranked queues - if queue_type != "RANKED_SOLO_5x5" && queue_type != "RANKED_FLEX_SR" { - continue; - } - - // Extract tier and division - let tier = queue_data - .get("tier") - .and_then(|t| t.as_str()) - .unwrap_or("UNRANKED") - .to_string(); - - let division = queue_data - .get("division") - .and_then(|d| d.as_str()) - .map(|s| s.to_string()); - - // Extract LP - let league_points = queue_data - .get("leaguePoints") - .and_then(|lp| lp.as_i64()) - .unwrap_or(0) as i32; - - // Extract previous LP if available (for calculating change) - let previous_lp = queue_data - .get("previousLeaguePoints") - .and_then(|lp| lp.as_i64()) - .unwrap_or(league_points as i64) as i32; - - // Calculate LP change - let lp_change = league_points - previous_lp; - - // Only emit event if there was an actual change - if lp_change == 0 { - continue; - } - - // Check for promotional series - let in_promos = queue_data.get("miniSeries").is_some(); - - let promo_progress = if in_promos { - queue_data - .get("miniSeries") - .and_then(|ms| ms.get("progress")) - .and_then(|p| p.as_str()) - .map(|s| s.to_string()) - } else { - None - }; - - let promo_wins = queue_data - .get("miniSeries") - .and_then(|ms| ms.get("wins")) - .and_then(|w| w.as_u64()) - .map(|w| w as u32); - - let promo_losses = queue_data - .get("miniSeries") - .and_then(|ms| ms.get("losses")) - .and_then(|l| l.as_u64()) - .map(|l| l as u32); - - // Extract total games stats - let total_wins = queue_data.get("wins").and_then(|w| w.as_u64()).unwrap_or(0) as u32; - - let total_losses = queue_data - .get("losses") - .and_then(|l| l.as_u64()) - .unwrap_or(0) as u32; - - let total_games = total_wins + total_losses; - - info!( - "LP change detected: {} {} LP change: {} ({} -> {})", - queue_type, tier, lp_change, previous_lp, league_points - ); - - let event_json = serde_json::json!({ - "eventType": "lcu-lp-change", - "queueType": queue_type, - "lpChange": lp_change, - "lpBefore": previous_lp, - "lpAfter": league_points, - "tier": tier, - "division": division, - "leaguePoints": league_points, - "inPromos": in_promos, - "promoProgress": promo_progress, - "promoWins": promo_wins, - "promoLosses": promo_losses, - "totalGames": total_games, - "totalWins": total_wins, - "totalLosses": total_losses - }); - - return GameEvent::from_json(&event_json); - } - - None -} - -/// Parse game events from the /lol-game-events/v1/game-events endpoint. -/// -/// This endpoint receives events like kills, deaths, and objectives. -/// The format varies but typically includes an EventName field. -fn parse_game_event(data: &serde_json::Value) -> Option { - // The game events API can send various event types - // Common event names: ChampionKill, ChampionDeath, DragonKill, BaronKill, etc. - let event_name = data.get("EventName").and_then(|n| n.as_str()).unwrap_or(""); - - info!("Parsing game event: {} -> {:?}", event_name, data); +/// Classify an in-game event based on EventName field. +fn classify_game_event(data: &serde_json::Value) -> String { + let event_name = data + .get("EventName") + .or_else(|| data.get("eventName")) + .and_then(|n| n.as_str()) + .unwrap_or(""); match event_name { - "ChampionKill" => { - // Extract kill information - let killer = data - .get("KillerName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown") - .to_string(); - - let victim = data - .get("VictimName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown") - .to_string(); - - let killer_champion = data - .get("KillerChampionName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - let victim_champion = data - .get("VictimChampionName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - // Check if it was a solo kill (no assisters) - let assisters = data - .get("Assisters") - .and_then(|a| a.as_array()) - .map(|arr| arr.len() as u32) - .unwrap_or(0); - - let solo_kill = assisters == 0; - - // Extract position if available - let position = data.get("Position").map(|p| super::events::Position { - x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32, - y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32, - }); - - // Get game time - let game_time = data.get("GameTime").and_then(|t| t.as_f64()); - - let event_json = serde_json::json!({ - "eventType": "lcu-kill", - "killer": killer, - "killerChampion": killer_champion, - "victim": victim, - "victimChampion": victim_champion, - "soloKill": solo_kill, - "assists": assisters, - "position": position, - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) - } - "ChampionDeath" => { - // Extract death information - let killer = data - .get("KillerName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - let killer_champion = data - .get("KillerChampionName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - // Death cause - could be champion, minion, tower, etc. - let cause = killer - .clone() - .or_else(|| { - data.get("DeathCause") - .and_then(|c| c.as_str()) - .map(|s| s.to_string()) - }) - .unwrap_or_else(|| "Unknown".to_string()); - - // Extract position if available - let position = data.get("Position").map(|p| super::events::Position { - x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32, - y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32, - }); - - // Get game time - let game_time = data.get("GameTime").and_then(|t| t.as_f64()); - - let event_json = serde_json::json!({ - "eventType": "lcu-death", - "killer": killer, - "killerChampion": killer_champion, - "cause": cause, - "position": position, - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) - } + "ChampionKill" => EVENT_TYPE_KILL.to_string(), + "ChampionDeath" => EVENT_TYPE_DEATH.to_string(), "DragonKill" | "BaronKill" | "HeraldKill" | "RiftHeraldKill" | "ElderDragonKill" => { - let objective_type = match event_name { - "DragonKill" => "dragon", - "BaronKill" => "baron", - "HeraldKill" | "RiftHeraldKill" => "herald", - "ElderDragonKill" => "elderdragon", - _ => "unknown", - }; - - let team = data.get("Team").and_then(|t| t.as_u64()).unwrap_or(0) as u32; - - let game_time = data.get("GameTime").and_then(|t| t.as_f64()); - - let event_json = serde_json::json!({ - "eventType": "lcu-objective", - "objectiveType": objective_type, - "team": team, - "participated": false, // Will be determined by the caller - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) - } - "TurretKill" | "InhibitorKill" | "NexusKill" => { - let objective_type = match event_name { - "TurretKill" => "tower", - "InhibitorKill" => "inhibitor", - "NexusKill" => "nexus", - _ => "unknown", - }; - - let team = data.get("Team").and_then(|t| t.as_u64()).unwrap_or(0) as u32; - - let game_time = data.get("GameTime").and_then(|t| t.as_f64()); - - let event_json = serde_json::json!({ - "eventType": "lcu-objective", - "objectiveType": objective_type, - "team": team, - "participated": false, - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) + EVENT_TYPE_OBJECTIVE.to_string() } + "TurretKill" | "InhibitorKill" | "NexusKill" => EVENT_TYPE_OBJECTIVE.to_string(), _ => { - // Try to parse as a generic event with eventType field - debug!( - "Unknown game event type: {}, attempting generic parse", - event_name - ); - GameEvent::from_json(data) + debug!("Unknown game event type: {}", event_name); + EVENT_TYPE_UNKNOWN.to_string() } } } @@ -852,225 +225,30 @@ fn parse_game_event(data: &serde_json::Value) -> Option { pub fn parse_live_client_event(data: &serde_json::Value) -> Option { let event_name = data.get("EventName").and_then(|n| n.as_str()).unwrap_or(""); - info!("Parsing live client event: {} -> {:?}", event_name, data); - - let event = match event_name { - "ChampionKill" => { - // Extract kill information - let killer = data - .get("KillerName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown") - .to_string(); - - let victim = data - .get("VictimName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown") - .to_string(); - - // Get champion names if available - let killer_champion = data - .get("KillerChampionName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - let victim_champion = data - .get("VictimChampionName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - // Check if it was a solo kill (no assisters) - let assisters = data - .get("Assisters") - .and_then(|a| a.as_array()) - .map(|arr| arr.len() as u32) - .unwrap_or(0); - - let solo_kill = assisters == 0; - - // Get game time - let game_time = data.get("EventTime").and_then(|t| t.as_f64()); - - // Extract position if available - let position = data.get("Position").map(|p| super::events::Position { - x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32, - y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32, - }); - - let event_json = serde_json::json!({ - "eventType": "lcu-kill", - "killer": killer, - "killerChampion": killer_champion, - "victim": victim, - "victimChampion": victim_champion, - "soloKill": solo_kill, - "assists": assisters, - "position": position, - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) - } - "ChampionDeath" => { - // Extract death information - let killer = data - .get("KillerName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - let killer_champion = data - .get("KillerChampionName") - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - // Death cause - let cause = killer - .clone() - .or_else(|| { - data.get("DeathCause") - .and_then(|c| c.as_str()) - .map(|s| s.to_string()) - }) - .unwrap_or_else(|| "Unknown".to_string()); - - // Get game time - let game_time = data.get("EventTime").and_then(|t| t.as_f64()); - - // Extract position if available - let position = data.get("Position").map(|p| super::events::Position { - x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32, - y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32, - }); - - let event_json = serde_json::json!({ - "eventType": "lcu-death", - "killer": killer, - "killerChampion": killer_champion, - "cause": cause, - "position": position, - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) - } + let event_type = match event_name { + "ChampionKill" => EVENT_TYPE_KILL.to_string(), + "ChampionDeath" => EVENT_TYPE_DEATH.to_string(), "DragonKill" | "BaronKill" | "HeraldKill" | "RiftHeraldKill" | "ElderDragonKill" => { - let objective_type = match event_name { - "DragonKill" => "dragon", - "BaronKill" => "baron", - "HeraldKill" | "RiftHeraldKill" => "herald", - "ElderDragonKill" => "elderdragon", - _ => "unknown", - }; - - let _killer = data - .get("KillerName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown"); - - // Determine team based on killer (would need player list to determine team) - let team = data.get("KillerTeam").and_then(|t| t.as_u64()).unwrap_or(0) as u32; - - let game_time = data.get("EventTime").and_then(|t| t.as_f64()); - - let event_json = serde_json::json!({ - "eventType": "lcu-objective", - "objectiveType": objective_type, - "team": team, - "participated": false, - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) + EVENT_TYPE_OBJECTIVE.to_string() } - "TurretKill" | "InhibitorKill" | "NexusKill" => { - let objective_type = match event_name { - "TurretKill" => "tower", - "InhibitorKill" => "inhibitor", - "NexusKill" => "nexus", - _ => "unknown", - }; - - let team = data.get("KillerTeam").and_then(|t| t.as_u64()).unwrap_or(0) as u32; - - let game_time = data.get("EventTime").and_then(|t| t.as_f64()); - - let event_json = serde_json::json!({ - "eventType": "lcu-objective", - "objectiveType": objective_type, - "team": team, - "participated": false, - "gameTime": game_time - }); - - GameEvent::from_json(&event_json) - } - "Multikill" => { - // Multikill events (double, triple, quadra, penta kills) - let killer = data - .get("KillerName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown"); - - let kill_count = data.get("KillCount").and_then(|k| k.as_u64()).unwrap_or(2) as u32; - - let game_time = data.get("EventTime").and_then(|t| t.as_f64()); - - info!( - "Multikill event: {} got a {}-kill at {:?}", - killer, kill_count, game_time - ); - - // Don't emit a separate event for multikills, they're derived from kills - None - } - "FirstBlood" => { - let killer = data - .get("KillerName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown"); - - let victim = data - .get("VictimName") - .and_then(|n| n.as_str()) - .unwrap_or("Unknown"); - - let game_time = data.get("EventTime").and_then(|t| t.as_f64()); - - info!( - "First Blood: {} killed {} at {:?}", - killer, victim, game_time - ); - - // First blood is just a special kill, the kill event will be emitted separately - None - } - "GameStart" => { - let game_time = data - .get("EventTime") - .and_then(|t| t.as_f64()) - .unwrap_or(0.0); - - info!("Game started at {:?}", game_time); - None - } - "GameEnd" => { - let game_time = data - .get("EventTime") - .and_then(|t| t.as_f64()) - .unwrap_or(0.0); - - info!("Game ended at {:?}", game_time); - None + "TurretKill" | "InhibitorKill" | "NexusKill" => EVENT_TYPE_OBJECTIVE.to_string(), + "Multikill" | "FirstBlood" | "GameStart" | "GameEnd" => { + // These are derived/special events — the kill/death events cover them + return None; } _ => { debug!("Unknown live client event type: {}", event_name); - None + return None; } }; - event.map(|e| ParsedEvent { - event: e, + info!( + "Live client event classified: type={}, name={}", + event_type, event_name + ); + + Some(ParsedEvent { + event_type, raw_data: data.clone(), uri: "/liveclientdata/eventdata".to_string(), }) @@ -1091,4 +269,58 @@ mod tests { let result = parse_websocket_message("[]"); assert!(result.is_none()); } + + #[test] + fn test_classify_game_event_kill() { + let data = serde_json::json!({ + "EventName": "ChampionKill", + "KillerName": "Player1", + "VictimName": "Player2" + }); + assert_eq!(classify_game_event(&data), EVENT_TYPE_KILL); + } + + #[test] + fn test_classify_game_event_objective() { + let data = serde_json::json!({ + "EventName": "DragonKill" + }); + assert_eq!(classify_game_event(&data), EVENT_TYPE_OBJECTIVE); + } + + #[test] + fn test_classify_event_from_uri_lp_change() { + let data = serde_json::json!({"lpChange": 22, "tier": "GOLD"}); + assert_eq!( + classify_event_from_uri( + "/lol-ranked/v1/current-lp-change-notification", + "Update", + &data + ), + EVENT_TYPE_LP_CHANGE + ); + } + + #[test] + fn test_classify_event_from_uri_game_end() { + let data = serde_json::json!({"gameId": 123}); + assert_eq!( + classify_event_from_uri("/lol-end-of-game/v1/eog-stats-block", "Update", &data), + EVENT_TYPE_GAME_END + ); + } + + #[test] + fn test_parse_live_client_event_kill() { + let data = serde_json::json!({ + "EventName": "ChampionKill", + "KillerName": "Player1", + "VictimName": "Player2", + "Assisters": [] + }); + let result = parse_live_client_event(&data).unwrap(); + assert_eq!(result.event_type, EVENT_TYPE_KILL); + // Raw data is preserved as-is + assert_eq!(result.raw_data["KillerName"], "Player1"); + } } diff --git a/record-daemon/src/main.rs b/record-daemon/src/main.rs index 793979a..d8e5c7f 100644 --- a/record-daemon/src/main.rs +++ b/record-daemon/src/main.rs @@ -13,7 +13,10 @@ use record_daemon::{ config::{self, Settings}, error::Result, ipc::{self, IpcHandlers, IpcServer, IpcServerConfig}, - lqp::{GameEvent, LockfileWatcher, LqpClient}, + lqp::{ + describe_event, LockfileWatcher, LqpClient, EVENT_TYPE_CHAMPION_PICK, + EVENT_TYPE_GAME_START, EVENT_TYPE_PHASE_CHANGE, + }, recording::RecordingEngine, state::{DaemonStateMachine, DaemonStatus, StateTransition}, timeline::{EventMapper, TimelineStore, TimestampedEvent}, @@ -226,25 +229,44 @@ impl Daemon { /// Handle a game event. async fn handle_game_event(&self, parsed: record_daemon::lqp::ParsedEvent) -> Result<()> { - let event = &parsed.event; - info!("[EVENT_HANDLER] Game event received: {:?}", event); + let event_type = &parsed.event_type; + let raw_data = &parsed.raw_data; + let description = describe_event(event_type, raw_data); + + info!( + "[EVENT_HANDLER] Game event received: type={}, desc={}", + event_type, description + ); // Handle pre-game data collection - match event { - GameEvent::PhaseChange(info) if info.phase == "ChampSelect" => { - info!("[EVENT_HANDLER] Champion select started"); + match event_type.as_str() { + EVENT_TYPE_PHASE_CHANGE => { + let phase = raw_data + .as_str() + .or_else(|| raw_data.get("phase").and_then(|v| v.as_str())) + .unwrap_or(""); + if phase == "ChampSelect" { + info!("[EVENT_HANDLER] Champion select started"); + } } - GameEvent::ChampionPick(pick) if pick.is_local_player => { - info!( - "[EVENT_HANDLER] Local player picked champion: {}", - pick.champion_name - ); + EVENT_TYPE_CHAMPION_PICK => { + let is_local = raw_data + .get("isLocalPlayer") + .or_else(|| raw_data.get("is_local_player")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if is_local { + let champion = raw_data + .get("championName") + .or_else(|| raw_data.get("champion_name")) + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + info!("[EVENT_HANDLER] Local player picked champion: {}", champion); + } } - GameEvent::GameStart(info) => { - info!( - "[EVENT_HANDLER] Game started with metadata: queue={:?}, mode={:?}, map={:?}", - info.queue_type, info.game_mode, info.map_name - ); + EVENT_TYPE_GAME_START => { + let game_id = raw_data.get("gameId").and_then(|v| v.as_u64()).unwrap_or(0); + info!("[EVENT_HANDLER] Game started with game_id: {}", game_id); } _ => {} } @@ -252,7 +274,7 @@ impl Daemon { // Record event to timeline if recording (BEFORE state transition for GameEnd) // This ensures GameEnd events are recorded while still in recording state if self.state_machine.is_recording() { - if let Some((video_ts, game_ts)) = self.event_mapper.write().handle_event(event) { + if let Some((video_ts, game_ts)) = self.event_mapper.write().handle_event(event_type) { // Get the current recording ID if let Some(recording_id) = *self.current_recording_id.read() { // Create a timestamped event with raw data @@ -260,11 +282,10 @@ impl Daemon { video_timestamp: video_ts, game_timestamp: game_ts, timestamp: chrono::Utc::now(), - event_type: event.event_type_name().to_string(), - description: event.description(), - event: event.clone(), - raw_data: Some(parsed.raw_data.clone()), - uri: Some(parsed.uri.clone()), + event_type: event_type.clone(), + description: description.clone(), + raw_data: parsed.raw_data.clone(), + uri: parsed.uri.clone(), }; // Add the event to the timeline store @@ -277,9 +298,7 @@ impl Daemon { } else { debug!( "Event added to timeline: video_ts={:?}, game_ts={:?}, type={}", - video_ts, - game_ts, - event.event_type_name() + video_ts, game_ts, event_type ); } } else { @@ -289,7 +308,7 @@ impl Daemon { } // Process state transitions - if let Some(transition) = self.state_machine.process_event(event) { + if let Some(transition) = self.state_machine.process_event(event_type, raw_data) { info!("[EVENT_HANDLER] State transition: {:?}", transition); // Only process the transition if it's valid @@ -297,12 +316,8 @@ impl Daemon { // Handle recording start/stop match transition { StateTransition::GameStarted => { - // Extract game_id from the GameStart event - let game_id = if let GameEvent::GameStart(ref info) = event { - info.game_id - } else { - 0 - }; + // Extract game_id from raw_data + let game_id = raw_data.get("gameId").and_then(|v| v.as_u64()).unwrap_or(0); info!( "[EVENT_HANDLER] GameStarted transition - game_id: {}", diff --git a/record-daemon/src/state/machine.rs b/record-daemon/src/state/machine.rs index c00683e..2af1d81 100644 --- a/record-daemon/src/state/machine.rs +++ b/record-daemon/src/state/machine.rs @@ -9,7 +9,7 @@ use parking_lot::RwLock; use tracing::{info, trace, warn}; use super::DaemonStatus; -use crate::lqp::{GameEvent, GameflowPhase}; +use crate::lqp::GameflowPhase; /// Internal daemon state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -203,22 +203,29 @@ impl DaemonStateMachine { /// Process a game event and potentially trigger a transition. /// - /// Only returns state transitions - /// All data is stored in events in the timeline and processed at game end. - pub fn process_event(&self, event: &GameEvent) -> Option { + /// Uses event_type string and raw_data instead of typed GameEvent. + /// Only returns state transitions — all data is stored in raw events. + pub fn process_event( + &self, + event_type: &str, + raw_data: &serde_json::Value, + ) -> Option { trace!( - "Processing event in state {:?}: {:?}", + "Processing event in state {:?}: type={}", self.current_state(), - event + event_type ); - match event { - GameEvent::GameStart(_) => Some(StateTransition::GameStarted), - GameEvent::GameEnd(_) => Some(StateTransition::GameEnded), - GameEvent::PhaseChange(info) => { + match event_type { + crate::lqp::EVENT_TYPE_GAME_START => Some(StateTransition::GameStarted), + crate::lqp::EVENT_TYPE_GAME_END => Some(StateTransition::GameEnded), + crate::lqp::EVENT_TYPE_PHASE_CHANGE => { // Only trigger GameEnded on EndOfGame phase (stats are available by then) - // The actual GameEnd event with stats comes from /lol-end-of-game/v1/eog-stats-block - if info.phase == "EndOfGame" && self.is_recording() { + let phase = raw_data + .as_str() + .or_else(|| raw_data.get("phase").and_then(|v| v.as_str())) + .unwrap_or(""); + if phase == "EndOfGame" && self.is_recording() { Some(StateTransition::GameEnded) } else { None @@ -294,4 +301,26 @@ mod tests { assert_eq!(machine.current_state(), DaemonState::Idle); assert_eq!(machine.last_error(), None); } + + #[test] + fn test_process_event_game_start() { + let machine = DaemonStateMachine::new(); + machine.transition(StateTransition::ClientStarted); + + let raw = serde_json::json!({"gameId": 12345}); + let transition = machine.process_event(crate::lqp::EVENT_TYPE_GAME_START, &raw); + assert!(matches!(transition, Some(StateTransition::GameStarted))); + } + + #[test] + fn test_process_event_phase_change_end_of_game() { + let machine = DaemonStateMachine::new(); + machine.transition(StateTransition::ClientStarted); + machine.transition(StateTransition::GameStarted); + + // Phase change to EndOfGame while recording should trigger GameEnded + let raw = serde_json::json!("EndOfGame"); + let transition = machine.process_event(crate::lqp::EVENT_TYPE_PHASE_CHANGE, &raw); + assert!(matches!(transition, Some(StateTransition::GameEnded))); + } } diff --git a/record-daemon/src/timeline/mapper.rs b/record-daemon/src/timeline/mapper.rs index 09ff907..7671f87 100644 --- a/record-daemon/src/timeline/mapper.rs +++ b/record-daemon/src/timeline/mapper.rs @@ -3,8 +3,6 @@ use chrono::{DateTime, Duration, Utc}; use tracing::debug; -use crate::lqp::GameEvent; - /// Event mapper that tracks recording start time and maps events to video timestamps. pub struct EventMapper { /// Recording start time. @@ -45,8 +43,8 @@ impl EventMapper { self.start_time.is_some() } - /// Map a game event to video and game timestamps. - pub fn map_event(&self, _event: &GameEvent) -> Option<(Duration, Option)> { + /// Map an event to video and game timestamps. + pub fn map_event(&self) -> Option<(Duration, Option)> { let start_time = self.start_time?; let now = Utc::now(); @@ -60,25 +58,23 @@ impl EventMapper { self.synchronizer.adjust_game_timestamp(raw_ts) }); - // Update game start time if this is a game start event - // (handled separately in handle_event) - Some((video_timestamp, game_timestamp)) } /// Handle a game event and return mapped timestamps. - pub fn handle_event(&mut self, event: &GameEvent) -> Option<(Duration, Option)> { + /// event_type is used to detect game start for timestamp tracking. + pub fn handle_event(&mut self, event_type: &str) -> Option<(Duration, Option)> { if !self.is_active() { return None; } // Track game start time - if let GameEvent::GameStart(_) = event { + if event_type == crate::lqp::EVENT_TYPE_GAME_START { self.game_start_time = Some(Utc::now()); debug!("Game start time recorded: {:?}", self.game_start_time); } - let result = self.map_event(event); + let result = self.map_event(); // Add sync point if we have both timestamps if let Some((video_ts, Some(game_ts))) = result { diff --git a/record-daemon/src/timeline/mod.rs b/record-daemon/src/timeline/mod.rs index af1714f..74d6f86 100644 --- a/record-daemon/src/timeline/mod.rs +++ b/record-daemon/src/timeline/mod.rs @@ -10,8 +10,6 @@ use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::lqp::GameEvent; - /// A timeline of events for a recording. /// Stores raw API responses for maximum flexibility and future-proofing. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -79,31 +77,11 @@ impl Timeline { } } - /// Add an event to the timeline. + /// Add an event to the timeline with raw data. pub fn add_event( &mut self, - event: GameEvent, - video_timestamp: Duration, - game_timestamp: Option, - ) { - let timestamped = TimestampedEvent { - video_timestamp, - game_timestamp, - timestamp: Utc::now(), - event_type: event_type_name(&event), - description: event.description(), - event, - raw_data: None, - uri: None, - }; - - self.events.push(timestamped); - } - - /// Add an event to the timeline with raw data. - pub fn add_event_with_raw( - &mut self, - event: GameEvent, + event_type: &str, + description: &str, video_timestamp: Duration, game_timestamp: Option, raw_data: serde_json::Value, @@ -113,11 +91,10 @@ impl Timeline { video_timestamp, game_timestamp, timestamp: Utc::now(), - event_type: event_type_name(&event), - description: event.description(), - event, - raw_data: Some(raw_data), - uri: Some(uri), + event_type: event_type.to_string(), + description: description.to_string(), + raw_data, + uri, }; self.events.push(timestamped); @@ -192,29 +169,9 @@ fn format_timestamp(duration: Duration) -> String { format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis) } -/// Get the event type name. -fn event_type_name(event: &GameEvent) -> String { - match event { - GameEvent::MatchFound(_) => "match_found", - GameEvent::ChampSelectStart(_) => "champ_select_start", - GameEvent::ChampionPick(_) => "champion_pick", - GameEvent::GameStart(_) => "game_start", - GameEvent::Kill(_) => "kill", - GameEvent::Death(_) => "death", - GameEvent::Objective(_) => "objective", - GameEvent::StatsUpdate(_) => "stats_update", - GameEvent::GameEnd(_) => "game_end", - GameEvent::PhaseChange(_) => "phase_change", - GameEvent::LpChange(_) => "lp_change", - GameEvent::Unknown => "unknown", - } - .to_string() -} - #[cfg(test)] mod tests { use super::*; - use crate::lqp::KillEvent; #[test] fn test_timeline_creation() { @@ -230,21 +187,22 @@ mod tests { let id = Uuid::new_v4(); let mut timeline = Timeline::new(id); - let event = GameEvent::Kill(KillEvent { - killer: "Player1".to_string(), - killer_champion: Some("Ahri".to_string()), - victim: "Player2".to_string(), - victim_champion: Some("Lux".to_string()), - solo_kill: true, - assists: 0, - position: None, - game_time: Some(120.0), - timestamp: Utc::now(), + let raw_data = serde_json::json!({ + "killer": "Player1", + "victim": "Player2" }); - timeline.add_event(event, Duration::seconds(5), Some(Duration::seconds(120))); + timeline.add_event( + "kill", + "Player1 killed Player2", + Duration::seconds(5), + Some(Duration::seconds(120)), + raw_data, + "/lol-game-events/v1/events".to_string(), + ); assert_eq!(timeline.event_count(), 1); + assert_eq!(timeline.events[0].event_type, "kill"); } #[test] diff --git a/record-daemon/src/timeline/store.rs b/record-daemon/src/timeline/store.rs index 1b77b03..7a456f4 100644 --- a/record-daemon/src/timeline/store.rs +++ b/record-daemon/src/timeline/store.rs @@ -9,32 +9,30 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::error::{Result, TimelineError}; -use crate::lqp::GameEvent; use crate::recording::RecordingResult; /// A timestamped event in the timeline. +/// Stores raw API data only — no typed event parsing. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimestampedEvent { /// Video timestamp (offset from recording start). - // #[serde(with = "chrono::serde::seconds")] pub video_timestamp: Duration, /// Game timestamp (in-game time). - // #[serde(with = "chrono::serde::seconds_option")] pub game_timestamp: Option, /// Real-world timestamp. pub timestamp: DateTime, - /// Event type name (derived from URI). + /// Event type string derived from URI (e.g. "game_start", "lp_change", "kill"). pub event_type: String, - /// Human-readable description. - pub description: String, - /// The actual event data. - pub event: GameEvent, - /// Raw JSON data from the API (for flexibility). + /// Human-readable description for logging. #[serde(default)] - pub raw_data: Option, + pub description: String, + /// The raw JSON data from the API — the single source of truth. + /// The tauri-app parses the values it needs from this data. + #[serde(default)] + pub raw_data: serde_json::Value, /// URI of the endpoint that triggered this event. #[serde(default)] - pub uri: Option, + pub uri: String, } /// Metadata for a recording. diff --git a/tauri-app/src/components/GameReview.vue b/tauri-app/src/components/GameReview.vue index bcfae39..4cc8e66 100644 --- a/tauri-app/src/components/GameReview.vue +++ b/tauri-app/src/components/GameReview.vue @@ -94,10 +94,11 @@ function getEventColor(event: TimestampedEvent): string { case "phase_change": return "#60a5fa"; // blue case "kill": - case "champions_killed": return getKillEventColor(event); case "objective": return "#fbbf24"; // yellow + case "lp_change": + return "#a78bfa"; // purple default: return "#9ca3af"; // gray } @@ -105,14 +106,13 @@ function getEventColor(event: TimestampedEvent): string { // Get kill event color based on player involvement function getKillEventColor(event: TimestampedEvent): string { - const rawEvent = event.event as { killer?: string; victim?: string } | null; - const rawData = event.raw_data as { Assisters?: string[] } | null; + const rawData = event.raw_data as { KillerName?: string; VictimName?: string; Assisters?: string[] } | null; - if (!rawEvent) return "#9ca3af"; // gray for unknown + if (!rawData) return "#9ca3af"; // gray for unknown - const killer = rawEvent.killer; - const victim = rawEvent.victim; - const assisters = rawData?.Assisters || []; + const killer = rawData.KillerName; + const victim = rawData.VictimName; + const assisters = rawData.Assisters || []; // Get local player's summoner name const localPlayerName = localPlayer.value?.riotIdGameName; diff --git a/tauri-app/src/types/timeline.ts b/tauri-app/src/types/timeline.ts index 4c222f6..30cc132 100644 --- a/tauri-app/src/types/timeline.ts +++ b/tauri-app/src/types/timeline.ts @@ -13,10 +13,14 @@ export interface TimestampedEvent { game_timestamp: [number, number] | null; /** Real-world timestamp (ISO 8601). */ timestamp: string; - /** Event type name. */ + /** Event type name (e.g. "game_start", "lp_change", "kill"). */ event_type: string; /** Human-readable description. */ description: string; + /** Raw JSON data from the League Client API — the single source of truth. */ + raw_data: Record; + /** URI of the endpoint that triggered this event. */ + uri: string; } /** @@ -601,12 +605,19 @@ export function getEventCategory(eventType: string): EventCategory { return "death"; case "objective": return "objective"; - case "gameend": - case "gamestart": - case "matchfound": + case "game_end": + case "game_start": + case "match_found": return "game"; - case "phasechange": + case "phase_change": return "phase"; + case "lp_change": + return "game"; + case "champ_select_start": + case "champion_pick": + return "phase"; + case "stats_update": + return "unknown"; default: return "unknown"; } @@ -623,14 +634,22 @@ export function getEventLabel(eventType: string): string { return "Death"; case "objective": return "Objective"; - case "gameend": + case "game_end": return "Game End"; - case "gamestart": + case "game_start": return "Game Start"; - case "matchfound": + case "match_found": return "Match Found"; - case "phasechange": + case "phase_change": return "Phase Change"; + case "lp_change": + return "LP Change"; + case "champ_select_start": + return "Champ Select"; + case "champion_pick": + return "Champion Pick"; + case "stats_update": + return "Stats Update"; default: return eventType; }