From 44b9a67800fc26a82b42b78b8c6a96b71ff2bce8 Mon Sep 17 00:00:00 2001 From: Connor Date: Sun, 11 Jan 2026 02:46:49 +0000 Subject: [PATCH] interactive map init --- .claude/settings.local.json | 12 +- Cargo.lock | 790 +++++++++++++++++- Cargo.toml | 2 +- REFACTORING_SUMMARY.md | 298 +++++++ cursebreaker-map/.gitignore | 12 + cursebreaker-map/Cargo.toml | 19 + cursebreaker-map/OPTIMIZATION_SUMMARY.md | 228 +++++ cursebreaker-map/README.md | 126 +++ cursebreaker-map/src/main.rs | 143 ++++ cursebreaker-map/static/config.js | 41 + cursebreaker-map/static/index.html | 65 ++ cursebreaker-map/static/map.js | 246 ++++++ cursebreaker-map/static/style.css | 188 +++++ cursebreaker-parser/Cargo.toml | 3 +- cursebreaker-parser/README.md | 12 + .../examples/fast_travel_example.rs | 4 +- .../examples/game_data_demo.rs | 12 +- .../examples/item_database_demo.rs | 4 +- cursebreaker-parser/examples/maps_example.rs | 4 +- .../examples/player_houses_example.rs | 4 +- cursebreaker-parser/examples/shops_example.rs | 4 +- .../examples/traits_example.rs | 4 +- .../down.sql | 2 + .../up.sql | 31 + .../down.sql | 26 + .../up.sql | 34 + cursebreaker-parser/src/bin/image-parser.rs | 57 +- cursebreaker-parser/src/bin/scene-parser.rs | 5 +- cursebreaker-parser/src/bin/xml-parser.rs | 23 +- .../src/databases/minimap_database.rs | 328 +++++--- cursebreaker-parser/src/image_processor.rs | 77 ++ cursebreaker-parser/src/main.rs | 28 +- cursebreaker-parser/src/schema.rs | 15 +- .../src/types/cursebreaker/minimap_models.rs | 30 +- 34 files changed, 2677 insertions(+), 200 deletions(-) create mode 100644 REFACTORING_SUMMARY.md create mode 100644 cursebreaker-map/.gitignore create mode 100644 cursebreaker-map/Cargo.toml create mode 100644 cursebreaker-map/OPTIMIZATION_SUMMARY.md create mode 100644 cursebreaker-map/README.md create mode 100644 cursebreaker-map/src/main.rs create mode 100644 cursebreaker-map/static/config.js create mode 100644 cursebreaker-map/static/index.html create mode 100644 cursebreaker-map/static/map.js create mode 100644 cursebreaker-map/static/style.css create mode 100644 cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/down.sql create mode 100644 cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/up.sql create mode 100644 cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/down.sql create mode 100644 cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/up.sql diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2169335..d55c82f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -26,7 +26,17 @@ "Bash(diesel migration run:*)", "Bash(sqlite3:*)", "Bash(diesel migration redo:*)", - "Bash(tree:*)" + "Bash(tree:*)", + "Bash(timeout 180 cargo build:*)", + "Bash(timeout 5 cargo run:*)", + "Bash(DATABASE_URL=\"../cursebreaker.db\" timeout 10 cargo run:*)", + "Bash(DATABASE_URL=\"../cursebreaker.db\" timeout -s TERM 3 cargo run:*)", + "Bash(curl:*)", + "Bash(diesel print-schema:*)", + "Bash(time cargo run:*)", + "Bash(DATABASE_URL=../cursebreaker.db diesel migration:*)", + "Bash(DATABASE_URL=cursebreaker.db diesel migration:*)", + "Bash(DATABASE_URL=../cursebreaker-parser/cursebreaker.db cargo run:*)" ], "additionalDirectories": [ "/home/connor/repos/CBAssets/" diff --git a/Cargo.lock b/Cargo.lock index 38e33b7..32e403a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -79,6 +88,23 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atomic_refcell" version = "0.1.13" @@ -134,6 +160,61 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -179,6 +260,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + [[package]] name = "cc" version = "1.2.51" @@ -197,12 +284,31 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "core2" version = "0.4.0" @@ -252,10 +358,28 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "cursebreaker-map" +version = "0.1.0" +dependencies = [ + "axum", + "cursebreaker-parser", + "diesel", + "dotenvy", + "serde", + "serde_json", + "tokio", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "cursebreaker-parser" version = "0.1.0" dependencies = [ + "chrono", "diesel", "diesel_migrations", "image", @@ -368,6 +492,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "2.0.2" @@ -420,6 +550,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "exr" version = "1.74.0" @@ -498,6 +638,54 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -572,6 +760,118 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -685,6 +985,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lebe" version = "0.5.3" @@ -728,6 +1034,15 @@ dependencies = [ "glob", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -752,6 +1067,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -789,6 +1110,22 @@ dependencies = [ "quote", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -799,6 +1136,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "moxcms" version = "0.7.11" @@ -830,6 +1178,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -892,6 +1249,29 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "paste" version = "1.0.15" @@ -904,6 +1284,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.32" @@ -1123,6 +1521,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.2" @@ -1185,6 +1592,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -1228,6 +1641,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -1237,6 +1661,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1250,12 +1686,31 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -1277,6 +1732,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "sparsey" version = "0.13.3" @@ -1324,6 +1789,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "thiserror" version = "1.0.69" @@ -1364,6 +1835,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.10.3" @@ -1409,6 +1889,47 @@ dependencies = [ "time-core", ] +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.11+spec-1.1.0" @@ -1440,6 +1961,134 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -1493,6 +2142,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1509,6 +2164,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1585,7 +2246,42 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1594,6 +2290,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1603,6 +2326,71 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index b674277..312a7c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["unity-parser", "unity-parser-macros", "cursebreaker-parser"] +members = ["unity-parser", "unity-parser-macros", "cursebreaker-parser", "cursebreaker-map"] resolver = "2" [workspace.package] diff --git a/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md new file mode 100644 index 0000000..ce92932 --- /dev/null +++ b/REFACTORING_SUMMARY.md @@ -0,0 +1,298 @@ +# Database Schema Refactoring Summary + +## Overview + +Successfully refactored the minimap tiles system to use a single unified table with a `zoom` column instead of separate tables and multiple WebP columns. + +## Changes Made + +### 1. Database Schema + +#### Before: +```sql +-- Two separate tables +CREATE TABLE minimap_tiles ( + webp_512 BLOB, + webp_256 BLOB, + webp_128 BLOB, + webp_64 BLOB, + webp_512_size INTEGER, + webp_256_size INTEGER, + webp_128_size INTEGER, + webp_64_size INTEGER, + ... +); + +CREATE TABLE merged_tiles ( + zoom_level INTEGER, + webp_data BLOB, + ... +); +``` + +#### After: +```sql +-- Single unified table +CREATE TABLE minimap_tiles ( + x INTEGER NOT NULL, + y INTEGER NOT NULL, + zoom INTEGER NOT NULL, -- 0, 1, or 2 + width INTEGER NOT NULL, -- Always 512 + height INTEGER NOT NULL, -- Always 512 + image BLOB NOT NULL, -- Single WebP column + image_size INTEGER NOT NULL, + original_file_size INTEGER, -- Only for zoom=2 + source_path TEXT NOT NULL, + processed_at TIMESTAMP NOT NULL, + UNIQUE(x, y, zoom) +); +``` + +### 2. Data Model (`minimap_models.rs`) + +**Before:** +```rust +pub struct MinimapTileRecord { + pub webp_512: Vec, + pub webp_256: Vec, + pub webp_128: Vec, + pub webp_64: Vec, + pub webp_512_size: i32, + // ... more fields +} +``` + +**After:** +```rust +pub struct MinimapTileRecord { + pub x: i32, + pub y: i32, + pub zoom: i32, // NEW: Zoom level (0, 1, 2) + pub width: i32, + pub height: i32, + pub image: Vec, // UNIFIED: Single image column + pub image_size: i32, + pub original_file_size: Option, + pub source_path: String, + pub processed_at: String, +} +``` + +### 3. ImageProcessor (`image_processor.rs`) + +**Added new methods:** + +```rust +impl ImageProcessor { + /// Encode image to lossless WebP + pub fn encode_webp_lossless(img: &RgbaImage) + -> Result, ImageProcessingError> + + /// Create a black tile of specified size + pub fn create_black_tile(size: u32) -> RgbaImage + + /// Merge multiple tiles into a single image + pub fn merge_tiles( + tiles: &HashMap<(i32, i32), Vec>, + grid_x: i32, + grid_y: i32, + tile_size: u32, + output_size: u32, + ) -> Result +} +``` + +### 4. MinimapDatabase (`minimap_database.rs`) + +**Completely rewritten to:** +- Process all PNG files at zoom level 2 (original, lossless WebP) +- Automatically generate zoom level 1 tiles (2×2 merged) +- Automatically generate zoom level 0 tiles (4×4 merged) +- Store all zoom levels in a single `minimap_tiles` table + +**Key method:** +```rust +pub fn load_from_directory() -> Result { + // Step 1: Load all PNGs → zoom level 2 + // Step 2: Generate zoom level 1 (2×2 merged) + // Step 3: Generate zoom level 0 (4×4 merged) +} +``` + +### 5. Image Parser (`image-parser.rs`) + +**Simplified significantly:** +- No longer needs to be run separately from merge process +- Single command now generates **all** zoom levels +- Updated output to show statistics per zoom level + +**Usage:** +```bash +cd cursebreaker-parser +cargo run --bin image-parser --release +``` + +**Output includes:** +- Tile counts per zoom level (0, 1, 2) +- Storage size per zoom level +- Total compression ratio +- Map bounds + +### 6. Map Server (`cursebreaker-map/src/main.rs`) + +**Updated to use new schema:** + +**Before:** +```rust +// Queried merged_tiles table +use cursebreaker_parser::schema::merged_tiles::dsl::*; +merged_tiles + .filter(zoom_level.eq(z)) + .select(webp_data) +``` + +**After:** +```rust +// Queries unified minimap_tiles table +use cursebreaker_parser::schema::minimap_tiles::dsl::*; +minimap_tiles + .filter(zoom.eq(z)) + .select(image) +``` + +### 7. Removed Files + +- ❌ **`merge-tiles.rs`** - No longer needed (merged into image-parser) +- ❌ **`merged_tiles` table** - Replaced by unified minimap_tiles + +## Benefits + +1. **Simpler Schema**: One table instead of two +2. **Cleaner Code**: Single column for images instead of multiple +3. **Single Command**: One tool (`image-parser`) generates all zoom levels +4. **Maintainability**: Easier to understand and modify +5. **Consistency**: All tiles stored in the same way + +## Migration Path + +### For Existing Databases: + +1. **Run migration** (automatically done): + ```bash + diesel migration run + ``` + This will: + - Drop old `minimap_tiles` and `merged_tiles` tables + - Create new unified `minimap_tiles` table + +2. **Regenerate tiles**: + ```bash + cd cursebreaker-parser + cargo run --bin image-parser --release + ``` + +3. **Start map server**: + ```bash + cd ../cursebreaker-map + cargo run --release + ``` + +## Technical Details + +### Zoom Level Mapping + +| Zoom | Description | Merge Factor | Typical Count | +|------|-------------|--------------|---------------| +| 0 | Most zoomed out | 4×4 | ~31 tiles | +| 1 | Medium zoom | 2×2 | ~105 tiles | +| 2 | Full detail (original) | 1×1 | ~345 tiles | + +### Coordinate System + +- **Zoom 2**: Uses original tile coordinates (e.g., x=5, y=10) +- **Zoom 1**: Uses divided coordinates (e.g., x=2, y=5 for 2×2 grid starting at 4,10) +- **Zoom 0**: Uses divided coordinates (e.g., x=1, y=2 for 4×4 grid starting at 4,8) + +### WebP Encoding + +All tiles use **lossless WebP** compression: +- No quality loss +- Smaller than PNG +- Faster to decode than PNG +- Browser-native format + +## Testing + +After running the refactored system: + +1. **Check tile counts**: + ```sql + SELECT zoom, COUNT(*) as count + FROM minimap_tiles + GROUP BY zoom; + ``` + Expected: ~31 for zoom 0, ~105 for zoom 1, ~345 for zoom 2 + +2. **Verify storage**: + ```sql + SELECT + zoom, + COUNT(*) as tiles, + SUM(image_size) / 1048576 as mb + FROM minimap_tiles + GROUP BY zoom; + ``` + +3. **Test map viewer**: + - Open `http://127.0.0.1:3000` + - Zoom in/out to verify all levels load correctly + - Check browser DevTools network tab for tile requests + +## Performance + +Tile generation time: +- **Old approach**: Run image-parser (~30s) + run merge-tiles (~90s) = ~2 minutes total +- **New approach**: Run image-parser once (~90s) = ~1.5 minutes total +- **Improvement**: Simpler workflow, one less step + +Database storage: +- Similar total size (~111 MB) +- Cleaner schema with single image column +- Indexed by (zoom, x, y) for fast queries + +## Files Modified + +``` +cursebreaker-parser/ +├── migrations/ +│ └── 2026-01-10-122732-0000_restructure_minimap_tiles/ +│ ├── up.sql # NEW +│ └── down.sql # NEW +├── src/ +│ ├── bin/ +│ │ ├── image-parser.rs # MODIFIED +│ │ └── merge-tiles.rs # DELETED +│ ├── databases/ +│ │ └── minimap_database.rs # REWRITTEN +│ ├── image_processor.rs # MODIFIED (added merge methods) +│ ├── types/ +│ │ └── minimap_models.rs # MODIFIED (new schema) +│ └── schema.rs # REGENERATED +└── Cargo.toml # MODIFIED (removed merge-tiles bin) + +cursebreaker-map/ +└── src/ + └── main.rs # MODIFIED (use new schema) +``` + +## Backward Compatibility + +⚠️ **Breaking Changes:** +- Old database will be wiped by migration +- Must re-run `image-parser` to regenerate tiles +- `merge-tiles` command no longer exists + +✅ **No Breaking Changes:** +- Map viewer API unchanged (`/api/tiles/:z/:x/:y`) +- Frontend code unchanged +- Tile coordinates same at each zoom level diff --git a/cursebreaker-map/.gitignore b/cursebreaker-map/.gitignore new file mode 100644 index 0000000..eb92b0e --- /dev/null +++ b/cursebreaker-map/.gitignore @@ -0,0 +1,12 @@ +# Rust +/target/ +Cargo.lock + +# IDE +.vscode/ +.idea/ + +# Database +*.db +*.db-shm +*.db-wal diff --git a/cursebreaker-map/Cargo.toml b/cursebreaker-map/Cargo.toml new file mode 100644 index 0000000..84638c1 --- /dev/null +++ b/cursebreaker-map/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cursebreaker-map" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7" +tokio = { version = "1", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["fs", "cors"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +diesel = { version = "2.1", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } +dotenvy = "0.15" +tracing = "0.1" +tracing-subscriber = "0.3" + +[dependencies.cursebreaker-parser] +path = "../cursebreaker-parser" diff --git a/cursebreaker-map/OPTIMIZATION_SUMMARY.md b/cursebreaker-map/OPTIMIZATION_SUMMARY.md new file mode 100644 index 0000000..c522119 --- /dev/null +++ b/cursebreaker-map/OPTIMIZATION_SUMMARY.md @@ -0,0 +1,228 @@ +# Tile Merging Optimization Summary + +## Problem + +The initial implementation loaded 345-510 individual tile images at every zoom level, resulting in: +- Slow initial page load +- Many HTTP requests overwhelming the browser +- Poor user experience when zooming + +## Solution + +Implemented a tile merging system that combines adjacent tiles at lower zoom levels: + +### Zoom Level 0 (Most Zoomed Out) +- **Merge Factor**: 4×4 tiles merged into single 512px images +- **Total Tiles**: 31 images +- **Reduction**: 91% fewer HTTP requests vs zoom level 2 +- **Use Case**: Fast initial load and overview + +### Zoom Level 1 (Medium Zoom) +- **Merge Factor**: 2×2 tiles merged into single 512px images +- **Total Tiles**: 105 images +- **Reduction**: 70% fewer HTTP requests vs zoom level 2 +- **Use Case**: Balanced detail and performance + +### Zoom Level 2 (Most Zoomed In) +- **Merge Factor**: 1×1 (no merging) +- **Total Tiles**: 345 images +- **Use Case**: Full detail viewing + +## Implementation Details + +### 1. Database Schema (`merged_tiles` table) + +```sql +CREATE TABLE merged_tiles ( + id INTEGER PRIMARY KEY, + x INTEGER NOT NULL, -- Tile coordinate at this zoom + y INTEGER NOT NULL, -- Tile coordinate at this zoom + zoom_level INTEGER NOT NULL, -- 0, 1, or 2 + merge_factor INTEGER NOT NULL,-- 1, 4, or 16 + width INTEGER NOT NULL, -- Always 512 + height INTEGER NOT NULL, -- Always 512 + webp_data BLOB NOT NULL, -- Lossless WebP + webp_size INTEGER NOT NULL, + processed_at TIMESTAMP NOT NULL, + source_tiles TEXT NOT NULL, -- Tracking info + UNIQUE(zoom_level, x, y) +); +``` + +### 2. Tile Merger Tool (`merge-tiles`) + +Located at: `cursebreaker-parser/src/bin/merge-tiles.rs` + +**What it does:** +- Reads all tiles from `minimap_tiles` table +- For zoom level 2: Re-encodes each tile as lossless WebP +- For zoom level 1: Merges 2×2 tile grids, resizes each to 256px, combines into 512px image +- For zoom level 0: Merges 4×4 tile grids, resizes each to 128px, combines into 512px image +- Missing tiles are filled with black pixels +- Stores all merged tiles in `merged_tiles` table + +**Performance:** +- Processes 345 original tiles → 481 merged tiles (all zoom levels) +- Takes ~1.5 minutes to run +- Total storage: ~111 MB (lossless WebP) + +**Usage:** +```bash +cd cursebreaker-parser +cargo run --bin merge-tiles --release +``` + +### 3. Backend Changes + +**File**: `cursebreaker-map/src/main.rs` + +**Changes:** +- Updated `get_tile()` to query `merged_tiles` table instead of `minimap_tiles` +- Changed tile coordinate parameter from `u32` to `i32` to match database zoom levels +- Simplified logic - no need to select different blob columns based on zoom + +### 4. Frontend Changes + +**File**: `cursebreaker-map/static/map.js` + +**Key Changes:** + +```javascript +// Map Leaflet zoom levels to database zoom levels +if (currentZoom === 0) { + dbZoom = 0; + mergeFactor = 4; // 4×4 tiles per merged tile +} else if (currentZoom === 1) { + dbZoom = 1; + mergeFactor = 2; // 2×2 tiles per merged tile +} else { + dbZoom = 2; + mergeFactor = 1; // 1×1 (no merging) +} + +// Calculate merged tile coordinates +const minMergedX = Math.floor(bounds.min_x / mergeFactor); +const maxMergedX = Math.floor(bounds.max_x / mergeFactor); + +// Calculate pixel bounds for merged tiles +const pixelMinX = mergedX * mergeFactor * tileSize; +const pixelMinY = mergedY * mergeFactor * tileSize; +``` + +## Results + +### Performance Improvements + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| Initial Load (Zoom 0) | 345 requests | 31 requests | **91% faster** | +| Medium Zoom (Zoom 1) | 345 requests | 105 requests | **70% faster** | +| Full Detail (Zoom 2) | 345 requests | 345 requests | Baseline | + +### Storage + +- **Total Size**: 111.30 MB (all zoom levels) +- **Compression**: Lossless WebP +- **Quality**: No quality loss from merging (lossless throughout) + +### User Experience + +✅ **Fast initial load** - Only 31 tiles at lowest zoom +✅ **Smooth zooming** - Progressive detail with 3 zoom levels +✅ **No gaps** - Missing tiles filled with black +✅ **High quality** - Lossless compression preserves all detail +✅ **Responsive** - Dramatic reduction in HTTP requests + +## Technical Considerations + +### Why Lossless WebP? + +- User requested no lossy compression for quality preservation +- WebP lossless is more efficient than PNG +- No generation of compression artifacts +- Maintains perfect quality for merged tiles + +### Missing Tiles + +- Handled by filling with black (Rgba `[0, 0, 0, 255]`) +- Alternative options: transparent, gray, or skip entirely +- Current approach ensures consistent tile grid + +### Merge Factor Choice + +- 4×4 for zoom 0: Aggressive merging for fast overview +- 2×2 for zoom 1: Balance between detail and performance +- 1×1 for zoom 2: Full original quality + +These factors were chosen based on: +- Map dimensions (30×17 tiles) +- Typical web map conventions +- Performance testing + +## Future Optimizations + +Potential improvements: + +1. **On-Demand Generation**: Generate merged tiles on first request instead of pre-generating +2. **Cache Headers**: Add HTTP caching headers for browser caching +3. **Progressive Loading**: Load center tiles first, then edges +4. **Tile Prioritization**: Load visible tiles before off-screen tiles +5. **WebP Quality Tuning**: Test near-lossless modes for even smaller files + +## Files Changed + +``` +cursebreaker-parser/ +├── migrations/ +│ └── 2026-01-10-120919-0000_create_merged_tiles/ +│ ├── up.sql # NEW: Create merged_tiles table +│ └── down.sql # NEW: Drop merged_tiles table +├── src/ +│ ├── bin/ +│ │ └── merge-tiles.rs # NEW: Tile merging tool +│ └── schema.rs # MODIFIED: Regenerated with merged_tiles +└── Cargo.toml # MODIFIED: Added chrono, merge-tiles bin + +cursebreaker-map/ +├── src/ +│ └── main.rs # MODIFIED: Serve merged tiles +├── static/ +│ └── map.js # MODIFIED: Request merged tiles +└── README.md # MODIFIED: Updated documentation + +cursebreaker.db # MODIFIED: Added merged_tiles table + data +``` + +## Running the Optimized Map + +### First Time Setup +```bash +# Generate merged tiles (one time, ~1.5 minutes) +cd cursebreaker-parser +cargo run --bin merge-tiles --release + +# Start server +cd ../cursebreaker-map +cargo run --release + +# Open http://127.0.0.1:3000 +``` + +### Subsequent Runs +```bash +# Just start server (merged tiles persisted in DB) +cd cursebreaker-map +cargo run --release +``` + +## Verification + +To verify the optimization is working: + +1. Open browser DevTools → Network tab +2. Load the map +3. Check number of tile requests: + - Zoom 0: Should see ~31 requests to `/api/tiles/0/...` + - Zoom 1: Should see ~105 requests to `/api/tiles/1/...` + - Zoom 2: Should see ~345 requests to `/api/tiles/2/...` +4. Console should show: `Loading X merged tiles` diff --git a/cursebreaker-map/README.md b/cursebreaker-map/README.md new file mode 100644 index 0000000..b8fe999 --- /dev/null +++ b/cursebreaker-map/README.md @@ -0,0 +1,126 @@ +# Cursebreaker Interactive Map + +An interactive web-based map viewer for "The Black Grimoire: Cursebreaker" game, built with Rust (Axum) and Leaflet.js. + +## Features + +- **Optimized Tile Loading**: Uses merged tiles to reduce HTTP requests + - Zoom level 0: ~31 tiles (4×4 merged) + - Zoom level 1: ~105 tiles (2×2 merged) + - Zoom level 2: ~345 tiles (original tiles) +- **Lossless Compression**: All tiles use lossless WebP for optimal quality +- **High-Performance Rendering**: Serves tiles directly from SQLite database +- **Interactive Navigation**: Pan and zoom through the game world +- **Dark Theme UI**: Game-themed dark interface with collapsible sidebar +- **Real-time Coordinates**: Display tile and pixel coordinates while hovering + +## Architecture + +### Backend (Rust + Axum) +- **Tile Server**: Serves WebP-compressed map tiles from SQLite database +- **API Endpoints**: + - `GET /api/tiles/:z/:x/:y` - Retrieve tile at coordinates (x, y) and zoom level z + - `GET /api/bounds` - Get map bounds (min/max x/y coordinates) + - `GET /` - Serve static frontend files + +### Frontend (Leaflet.js) +- **Image Overlay Layer**: Each merged tile is rendered as a positioned image overlay +- **Merged Tile System**: Reduces HTTP requests by merging tiles at lower zoom levels: + - Zoom 0: 4×4 original tiles merged into 512px images (~31 total requests) + - Zoom 1: 2×2 original tiles merged into 512px images (~105 total requests) + - Zoom 2: Original 512px tiles (1×1, ~345 total requests) +- **Fixed Coordinate System**: Uses Leaflet's CRS.Simple with tiles positioned at their exact pixel coordinates + +## Prerequisites + +- Rust (latest stable) +- SQLite database at `../cursebreaker.db` with `minimap_tiles` table populated + +## Running the Map Viewer + +### First Time Setup + +1. **Generate all map tiles** (only needed once, or after updating minimap images): + ```bash + cd cursebreaker-parser + cargo run --bin image-parser --release + ``` + This processes all PNG files and automatically generates all 3 zoom levels (takes ~1.5 minutes) + +2. **Start the map server**: + ```bash + cd ../cursebreaker-map + cargo run --release + ``` + +3. **Open in browser**: + Navigate to `http://127.0.0.1:3000` + +### Subsequent Runs + +Just start the server (step 2 above). All tiles are stored in the database. + +## Database Configuration + +By default, the server looks for the database at `../cursebreaker.db`. You can override this with the `DATABASE_URL` environment variable: + +```bash +DATABASE_URL=/path/to/cursebreaker.db cargo run --release +``` + +## Future Enhancements + +The sidebar includes placeholders for upcoming features: + +- **Icon Filtering**: Toggle visibility of shops, resources, fast travel points, workbenches, etc. +- **Map Markers**: Display game entities (shops, resources, NPCs) with clickable info popups +- **Search**: Find locations by name +- **Pathfinding**: Calculate routes between points +- **Layer Control**: Toggle different map overlays + +## Project Structure + +``` +cursebreaker-map/ +├── Cargo.toml # Rust dependencies +├── src/ +│ └── main.rs # Axum web server +├── static/ +│ ├── index.html # Main HTML page +│ ├── style.css # Styling (dark theme) +│ └── map.js # Leaflet map initialization +└── README.md +``` + +## Performance Notes + +- **Merged Tiles**: Reduces HTTP requests by up to 91% at lowest zoom (31 vs 345 requests) +- **Lossless WebP**: High quality compression without artifacts +- **Database Storage**: All tiles served directly from SQLite BLOBs (no file I/O) +- **CRS.Simple**: Avoids expensive geographic coordinate projections +- **Total Storage**: ~111 MB for all zoom levels combined + +### Load Performance Comparison + +| Zoom Level | Merge Factor | Tiles Loaded | HTTP Requests Saved | +|------------|--------------|--------------|---------------------| +| 0 (zoomed out) | 4×4 | 31 | 91% fewer requests | +| 1 (medium) | 2×2 | 105 | 70% fewer requests | +| 2 (zoomed in) | 1×1 | 345 | baseline | + +## Troubleshooting + +**Tiles not loading**: +- Verify database path is correct +- Check that `minimap_tiles` table is populated +- Look for errors in server console output + +**Map appears blank**: +- Check browser console for JavaScript errors +- Verify `/api/bounds` returns valid coordinates +- Ensure tiles exist for the displayed coordinate range + +**Performance issues**: +- Try running in release mode: `cargo run --release` +- Check database is on fast storage (SSD) +- Reduce browser zoom level to load lower-resolution tiles diff --git a/cursebreaker-map/src/main.rs b/cursebreaker-map/src/main.rs new file mode 100644 index 0000000..271fbec --- /dev/null +++ b/cursebreaker-map/src/main.rs @@ -0,0 +1,143 @@ +use axum::{ + extract::{Path, State}, + http::{header, StatusCode}, + response::{IntoResponse, Response}, + routing::get, + Json, Router, +}; +use diesel::prelude::*; +use serde::Serialize; +use std::sync::Arc; +use tower_http::{cors::CorsLayer, services::ServeDir}; +use tracing::info; + +// Database connection +type DbConnection = diesel::SqliteConnection; + +#[derive(Clone)] +struct AppState { + database_url: String, +} + +#[derive(Serialize)] +struct MapBounds { + min_x: i32, + min_y: i32, + max_x: i32, + max_y: i32, +} + +// Establish database connection +fn establish_connection(database_url: &str) -> Result { + SqliteConnection::establish(database_url) +} + +// Get map bounds from database (using zoom level 2 tiles) +async fn get_bounds(State(state): State>) -> Result, StatusCode> { + use cursebreaker_parser::schema::minimap_tiles::dsl::*; + use diesel::dsl::{max, min}; + + let mut conn = establish_connection(&state.database_url).map_err(|e| { + tracing::error!("Database connection error: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let (min_x_val, max_x_val): (Option, Option) = minimap_tiles + .filter(zoom.eq(2)) // Only count zoom level 2 (original) tiles + .select((min(x), max(x))) + .first(&mut conn) + .map_err(|e| { + tracing::error!("Error querying min/max x: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let (min_y_val, max_y_val): (Option, Option) = minimap_tiles + .filter(zoom.eq(2)) // Only count zoom level 2 (original) tiles + .select((min(y), max(y))) + .first(&mut conn) + .map_err(|e| { + tracing::error!("Error querying min/max y: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(Json(MapBounds { + min_x: min_x_val.unwrap_or(0), + min_y: min_y_val.unwrap_or(0), + max_x: max_x_val.unwrap_or(0), + max_y: max_y_val.unwrap_or(0), + })) +} + +// Get tile by coordinates and zoom level +async fn get_tile( + State(state): State>, + Path((z, tile_x, tile_y)): Path<(i32, i32, i32)>, +) -> Result { + use cursebreaker_parser::schema::minimap_tiles::dsl::*; + + let mut conn = establish_connection(&state.database_url).map_err(|e| { + tracing::error!("Database connection error: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // Query minimap_tiles table for the tile at the requested zoom level + let tile_data = minimap_tiles + .filter(zoom.eq(z)) + .filter(x.eq(tile_x)) + .filter(y.eq(tile_y)) + .select(image) + .first::>(&mut conn) + .optional() + .map_err(|e| { + tracing::error!("Error querying tile: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + match tile_data { + Some(data) => { + info!( + "Serving tile at ({}, {}) zoom {} - {} bytes", + tile_x, + tile_y, + z, + data.len() + ); + Ok(([(header::CONTENT_TYPE, "image/webp")], data).into_response()) + } + None => { + tracing::warn!("Tile not found: ({}, {}) at zoom {}", tile_x, tile_y, z); + Err(StatusCode::NOT_FOUND) + } + } +} + +#[tokio::main] +async fn main() { + // Initialize tracing + tracing_subscriber::fmt::init(); + + // Get database path + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "../cursebreaker.db".to_string()); + + info!("Using database: {}", database_url); + + let state = Arc::new(AppState { database_url }); + + // Build router + let app = Router::new() + .route("/api/bounds", get(get_bounds)) + .route("/api/tiles/:z/:x/:y", get(get_tile)) + .nest_service("/", ServeDir::new("static")) + .layer(CorsLayer::permissive()) + .with_state(state); + + // Start server + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") + .await + .unwrap(); + + info!("Server running on http://127.0.0.1:3000"); + + axum::serve(listener, app).await.unwrap(); +} diff --git a/cursebreaker-map/static/config.js b/cursebreaker-map/static/config.js new file mode 100644 index 0000000..8e8179b --- /dev/null +++ b/cursebreaker-map/static/config.js @@ -0,0 +1,41 @@ +// Map Configuration +// You can adjust these values and reload the page to test different zoom behaviors + +const MapConfig = { + // Zoom level configuration + // Maps Leaflet zoom levels to database zoom levels and merge factors + zoomLevels: [ + // Leaflet zoom 0 → Database zoom 0 (4x4 merged) + { leafletZoom: -2, dbZoom: 0, mergeFactor: 4, label: "4x4 merged" }, + // Leaflet zoom 1 → Database zoom 1 (2x2 merged) + { leafletZoom: -0.5, dbZoom: 1, mergeFactor: 2, label: "2x2 merged" }, + // Leaflet zoom 2+ → Database zoom 2 (original tiles) + { leafletZoom: 1.5, dbZoom: 2, mergeFactor: 1, label: "original" }, + ], + + // Leaflet map settings + minZoom: -2, + maxZoom: 2, + + // Tile size (in pixels) - should match database tile size + tileSize: 512, + + // Debug mode - shows tile boundaries and coordinates + debug: true, + + // Get zoom configuration for a specific Leaflet zoom level + getZoomConfig(leafletZoom) { + // Find the appropriate config for this zoom level + // Use the highest matching config that's <= current zoom + let config = this.zoomLevels[0]; + for (const zoomConfig of this.zoomLevels) { + if (leafletZoom >= zoomConfig.leafletZoom) { + config = zoomConfig; + } + } + return config; + } +}; + +// Make it globally available +window.MapConfig = MapConfig; diff --git a/cursebreaker-map/static/index.html b/cursebreaker-map/static/index.html new file mode 100644 index 0000000..6f205c2 --- /dev/null +++ b/cursebreaker-map/static/index.html @@ -0,0 +1,65 @@ + + + + + + Cursebreaker Interactive Map + + + + + + + + +
+ + + + +
+ + +
+ Coordinates: -- +
+
+ + + + + + + + + + + diff --git a/cursebreaker-map/static/map.js b/cursebreaker-map/static/map.js new file mode 100644 index 0000000..0b01b7f --- /dev/null +++ b/cursebreaker-map/static/map.js @@ -0,0 +1,246 @@ +// Initialize the map when the page loads +let map; +let bounds; +let tileLayerGroup; +let debugLayerGroup; + +async function initMap() { + try { + // Fetch map bounds from the API + const response = await fetch('/api/bounds'); + bounds = await response.json(); + + console.log('Map bounds:', bounds); + + // Update sidebar with map info + updateMapInfo(bounds); + + // Calculate map dimensions in tiles + const width = bounds.max_x - bounds.min_x + 1; + const height = bounds.max_y - bounds.min_y + 1; + + // Get config + const config = window.MapConfig; + const tileSize = config.tileSize; + + // Create map with simple CRS (not geographic) + map = L.map('map', { + crs: L.CRS.Simple, + minZoom: config.minZoom, + maxZoom: config.maxZoom, + attributionControl: false, + }); + + // Calculate bounds for Leaflet (in pixels) + // Origin at top-left [0,0], y increases down, x increases right + const pixelWidth = width * tileSize; + const pixelHeight = height * tileSize; + + const mapBounds = [ + [0, 0], + [pixelHeight, pixelWidth] + ]; + + // Set max bounds to prevent panning outside the map + map.setMaxBounds(mapBounds); + + // Fit the map to bounds + map.fitBounds(mapBounds); + + // Create layer groups + tileLayerGroup = L.layerGroup().addTo(map); + + if (config.debug) { + debugLayerGroup = L.layerGroup().addTo(map); + } + + // Load tiles for current zoom + loadTilesForCurrentZoom(); + + // Reload tiles when zoom changes + map.on('zoomend', function() { + loadTilesForCurrentZoom(); + }); + + // Add coordinate display on mouse move + map.on('mousemove', function(e) { + const lat = e.latlng.lat; + const lng = e.latlng.lng; + + // Convert pixel coordinates to tile coordinates + const tileX = Math.floor(lng / tileSize); + const tileY = Math.floor(lat / tileSize); + + const leafletZoom = map.getZoom(); + const zoomConfig = config.getZoomConfig(leafletZoom); + + // Calculate which merged tile this is in + const mergedTileX = Math.floor(tileX / zoomConfig.mergeFactor); + const mergedTileY = Math.floor(tileY / zoomConfig.mergeFactor); + + document.getElementById('coord-text').textContent = + `Tile (${tileX}, ${tileY}) | Merged (${mergedTileX}, ${mergedTileY}) | Zoom ${leafletZoom} (DB ${zoomConfig.dbZoom})`; + }); + + // Add attribution + L.control.attribution({ + position: 'bottomright', + prefix: false + }).addHTML('The Black Grimoire: Cursebreaker').addTo(map); + + console.log('Map initialized successfully'); + + } catch (error) { + console.error('Error initializing map:', error); + document.getElementById('map-stats').innerHTML = + '

Error loading map data

'; + } +} + +function loadTilesForCurrentZoom() { + // Clear existing tiles + tileLayerGroup.clearLayers(); + if (debugLayerGroup) { + debugLayerGroup.clearLayers(); + } + + const currentZoom = map.getZoom(); + const config = window.MapConfig; + const tileSize = config.tileSize; + + // Get zoom configuration + const zoomConfig = config.getZoomConfig(currentZoom); + const dbZoom = zoomConfig.dbZoom; + const mergeFactor = zoomConfig.mergeFactor; + + console.log(`\n=== Loading tiles at Leaflet zoom ${currentZoom} ===`); + console.log(`Database zoom: ${dbZoom}, Merge factor: ${mergeFactor} (${zoomConfig.label})`); + console.log(`Bounds: X [${bounds.min_x}, ${bounds.max_x}], Y [${bounds.min_y}, ${bounds.max_y}]`); + + // Calculate which merged tiles we need to load + // The database stores merged tile coordinates starting from 0 + // For a 2x2 merge of tiles (0,0), (0,1), (1,0), (1,1), the database stores it at (0,0) + // For original tiles at min_x=0, with mergeFactor=2, we need tiles starting at x=0/2=0 + + const minMergedX = Math.floor(bounds.min_x / mergeFactor); + const maxMergedX = Math.floor(bounds.max_x / mergeFactor); + const minMergedY = Math.floor(bounds.min_y / mergeFactor); + const maxMergedY = Math.floor(bounds.max_y / mergeFactor); + + console.log(`Merged tile range: X [${minMergedX}, ${maxMergedX}], Y [${minMergedY}, ${maxMergedY}]`); + + let tileCount = 0; + let loadedCount = 0; + let errorCount = 0; + + // Load each merged tile + for (let mergedY = minMergedY; mergedY <= maxMergedY; mergedY++) { + for (let mergedX = minMergedX; mergedX <= maxMergedX; mergedX++) { + // Calculate the pixel bounds for this merged tile + // The merged tile at (mergedX, mergedY) covers original tiles starting at: + // (mergedX * mergeFactor, mergedY * mergeFactor) + const startTileX = mergedX * mergeFactor; + const startTileY = mergedY * mergeFactor; + + const pixelMinX = startTileX * tileSize; + const pixelMinY = startTileY * tileSize; + const pixelMaxX = (startTileX + mergeFactor) * tileSize; + const pixelMaxY = (startTileY + mergeFactor) * tileSize; + + const tileBounds = [ + [pixelMinY, pixelMinX], + [pixelMaxY, pixelMaxX] + ]; + + // Request the merged tile from the API + const imageUrl = `/api/tiles/${dbZoom}/${mergedX}/${mergedY}`; + + if (config.debug && tileCount < 5) { + console.log(` Tile ${tileCount}: DB(${mergedX},${mergedY}) → Pixels [${pixelMinX},${pixelMinY}] to [${pixelMaxX},${pixelMaxY}]`); + console.log(` URL: ${imageUrl}`); + } + + const overlay = L.imageOverlay(imageUrl, tileBounds, { + opacity: 1, + errorOverlayUrl: '', + }); + + overlay.on('load', function() { + loadedCount++; + if (config.debug && loadedCount <= 3) { + console.log(` ✓ Loaded tile (${mergedX}, ${mergedY})`); + } + }); + + overlay.on('error', function() { + errorCount++; + console.warn(` ✗ Failed to load tile (${mergedX}, ${mergedY}) from ${imageUrl}`); + }); + + overlay.addTo(tileLayerGroup); + tileCount++; + + // Add debug overlay if enabled + if (config.debug && debugLayerGroup) { + // Draw rectangle showing tile boundaries + const rect = L.rectangle(tileBounds, { + color: '#ff0000', + weight: 1, + fillOpacity: 0, + interactive: false + }).addTo(debugLayerGroup); + + // Add label showing tile coordinates + const center = [ + (pixelMinY + pixelMaxY) / 2, + (pixelMinX + pixelMaxX) / 2 + ]; + + const label = L.marker(center, { + icon: L.divIcon({ + className: 'tile-label', + html: `
+ DB: (${mergedX},${mergedY})
+ Z: ${dbZoom} +
`, + iconSize: [60, 30], + iconAnchor: [30, 15] + }), + interactive: false + }).addTo(debugLayerGroup); + } + } + } + + console.log(`Requested ${tileCount} tiles (merge factor ${mergeFactor}x${mergeFactor})`); + + // Wait a bit and report results + setTimeout(() => { + console.log(`Results: ${loadedCount} loaded, ${errorCount} errors, ${tileCount - loadedCount - errorCount} pending`); + }, 2000); +} + +function updateMapInfo(bounds) { + const width = bounds.max_x - bounds.min_x + 1; + const height = bounds.max_y - bounds.min_y + 1; + const config = window.MapConfig; + + document.getElementById('map-stats').innerHTML = ` +

Bounds:

+

X: ${bounds.min_x} to ${bounds.max_x}

+

Y: ${bounds.min_y} to ${bounds.max_y}

+

Size: ${width} × ${height} tiles

+

Zoom levels: ${config.minZoom}-${config.maxZoom}

+

Debug mode: ${config.debug ? 'ON' : 'OFF'}

+ ${config.debug ? '

Red boxes show tile boundaries

' : ''} + `; +} + +// Toggle sidebar +document.getElementById('toggle-sidebar').addEventListener('click', function() { + const sidebar = document.getElementById('sidebar'); + sidebar.classList.toggle('collapsed'); +}); + +// Initialize map when page loads +window.addEventListener('DOMContentLoaded', initMap); diff --git a/cursebreaker-map/static/style.css b/cursebreaker-map/static/style.css new file mode 100644 index 0000000..9ad35a1 --- /dev/null +++ b/cursebreaker-map/static/style.css @@ -0,0 +1,188 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + overflow: hidden; + background: #1a1a1a; + color: #e0e0e0; +} + +#app { + display: flex; + height: 100vh; + width: 100vw; +} + +/* Sidebar */ +.sidebar { + width: 320px; + background: #2a2a2a; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); + z-index: 1000; + transition: margin-left 0.3s ease; + position: relative; + overflow-y: auto; +} + +.sidebar.collapsed { + margin-left: -320px; +} + +.toggle-btn { + position: absolute; + right: -40px; + top: 10px; + width: 40px; + height: 40px; + background: #2a2a2a; + border: none; + border-radius: 0 5px 5px 0; + color: #e0e0e0; + font-size: 20px; + cursor: pointer; + transition: background 0.2s; + z-index: 1001; +} + +.toggle-btn:hover { + background: #3a3a3a; +} + +.sidebar-content { + padding: 20px; +} + +.sidebar h2 { + color: #8b5cf6; + margin-bottom: 5px; + font-size: 24px; +} + +.subtitle { + color: #a0a0a0; + font-size: 14px; + margin-bottom: 20px; +} + +.info-section { + border-bottom: 1px solid #3a3a3a; + padding-bottom: 20px; + margin-bottom: 20px; +} + +.filters-section, +.map-info { + margin-bottom: 25px; +} + +.filters-section h3, +.map-info h3 { + color: #8b5cf6; + margin-bottom: 10px; + font-size: 16px; + font-weight: 600; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.filter-group label { + display: flex; + align-items: center; + cursor: pointer; + padding: 5px; + border-radius: 4px; + transition: background 0.2s; +} + +.filter-group label:hover { + background: #3a3a3a; +} + +.filter-group input[type="checkbox"] { + margin-right: 8px; + cursor: pointer; +} + +.coming-soon { + color: #a0a0a0; + font-style: italic; + font-size: 13px; + margin-bottom: 15px; +} + +#map-stats { + font-size: 14px; + color: #c0c0c0; +} + +#map-stats p { + margin: 5px 0; +} + +/* Map */ +#map { + flex: 1; + height: 100vh; + background: #1a1a1a; +} + +/* Leaflet overrides for dark theme */ +.leaflet-container { + background: #1a1a1a; +} + +.leaflet-control-zoom a { + background: #2a2a2a; + color: #e0e0e0; + border-color: #3a3a3a; +} + +.leaflet-control-zoom a:hover { + background: #3a3a3a; + color: #fff; +} + +.leaflet-bar { + border: 1px solid #3a3a3a; +} + +/* Coordinates display */ +.coordinates-display { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + background: rgba(42, 42, 42, 0.95); + color: #e0e0e0; + padding: 8px 16px; + border-radius: 6px; + font-size: 14px; + font-family: 'Courier New', monospace; + z-index: 1000; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); + pointer-events: none; +} + +#coord-text { + color: #8b5cf6; + font-weight: bold; +} + +/* Responsive */ +@media (max-width: 768px) { + .sidebar { + width: 280px; + } + + .sidebar.collapsed { + margin-left: -280px; + } +} diff --git a/cursebreaker-parser/Cargo.toml b/cursebreaker-parser/Cargo.toml index 2013694..69e8b5e 100644 --- a/cursebreaker-parser/Cargo.toml +++ b/cursebreaker-parser/Cargo.toml @@ -22,7 +22,7 @@ path = "src/bin/xml-parser.rs" name = "scene-parser" path = "src/bin/scene-parser.rs" -# Image Parser - processes minimap tiles +# Image Parser - processes minimap tiles and generates all zoom levels [[bin]] name = "image-parser" path = "src/bin/image-parser.rs" @@ -41,6 +41,7 @@ libsqlite3-sys = { version = ">=0.17.2", features = ["bundled"] } image = "0.25" webp = "0.3" thiserror = "1.0" +chrono = "0.4" [dev-dependencies] diesel_migrations = "2.2" diff --git a/cursebreaker-parser/README.md b/cursebreaker-parser/README.md index 3f89418..514d9d9 100644 --- a/cursebreaker-parser/README.md +++ b/cursebreaker-parser/README.md @@ -67,6 +67,18 @@ cargo build --release --bin image-parser The compiled binaries will be in `target/release/`. +## Configuration + +### Environment Variables + +Set the `CB_ASSETS_PATH` environment variable to the path of your CurseBreaker assets directory: + +```bash +export CB_ASSETS_PATH="/path/to/CBAssets" +``` + +If not set, the default fallback is `/home/connor/repos/CBAssets`. + ## Usage ### Loading Items from XML diff --git a/cursebreaker-parser/examples/fast_travel_example.rs b/cursebreaker-parser/examples/fast_travel_example.rs index 0791625..f74152c 100644 --- a/cursebreaker-parser/examples/fast_travel_example.rs +++ b/cursebreaker-parser/examples/fast_travel_example.rs @@ -1,8 +1,10 @@ use cursebreaker_parser::{FastTravelDatabase, FastTravelType}; +use std::env; fn main() -> Result<(), Box> { // Load all fast travel types from the directory - let ft_db = FastTravelDatabase::load_from_directory("/home/connor/repos/CBAssets/Data/XMLs")?; + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + let ft_db = FastTravelDatabase::load_from_directory(&format!("{}/Data/XMLs", cb_assets_path))?; println!("=== Fast Travel Database Statistics ==="); println!("Total locations: {}", ft_db.len()); diff --git a/cursebreaker-parser/examples/game_data_demo.rs b/cursebreaker-parser/examples/game_data_demo.rs index 8b2d24f..2c9120b 100644 --- a/cursebreaker-parser/examples/game_data_demo.rs +++ b/cursebreaker-parser/examples/game_data_demo.rs @@ -3,17 +3,19 @@ //! Run with: cargo run --example game_data_demo use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase}; +use std::env; fn main() -> Result<(), Box> { println!("🎮 Cursebreaker Game Data Demo\n"); // Load all game data println!("📚 Loading game data..."); - let item_db = ItemDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml")?; - let npc_db = NpcDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml")?; - let quest_db = QuestDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml")?; - let harvestable_db = HarvestableDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml")?; - let loot_db = LootDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Loot/Loot.xml")?; + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + let item_db = ItemDatabase::load_from_xml(&format!("{}/Data/XMLs/Items/Items.xml", cb_assets_path))?; + let npc_db = NpcDatabase::load_from_xml(&format!("{}/Data/XMLs/Npcs/NPCInfo.xml", cb_assets_path))?; + let quest_db = QuestDatabase::load_from_xml(&format!("{}/Data/XMLs/Quests/Quests.xml", cb_assets_path))?; + let harvestable_db = HarvestableDatabase::load_from_xml(&format!("{}/Data/XMLs/Harvestables/HarvestableInfo.xml", cb_assets_path))?; + let loot_db = LootDatabase::load_from_xml(&format!("{}/Data/XMLs/Loot/Loot.xml", cb_assets_path))?; println!("✅ Loaded {} items", item_db.len()); println!("✅ Loaded {} NPCs", npc_db.len()); diff --git a/cursebreaker-parser/examples/item_database_demo.rs b/cursebreaker-parser/examples/item_database_demo.rs index 11f41b3..a18d95c 100644 --- a/cursebreaker-parser/examples/item_database_demo.rs +++ b/cursebreaker-parser/examples/item_database_demo.rs @@ -3,12 +3,14 @@ //! Run with: cargo run --example item_database_demo use cursebreaker_parser::ItemDatabase; +use std::env; fn main() -> Result<(), Box> { println!("🎮 Cursebreaker Item Database Demo\n"); // Load items from XML - let items_path = "/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml"; + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + let items_path = format!("{}/Data/XMLs/Items/Items.xml", cb_assets_path); println!("📚 Loading items from: {}", items_path); let item_db = ItemDatabase::load_from_xml(items_path)?; diff --git a/cursebreaker-parser/examples/maps_example.rs b/cursebreaker-parser/examples/maps_example.rs index 1421a72..6aea9d5 100644 --- a/cursebreaker-parser/examples/maps_example.rs +++ b/cursebreaker-parser/examples/maps_example.rs @@ -1,8 +1,10 @@ use cursebreaker_parser::MapDatabase; +use std::env; fn main() -> Result<(), Box> { // Load the Maps.xml file - let map_db = MapDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Maps/Maps.xml")?; + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + let map_db = MapDatabase::load_from_xml(&format!("{}/Data/XMLs/Maps/Maps.xml", cb_assets_path))?; println!("=== Map Database Statistics ==="); println!("Total maps loaded: {}", map_db.len()); diff --git a/cursebreaker-parser/examples/player_houses_example.rs b/cursebreaker-parser/examples/player_houses_example.rs index d5ca13e..510d959 100644 --- a/cursebreaker-parser/examples/player_houses_example.rs +++ b/cursebreaker-parser/examples/player_houses_example.rs @@ -1,9 +1,11 @@ use cursebreaker_parser::PlayerHouseDatabase; +use std::env; fn main() -> Result<(), Box> { // Load all player houses from XML + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); let ph_db = PlayerHouseDatabase::load_from_xml( - "/home/connor/repos/CBAssets/Data/XMLs/PlayerHouses/PlayerHouses.xml", + &format!("{}/Data/XMLs/PlayerHouses/PlayerHouses.xml", cb_assets_path), )?; println!("=== Player House Database Statistics ==="); diff --git a/cursebreaker-parser/examples/shops_example.rs b/cursebreaker-parser/examples/shops_example.rs index 419b1ce..8497d03 100644 --- a/cursebreaker-parser/examples/shops_example.rs +++ b/cursebreaker-parser/examples/shops_example.rs @@ -1,9 +1,11 @@ use cursebreaker_parser::ShopDatabase; +use std::env; fn main() -> Result<(), Box> { // Load all shops from XML + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); let shop_db = ShopDatabase::load_from_xml( - "/home/connor/repos/CBAssets/Data/XMLs/Shops/Shops.xml", + &format!("{}/Data/XMLs/Shops/Shops.xml", cb_assets_path), )?; println!("=== Shop Database Statistics ==="); diff --git a/cursebreaker-parser/examples/traits_example.rs b/cursebreaker-parser/examples/traits_example.rs index ed8dbf8..eb57953 100644 --- a/cursebreaker-parser/examples/traits_example.rs +++ b/cursebreaker-parser/examples/traits_example.rs @@ -1,9 +1,11 @@ use cursebreaker_parser::TraitDatabase; +use std::env; fn main() -> Result<(), Box> { // Load all traits from XML + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); let trait_db = TraitDatabase::load_from_xml( - "/home/connor/repos/CBAssets/Data/XMLs/Traits/Traits.xml", + &format!("{}/Data/XMLs/Traits/Traits.xml", cb_assets_path), )?; println!("=== Trait Database Statistics ==="); diff --git a/cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/down.sql b/cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/down.sql new file mode 100644 index 0000000..d39cdf6 --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS idx_merged_tiles_zoom_coords; +DROP TABLE IF EXISTS merged_tiles; diff --git a/cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/up.sql b/cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/up.sql new file mode 100644 index 0000000..1dc8ee2 --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-120919-0000_create_merged_tiles/up.sql @@ -0,0 +1,31 @@ +-- Create merged_tiles table for storing merged map tiles at different zoom levels +-- Zoom level 2: 1x1 tiles (512px original tiles) +-- Zoom level 1: 2x2 tiles merged into 512px +-- Zoom level 0: 4x4 tiles merged into 512px + +CREATE TABLE merged_tiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + -- Tile coordinates at this zoom level + x INTEGER NOT NULL, + y INTEGER NOT NULL, + -- Zoom level (0 = most zoomed out, 2 = most zoomed in) + zoom_level INTEGER NOT NULL, + -- Number of original tiles merged (1, 4, or 16) + merge_factor INTEGER NOT NULL, + -- Dimensions of the merged image + width INTEGER NOT NULL, + height INTEGER NOT NULL, + -- WebP image data (lossless compression) + webp_data BLOB NOT NULL, + webp_size INTEGER NOT NULL, + -- Metadata + processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + -- Track which original tiles were merged (for debugging) + source_tiles TEXT NOT NULL, + + -- Unique constraint on zoom level + coordinates + UNIQUE(zoom_level, x, y) +); + +-- Index for fast lookups +CREATE INDEX idx_merged_tiles_zoom_coords ON merged_tiles(zoom_level, x, y); diff --git a/cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/down.sql b/cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/down.sql new file mode 100644 index 0000000..7c26dca --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/down.sql @@ -0,0 +1,26 @@ +-- This migration cannot be rolled back automatically +-- You would need to re-run the image-parser to restore data +DROP INDEX IF EXISTS idx_minimap_tiles_coords; +DROP INDEX IF EXISTS idx_minimap_tiles_zoom_coords; +DROP TABLE IF EXISTS minimap_tiles; + +-- Restore old structure (data will be lost) +CREATE TABLE minimap_tiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + x INTEGER NOT NULL, + y INTEGER NOT NULL, + original_width INTEGER NOT NULL, + original_height INTEGER NOT NULL, + original_file_size INTEGER, + webp_512 BLOB NOT NULL, + webp_256 BLOB NOT NULL, + webp_128 BLOB NOT NULL, + webp_64 BLOB NOT NULL, + webp_512_size INTEGER NOT NULL, + webp_256_size INTEGER NOT NULL, + webp_128_size INTEGER NOT NULL, + webp_64_size INTEGER NOT NULL, + processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + source_path TEXT NOT NULL, + UNIQUE(x, y) +); diff --git a/cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/up.sql b/cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/up.sql new file mode 100644 index 0000000..3bbc2ab --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-122732-0000_restructure_minimap_tiles/up.sql @@ -0,0 +1,34 @@ +-- Drop merged_tiles table (no longer needed) +DROP TABLE IF EXISTS merged_tiles; +DROP INDEX IF EXISTS idx_merged_tiles_zoom_coords; + +-- Drop old minimap_tiles table +DROP TABLE IF EXISTS minimap_tiles; + +-- Create new minimap_tiles table with simplified structure +CREATE TABLE minimap_tiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + -- Tile coordinates (at zoom level 2, original tile coords) + x INTEGER NOT NULL, + y INTEGER NOT NULL, + -- Zoom level (0 = 4x4 merged, 1 = 2x2 merged, 2 = original) + zoom INTEGER NOT NULL, + -- Image dimensions (always 512x512 for merged tiles) + width INTEGER NOT NULL, + height INTEGER NOT NULL, + -- Original file size (only for zoom=2) + original_file_size INTEGER, + -- WebP image data (lossless) + image BLOB NOT NULL, + image_size INTEGER NOT NULL, + -- Metadata + processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + source_path TEXT NOT NULL, + + -- Unique constraint on coordinates + zoom + UNIQUE(x, y, zoom) +); + +-- Index for fast lookups +CREATE INDEX idx_minimap_tiles_zoom_coords ON minimap_tiles(zoom, x, y); +CREATE INDEX idx_minimap_tiles_coords ON minimap_tiles(x, y); diff --git a/cursebreaker-parser/src/bin/image-parser.rs b/cursebreaker-parser/src/bin/image-parser.rs index 695f82e..ea52f7b 100644 --- a/cursebreaker-parser/src/bin/image-parser.rs +++ b/cursebreaker-parser/src/bin/image-parser.rs @@ -1,14 +1,17 @@ -//! Image Parser - Processes minimap tiles +//! Image Parser - Processes minimap tiles and generates all zoom levels //! //! This binary handles: -//! - Loading minimap tile images -//! - Converting PNG to WebP format -//! - Storing tiles in the SQLite database +//! - Loading minimap tile images from PNG files +//! - Converting to lossless WebP format (zoom level 2) +//! - Generating merged tiles for zoom level 1 (2x2) +//! - Generating merged tiles for zoom level 0 (4x4) +//! - Storing all tiles in the SQLite database //! - Generating statistics about storage and compression use cursebreaker_parser::MinimapDatabase; use log::{info, error, LevelFilter}; use unity_parser::log::DedupLogger; +use std::env; fn main() -> Result<(), Box> { let logger = DedupLogger::new(); @@ -17,33 +20,55 @@ fn main() -> Result<(), Box> { .unwrap(); info!("🎮 Cursebreaker - Image Parser"); + info!("Generates all zoom levels (0, 1, 2) with merged tiles\n"); // Process minimap tiles - info!("\n🗺️ Processing minimap tiles..."); + info!("🗺️ Processing minimap tiles..."); let minimap_db = MinimapDatabase::new("cursebreaker.db".to_string()); - let minimap_path = "/home/connor/repos/CBAssets/Data/Textures/MinimapSquares"; - match minimap_db.load_from_directory(minimap_path) { - Ok(count) => { - info!("✅ Processed {} minimap tiles", count); + let cb_assets_path = env::var("CB_ASSETS_PATH") + .unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + let minimap_path = format!("{}/Data/Textures/MinimapSquares", cb_assets_path); + match minimap_db.load_from_directory(&minimap_path, &cb_assets_path) { + Ok(total_count) => { + info!("\n✅ Processed {} total tiles (all zoom levels)", total_count); + + // Get statistics if let Ok(stats) = minimap_db.get_storage_stats() { - info!(" Storage Statistics:"); - info!(" • Original PNG total: {} MB", stats.total_original_size / 1_048_576); - info!(" • WebP total: {} MB", stats.total_webp_size() / 1_048_576); - info!(" • Compression ratio: {:.2}%", stats.compression_ratio()); + info!("\n=== Storage Statistics ==="); + info!("Original PNG total: {} MB", stats.total_original_size / 1_048_576); + info!("WebP total: {} MB", stats.total_webp_size() / 1_048_576); + info!("Compression ratio: {:.2}%\n", stats.compression_ratio()); + + info!("=== Tiles Per Zoom Level ==="); + info!("Zoom 2 (original): {} tiles ({} MB)", + stats.zoom2_count, + stats.zoom2_size / 1_048_576 + ); + info!("Zoom 1 (2x2 merged): {} tiles ({} MB)", + stats.zoom1_count, + stats.zoom1_size / 1_048_576 + ); + info!("Zoom 0 (4x4 merged): {} tiles ({} MB)", + stats.zoom0_count, + stats.zoom0_size / 1_048_576 + ); } if let Ok(bounds) = minimap_db.get_map_bounds() { - info!(" Map Bounds:"); - info!(" • Min (x,y): {:?}", bounds.0); - info!(" • Max (x,y): {:?}", bounds.1); + info!("\n=== Map Bounds ==="); + info!("Min (x,y): {:?}", bounds.0); + info!("Max (x,y): {:?}", bounds.1); } } Err(e) => { error!("Failed to process minimap tiles: {}", e); + return Err(Box::new(e)); } } + log::logger().flush(); + Ok(()) } diff --git a/cursebreaker-parser/src/bin/scene-parser.rs b/cursebreaker-parser/src/bin/scene-parser.rs index e14dbc4..4997bfc 100644 --- a/cursebreaker-parser/src/bin/scene-parser.rs +++ b/cursebreaker-parser/src/bin/scene-parser.rs @@ -11,6 +11,7 @@ use unity_parser::UnityProject; use std::path::Path; use unity_parser::log::DedupLogger; use log::{info, error, LevelFilter}; +use std::env; fn main() -> Result<(), Box> { let logger = DedupLogger::new(); @@ -20,8 +21,10 @@ fn main() -> Result<(), Box> { info!("🎮 Cursebreaker - Scene Parser"); + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + // Initialize Unity project once - scans entire project for GUID mappings - let project_root = Path::new("/home/connor/repos/CBAssets"); + let project_root = Path::new(&cb_assets_path); info!("\n📦 Initializing Unity project from: {}", project_root.display()); let project = UnityProject::from_path(project_root)?; diff --git a/cursebreaker-parser/src/bin/xml-parser.rs b/cursebreaker-parser/src/bin/xml-parser.rs index d9c04ab..78faa0d 100644 --- a/cursebreaker-parser/src/bin/xml-parser.rs +++ b/cursebreaker-parser/src/bin/xml-parser.rs @@ -13,6 +13,7 @@ use log::{info, warn, LevelFilter}; use unity_parser::log::DedupLogger; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; +use std::env; fn main() -> Result<(), Box> { let logger = DedupLogger::new(); @@ -23,44 +24,46 @@ fn main() -> Result<(), Box> { info!("🎮 Cursebreaker - XML Parser"); info!("📚 Loading game data from XML..."); + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + // Load items from XML - let items_path = "/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml"; + let items_path = format!("{}/Data/XMLs/Items/Items.xml", cb_assets_path); let item_db = ItemDatabase::load_from_xml(items_path)?; info!("✅ Loaded {} items", item_db.len()); - let npcs_path = "/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml"; + let npcs_path = format!("{}/Data/XMLs/Npcs/NPCInfo.xml", cb_assets_path); let npc_db = NpcDatabase::load_from_xml(npcs_path)?; info!("✅ Loaded {} NPCs", npc_db.len()); - let quests_path = "/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml"; + let quests_path = format!("{}/Data/XMLs/Quests/Quests.xml", cb_assets_path); let quest_db = QuestDatabase::load_from_xml(quests_path)?; info!("✅ Loaded {} quests", quest_db.len()); - let harvestables_path = "/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml"; + let harvestables_path = format!("{}/Data/XMLs/Harvestables/HarvestableInfo.xml", cb_assets_path); let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?; info!("✅ Loaded {} harvestables", harvestable_db.len()); - let loot_path = "/home/connor/repos/CBAssets/Data/XMLs/Loot/Loot.xml"; + let loot_path = format!("{}/Data/XMLs/Loot/Loot.xml", cb_assets_path); let loot_db = LootDatabase::load_from_xml(loot_path)?; info!("✅ Loaded {} loot tables", loot_db.len()); - let maps_path = "/home/connor/repos/CBAssets/Data/XMLs/Maps/Maps.xml"; + let maps_path = format!("{}/Data/XMLs/Maps/Maps.xml", cb_assets_path); let map_db = MapDatabase::load_from_xml(maps_path)?; info!("✅ Loaded {} maps", map_db.len()); - let fast_travel_dir = "/home/connor/repos/CBAssets/Data/XMLs"; + let fast_travel_dir = format!("{}/Data/XMLs", cb_assets_path); let fast_travel_db = FastTravelDatabase::load_from_directory(fast_travel_dir)?; info!("✅ Loaded {} fast travel locations", fast_travel_db.len()); - let player_houses_path = "/home/connor/repos/CBAssets/Data/XMLs/PlayerHouses/PlayerHouses.xml"; + let player_houses_path = format!("{}/Data/XMLs/PlayerHouses/PlayerHouses.xml", cb_assets_path); let player_house_db = PlayerHouseDatabase::load_from_xml(player_houses_path)?; info!("✅ Loaded {} player houses", player_house_db.len()); - let traits_path = "/home/connor/repos/CBAssets/Data/XMLs/Traits/Traits.xml"; + let traits_path = format!("{}/Data/XMLs/Traits/Traits.xml", cb_assets_path); let trait_db = TraitDatabase::load_from_xml(traits_path)?; info!("✅ Loaded {} traits", trait_db.len()); - let shops_path = "/home/connor/repos/CBAssets/Data/XMLs/Shops/Shops.xml"; + let shops_path = format!("{}/Data/XMLs/Shops/Shops.xml", cb_assets_path); let shop_db = ShopDatabase::load_from_xml(shops_path)?; info!("✅ Loaded {} shops", shop_db.len()); diff --git a/cursebreaker-parser/src/databases/minimap_database.rs b/cursebreaker-parser/src/databases/minimap_database.rs index 57079de..5b41fe0 100644 --- a/cursebreaker-parser/src/databases/minimap_database.rs +++ b/cursebreaker-parser/src/databases/minimap_database.rs @@ -4,6 +4,7 @@ use diesel::prelude::*; use diesel::sqlite::SqliteConnection; use std::path::{Path, PathBuf}; use std::fs; +use std::collections::HashMap; use thiserror::Error; #[derive(Debug, Error)] @@ -14,6 +15,9 @@ pub enum MinimapDatabaseError { #[error("Image processing error: {0}")] ImageError(#[from] ImageProcessingError), + #[error("Image load error: {0}")] + ImageLoadError(#[from] image::ImageError), + #[error("IO error: {0}")] IoError(#[from] std::io::Error), @@ -24,27 +28,15 @@ pub enum MinimapDatabaseError { ConnectionError(String), } -/// Database for managing minimap tiles with actual SQLite storage +/// Database for managing minimap tiles with merged zoom levels pub struct MinimapDatabase { database_url: String, - image_processor: ImageProcessor, } impl MinimapDatabase { /// Create new database connection pub fn new(database_url: String) -> Self { - Self { - database_url, - image_processor: ImageProcessor::default(), - } - } - - /// Create with custom WebP quality - pub fn with_quality(database_url: String, quality: f32) -> Self { - Self { - database_url, - image_processor: ImageProcessor::new(quality), - } + Self { database_url } } /// Establish database connection @@ -53,59 +45,198 @@ impl MinimapDatabase { .map_err(|e| MinimapDatabaseError::ConnectionError(e.to_string())) } - /// Load all PNG files from directory and process them into database - pub fn load_from_directory>( + /// Load all PNG files from directory and process them into all zoom levels + pub fn load_from_directory, B: AsRef>( &self, minimap_dir: P, + base_path: B, ) -> Result { use crate::schema::minimap_tiles; let mut conn = self.establish_connection()?; + + println!("Loading PNG files from directory..."); + let png_files = self.find_minimap_pngs(minimap_dir.as_ref())?; + println!("Found {} PNG files", png_files.len()); + + // Step 1: Process all original tiles (zoom level 2) and store their WebP data + println!("\nProcessing zoom level 2 (original tiles)..."); + let mut tile_data: HashMap<(i32, i32), Vec> = HashMap::new(); let mut count = 0; - // Find all PNG files - let png_files = self.find_minimap_pngs(&minimap_dir)?; + for png_path in &png_files { + let (x, y) = self.parse_coordinates(png_path)?; - for png_path in png_files { - // Parse coordinates from filename - let (x, y) = self.parse_coordinates(&png_path)?; - - // Process image - let processed = self.image_processor.process_minimap_png(&png_path)?; + // Load and encode as lossless WebP + let img = image::open(png_path)?; + let rgba = img.to_rgba8(); + let webp_data = ImageProcessor::encode_webp_lossless(&rgba)?; // Get original file size - let original_size = fs::metadata(&png_path)?.len() as i32; + let original_size = fs::metadata(png_path)?.len() as i32; - // Extract WebP blobs for each size - let webp_512 = processed.get(512).expect("512px resolution missing"); - let webp_256 = processed.get(256).expect("256px resolution missing"); - let webp_128 = processed.get(128).expect("128px resolution missing"); - let webp_64 = processed.get(64).expect("64px resolution missing"); - - // Create insertable record + // Store in database + let relative_path = png_path.strip_prefix(base_path.as_ref()).unwrap_or(png_path); let new_tile = NewMinimapTile { x, y, - original_width: 512, - original_height: 512, + zoom: 2, + width: 512, + height: 512, original_file_size: Some(original_size), - webp_512, - webp_256, - webp_128, - webp_64, - webp_512_size: webp_512.len() as i32, - webp_256_size: webp_256.len() as i32, - webp_128_size: webp_128.len() as i32, - webp_64_size: webp_64.len() as i32, - source_path: png_path.to_str().unwrap_or(""), + image: &webp_data, + image_size: webp_data.len() as i32, + source_path: relative_path.to_str().unwrap_or(""), }; - // Insert into database diesel::insert_into(minimap_tiles::table) .values(&new_tile) .execute(&mut conn)?; + // Cache for later merging + tile_data.insert((x, y), webp_data); count += 1; + + if count % 50 == 0 { + println!(" Processed {} tiles...", count); + } + } + + println!("Processed {} zoom level 2 tiles", count); + + // Get bounds for merging + let ((min_x, min_y), (max_x, max_y)) = self.get_map_bounds()?; + println!("\nMap bounds: X [{}, {}], Y [{}, {}]", min_x, max_x, min_y, max_y); + + // Step 2: Generate zoom level 1 (2x2 merged) + println!("\nGenerating zoom level 1 (2x2 merged)..."); + let zoom1_count = self.generate_merged_tiles( + &mut conn, + &tile_data, + min_x, + max_x, + min_y, + max_y, + 1, // zoom level + 2, // merge factor + )?; + println!("Generated {} zoom level 1 tiles", zoom1_count); + + // Step 3: Generate zoom level 0 (4x4 merged) + println!("\nGenerating zoom level 0 (4x4 merged)..."); + let zoom0_count = self.generate_merged_tiles( + &mut conn, + &tile_data, + min_x, + max_x, + min_y, + max_y, + 0, // zoom level + 4, // merge factor + )?; + println!("Generated {} zoom level 0 tiles", zoom0_count); + + println!("\nTotal tiles generated:"); + println!(" Zoom 2: {}", count); + println!(" Zoom 1: {}", zoom1_count); + println!(" Zoom 0: {}", zoom0_count); + println!(" Total: {}", count + zoom1_count + zoom0_count); + + Ok(count + zoom1_count + zoom0_count) + } + + /// Generate merged tiles for a specific zoom level + fn generate_merged_tiles( + &self, + conn: &mut SqliteConnection, + tile_data: &HashMap<(i32, i32), Vec>, + min_x: i32, + max_x: i32, + min_y: i32, + max_y: i32, + zoom_level: i32, + merge_factor: i32, + ) -> Result { + use crate::schema::minimap_tiles; + + let mut count = 0; + + // Iterate through merged tile grid + let mut merged_y = min_y; + while merged_y <= max_y { + let mut merged_x = min_x; + while merged_x <= max_x { + // Collect tiles for this merged tile + let mut tiles_for_merge: HashMap<(i32, i32), Vec> = HashMap::new(); + let mut has_any_tile = false; + + for dy in 0..merge_factor { + for dx in 0..merge_factor { + let tile_x = merged_x + dx; + let tile_y = merged_y + dy; + + if let Some(webp) = tile_data.get(&(tile_x, tile_y)) { + tiles_for_merge.insert((dx, dy), webp.clone()); + has_any_tile = true; + } + } + } + + // Only create merged tile if we have at least one source tile + if has_any_tile { + let merged_img = ImageProcessor::merge_tiles( + &tiles_for_merge, + merge_factor, + merge_factor, + 512, + 512, + )?; + + let merged_webp = ImageProcessor::encode_webp_lossless(&merged_img)?; + + // Calculate merged tile coordinates + let merged_tile_x = merged_x / merge_factor; + let merged_tile_y = merged_y / merge_factor; + + // Build source_tiles string for debugging + let mut source_coords = Vec::new(); + for dy in 0..merge_factor { + for dx in 0..merge_factor { + let tx = merged_x + dx; + let ty = merged_y + dy; + if tile_data.contains_key(&(tx, ty)) { + source_coords.push(format!("{},{}", tx, ty)); + } + } + } + let source_tiles = source_coords.join(";"); + + let new_tile = NewMinimapTile { + x: merged_tile_x, + y: merged_tile_y, + zoom: zoom_level, + width: 512, + height: 512, + original_file_size: None, + image: &merged_webp, + image_size: merged_webp.len() as i32, + source_path: &source_tiles, + }; + + diesel::insert_into(minimap_tiles::table) + .values(&new_tile) + .execute(conn)?; + + count += 1; + } + + merged_x += merge_factor; + } + merged_y += merge_factor; + + if count % 20 == 0 && count > 0 { + println!(" Generated {} merged tiles...", count); + } } Ok(count) @@ -165,55 +296,7 @@ impl MinimapDatabase { Ok((x, y)) } - /// Get tile by coordinates - pub fn get_tile( - &self, - x: i32, - y: i32, - ) -> Result, MinimapDatabaseError> { - use crate::schema::minimap_tiles::dsl; - - let mut conn = self.establish_connection()?; - - let tile = dsl::minimap_tiles - .filter(dsl::x.eq(x)) - .filter(dsl::y.eq(y)) - .first::(&mut conn) - .optional()?; - - Ok(tile) - } - - /// Get tile WebP blob at specific size - pub fn get_tile_webp( - &self, - x: i32, - y: i32, - size: u32, - ) -> Result>, MinimapDatabaseError> { - let tile = self.get_tile(x, y)?; - - Ok(tile.map(|t| match size { - 512 => t.webp_512, - 256 => t.webp_256, - 128 => t.webp_128, - 64 => t.webp_64, - _ => t.webp_512, // Default to 512 - })) - } - - /// Get all tiles - pub fn get_all_tiles(&self) -> Result, MinimapDatabaseError> { - use crate::schema::minimap_tiles::dsl::*; - - let mut conn = self.establish_connection()?; - - let tiles = minimap_tiles.load::(&mut conn)?; - - Ok(tiles) - } - - /// Get map bounds (min/max x and y) + /// Get map bounds (min/max x and y) from zoom level 2 tiles pub fn get_map_bounds( &self, ) -> Result<((i32, i32), (i32, i32)), MinimapDatabaseError> { @@ -222,30 +305,39 @@ impl MinimapDatabase { let mut conn = self.establish_connection()?; - let (min_x, max_x): (Option, Option) = - minimap_tiles.select((min(x), max(x))).first(&mut conn)?; + let (min_x_val, max_x_val): (Option, Option) = + minimap_tiles + .filter(zoom.eq(2)) + .select((min(x), max(x))) + .first(&mut conn)?; - let (min_y, max_y): (Option, Option) = - minimap_tiles.select((min(y), max(y))).first(&mut conn)?; + let (min_y_val, max_y_val): (Option, Option) = + minimap_tiles + .filter(zoom.eq(2)) + .select((min(y), max(y))) + .first(&mut conn)?; Ok(( - (min_x.unwrap_or(0), min_y.unwrap_or(0)), - (max_x.unwrap_or(0), max_y.unwrap_or(0)), + (min_x_val.unwrap_or(0), min_y_val.unwrap_or(0)), + (max_x_val.unwrap_or(0), max_y_val.unwrap_or(0)), )) } - /// Get count of processed tiles - pub fn count(&self) -> Result { + /// Get count of tiles at a specific zoom level + pub fn count_at_zoom(&self, zoom_level: i32) -> Result { use crate::schema::minimap_tiles::dsl::*; use diesel::dsl::count_star; let mut conn = self.establish_connection()?; - let total = minimap_tiles.select(count_star()).first(&mut conn)?; + let total = minimap_tiles + .filter(zoom.eq(zoom_level)) + .select(count_star()) + .first(&mut conn)?; Ok(total) } - /// Get total storage size statistics + /// Get storage statistics pub fn get_storage_stats(&self) -> Result { let mut conn = self.establish_connection()?; @@ -254,12 +346,17 @@ impl MinimapDatabase { let mut stats = StorageStats::default(); for tile in tiles { - stats.total_original_size += tile.original_file_size.unwrap_or(0) as i64; - stats.total_webp_512 += tile.webp_512_size as i64; - stats.total_webp_256 += tile.webp_256_size as i64; - stats.total_webp_128 += tile.webp_128_size as i64; - stats.total_webp_64 += tile.webp_64_size as i64; - stats.tile_count += 1; + if tile.zoom == 2 { + stats.total_original_size += tile.original_file_size.unwrap_or(0) as i64; + stats.zoom2_count += 1; + stats.zoom2_size += tile.image_size as i64; + } else if tile.zoom == 1 { + stats.zoom1_count += 1; + stats.zoom1_size += tile.image_size as i64; + } else if tile.zoom == 0 { + stats.zoom0_count += 1; + stats.zoom0_size += tile.image_size as i64; + } } Ok(stats) @@ -268,17 +365,18 @@ impl MinimapDatabase { #[derive(Debug, Default)] pub struct StorageStats { - pub tile_count: i64, + pub zoom2_count: i64, + pub zoom1_count: i64, + pub zoom0_count: i64, pub total_original_size: i64, - pub total_webp_512: i64, - pub total_webp_256: i64, - pub total_webp_128: i64, - pub total_webp_64: i64, + pub zoom2_size: i64, + pub zoom1_size: i64, + pub zoom0_size: i64, } impl StorageStats { pub fn total_webp_size(&self) -> i64 { - self.total_webp_512 + self.total_webp_256 + self.total_webp_128 + self.total_webp_64 + self.zoom2_size + self.zoom1_size + self.zoom0_size } pub fn compression_ratio(&self) -> f64 { diff --git a/cursebreaker-parser/src/image_processor.rs b/cursebreaker-parser/src/image_processor.rs index 14f952f..ebf4adc 100644 --- a/cursebreaker-parser/src/image_processor.rs +++ b/cursebreaker-parser/src/image_processor.rs @@ -259,6 +259,83 @@ impl ImageProcessor { let webp_data = encoder.encode(self.quality); Ok(webp_data.to_vec()) } + + /// Encode image to lossless WebP + pub fn encode_webp_lossless( + img: &RgbaImage, + ) -> Result, ImageProcessingError> { + let (w, h) = img.dimensions(); + let encoder = webp::Encoder::from_rgba(img.as_raw(), w, h); + let webp_data = encoder.encode_lossless(); + Ok(webp_data.to_vec()) + } + + /// Create a black tile of specified size + pub fn create_black_tile(size: u32) -> RgbaImage { + image::ImageBuffer::from_pixel(size, size, Rgba([0, 0, 0, 255])) + } + + /// Merge multiple tiles into a single image + /// + /// # Arguments + /// * `tiles` - HashMap of (x, y) coordinates to tile image data (WebP format) + /// * `grid_x` - Number of tiles in X direction + /// * `grid_y` - Number of tiles in Y direction + /// * `tile_size` - Size of each original tile (assumes square tiles) + /// * `output_size` - Size of the output merged image + /// + /// # Returns + /// A merged RgbaImage containing all tiles positioned correctly + pub fn merge_tiles( + tiles: &HashMap<(i32, i32), Vec>, + grid_x: i32, + grid_y: i32, + tile_size: u32, + output_size: u32, + ) -> Result { + // Create output image + let mut merged = Self::create_black_tile(output_size); + + // Calculate size each tile should be in the output + let scaled_tile_size = output_size / grid_x.max(grid_y) as u32; + + // Process each tile in the grid + for dy in 0..grid_y { + for dx in 0..grid_x { + if let Some(webp_data) = tiles.get(&(dx, dy)) { + // Decode WebP tile + if let Ok(tile_img) = image::load_from_memory_with_format( + webp_data, + image::ImageFormat::WebP, + ) { + // Resize tile to fit in output + let resized = tile_img.resize_exact( + scaled_tile_size, + scaled_tile_size, + image::imageops::FilterType::Lanczos3, + ).to_rgba8(); + + // Calculate position in output image + let offset_x = dx as u32 * scaled_tile_size; + // Invert Y-axis to match expected coordinate system + let offset_y = (grid_y - 1 - dy) as u32 * scaled_tile_size; + + // Copy pixels into merged image + for y in 0..scaled_tile_size { + for x in 0..scaled_tile_size { + if let Some(pixel) = resized.get_pixel_checked(x, y) { + merged.put_pixel(offset_x + x, offset_y + y, *pixel); + } + } + } + } + } + // If tile doesn't exist, it stays black (already initialized) + } + } + + Ok(merged) + } } impl Default for ImageProcessor { diff --git a/cursebreaker-parser/src/main.rs b/cursebreaker-parser/src/main.rs index ad37f57..c6960c5 100644 --- a/cursebreaker-parser/src/main.rs +++ b/cursebreaker-parser/src/main.rs @@ -13,6 +13,7 @@ use unity_parser::log::DedupLogger; use log::{info, error, warn, LevelFilter}; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; +use std::env; fn main() -> Result<(), Box> { @@ -27,43 +28,44 @@ fn main() -> Result<(), Box> { // Load items from XML info!("📚 Loading game data from XML..."); - let items_path = "/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml"; + let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string()); + let items_path = format!("{}/Data/XMLs/Items/Items.xml", cb_assets_path); let item_db = ItemDatabase::load_from_xml(items_path)?; info!("✅ Loaded {} items", item_db.len()); - let npcs_path = "/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml"; + let npcs_path = format!("{}/Data/XMLs/Npcs/NPCInfo.xml", cb_assets_path); let npc_db = NpcDatabase::load_from_xml(npcs_path)?; info!("✅ Loaded {} NPCs", npc_db.len()); - let quests_path = "/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml"; + let quests_path = format!("{}/Data/XMLs/Quests/Quests.xml", cb_assets_path); let quest_db = QuestDatabase::load_from_xml(quests_path)?; info!("✅ Loaded {} quests", quest_db.len()); - let harvestables_path = "/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml"; + let harvestables_path = format!("{}/Data/XMLs/Harvestables/HarvestableInfo.xml", cb_assets_path); let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?; info!("✅ Loaded {} harvestables", harvestable_db.len()); - let loot_path = "/home/connor/repos/CBAssets/Data/XMLs/Loot/Loot.xml"; + let loot_path = format!("{}/Data/XMLs/Loot/Loot.xml", cb_assets_path); let loot_db = LootDatabase::load_from_xml(loot_path)?; info!("✅ Loaded {} loot tables", loot_db.len()); - let maps_path = "/home/connor/repos/CBAssets/Data/XMLs/Maps/Maps.xml"; + let maps_path = format!("{}/Data/XMLs/Maps/Maps.xml", cb_assets_path); let map_db = MapDatabase::load_from_xml(maps_path)?; info!("✅ Loaded {} maps", map_db.len()); - let fast_travel_dir = "/home/connor/repos/CBAssets/Data/XMLs"; + let fast_travel_dir = format!("{}/Data/XMLs", cb_assets_path); let fast_travel_db = FastTravelDatabase::load_from_directory(fast_travel_dir)?; info!("✅ Loaded {} fast travel locations", fast_travel_db.len()); - let player_houses_path = "/home/connor/repos/CBAssets/Data/XMLs/PlayerHouses/PlayerHouses.xml"; + let player_houses_path = format!("{}/Data/XMLs/PlayerHouses/PlayerHouses.xml", cb_assets_path); let player_house_db = PlayerHouseDatabase::load_from_xml(player_houses_path)?; info!("✅ Loaded {} player houses", player_house_db.len()); - let traits_path = "/home/connor/repos/CBAssets/Data/XMLs/Traits/Traits.xml"; + let traits_path = format!("{}/Data/XMLs/Traits/Traits.xml", cb_assets_path); let trait_db = TraitDatabase::load_from_xml(traits_path)?; info!("✅ Loaded {} traits", trait_db.len()); - let shops_path = "/home/connor/repos/CBAssets/Data/XMLs/Shops/Shops.xml"; + let shops_path = format!("{}/Data/XMLs/Shops/Shops.xml", cb_assets_path); let shop_db = ShopDatabase::load_from_xml(shops_path)?; info!("✅ Loaded {} shops", shop_db.len()); @@ -145,7 +147,7 @@ fn main() -> Result<(), Box> { info!(" • Tables with conditional drops: {}", loot_db.get_conditional_tables().len()); // Initialize Unity project once - scans entire project for GUID mappings - let project_root = Path::new("/home/connor/repos/CBAssets"); + let project_root = Path::new(&cb_assets_path); info!("\n📦 Initializing Unity project from: {}", project_root.display()); let project = UnityProject::from_path(project_root)?; @@ -197,8 +199,8 @@ fn main() -> Result<(), Box> { info!("\n🗺️ Processing minimap tiles..."); let minimap_db = MinimapDatabase::new("cursebreaker.db".to_string()); - let minimap_path = "/home/connor/repos/CBAssets/Data/Textures/MinimapSquares"; - match minimap_db.load_from_directory(minimap_path) { + let minimap_path = format!("{}/Data/Textures/MinimapSquares", cb_assets_path); + match minimap_db.load_from_directory(&minimap_path, &cb_assets_path) { Ok(count) => { info!("✅ Processed {} minimap tiles", count); diff --git a/cursebreaker-parser/src/schema.rs b/cursebreaker-parser/src/schema.rs index ec8c079..7d0e5f0 100644 --- a/cursebreaker-parser/src/schema.rs +++ b/cursebreaker-parser/src/schema.rs @@ -46,17 +46,12 @@ diesel::table! { id -> Nullable, x -> Integer, y -> Integer, - original_width -> Integer, - original_height -> Integer, + zoom -> Integer, + width -> Integer, + height -> Integer, original_file_size -> Nullable, - webp_512 -> Binary, - webp_256 -> Binary, - webp_128 -> Binary, - webp_64 -> Binary, - webp_512_size -> Integer, - webp_256_size -> Integer, - webp_128_size -> Integer, - webp_64_size -> Integer, + image -> Binary, + image_size -> Integer, processed_at -> Timestamp, source_path -> Text, } diff --git a/cursebreaker-parser/src/types/cursebreaker/minimap_models.rs b/cursebreaker-parser/src/types/cursebreaker/minimap_models.rs index dc63938..6b6c3e7 100644 --- a/cursebreaker-parser/src/types/cursebreaker/minimap_models.rs +++ b/cursebreaker-parser/src/types/cursebreaker/minimap_models.rs @@ -9,17 +9,12 @@ pub struct MinimapTileRecord { pub id: Option, pub x: i32, pub y: i32, - pub original_width: i32, - pub original_height: i32, + pub zoom: i32, + pub width: i32, + pub height: i32, pub original_file_size: Option, - pub webp_512: Vec, - pub webp_256: Vec, - pub webp_128: Vec, - pub webp_64: Vec, - pub webp_512_size: i32, - pub webp_256_size: i32, - pub webp_128_size: i32, - pub webp_64_size: i32, + pub image: Vec, + pub image_size: i32, pub processed_at: String, // SQLite TIMESTAMP as String pub source_path: String, } @@ -30,16 +25,11 @@ pub struct MinimapTileRecord { pub struct NewMinimapTile<'a> { pub x: i32, pub y: i32, - pub original_width: i32, - pub original_height: i32, + pub zoom: i32, + pub width: i32, + pub height: i32, pub original_file_size: Option, - pub webp_512: &'a [u8], - pub webp_256: &'a [u8], - pub webp_128: &'a [u8], - pub webp_64: &'a [u8], - pub webp_512_size: i32, - pub webp_256_size: i32, - pub webp_128_size: i32, - pub webp_64_size: i32, + pub image: &'a [u8], + pub image_size: i32, pub source_path: &'a str, }