From 30e66d4b04968fcd54bb35cbd0dc7977ee17f093 Mon Sep 17 00:00:00 2001 From: Connor Date: Sat, 10 Jan 2026 07:44:26 +0000 Subject: [PATCH] sql database --- .claude/settings.local.json | 10 +- Cargo.lock | 945 ++++++++++++++++++ cursebreaker-parser/Cargo.toml | 8 +- .../DATABASE_MIGRATION_GUIDE.md | 273 +++++ cursebreaker-parser/diesel.toml | 9 + cursebreaker-parser/migrations/.diesel_lock | 0 cursebreaker-parser/migrations/.keep | 0 .../down.sql | 5 + .../up.sql | 39 + .../down.sql | 33 + .../up.sql | 98 ++ .../src/databases/db_helper.rs | 24 + .../src/databases/item_database.rs | 59 +- .../src/databases/minimap_database.rs | 290 ++++++ cursebreaker-parser/src/databases/mod.rs | 2 + cursebreaker-parser/src/image_processor.rs | 400 ++++++++ cursebreaker-parser/src/lib.rs | 10 + cursebreaker-parser/src/main.rs | 42 +- cursebreaker-parser/src/schema.rs | 122 +++ .../src/types/cursebreaker/minimap_models.rs | 45 + .../src/types/cursebreaker/minimap_tile.rs | 61 ++ .../src/types/cursebreaker/mod.rs | 4 + .../src/types/monobehaviours/map_icon.rs | 150 +++ .../src/types/monobehaviours/mod.rs | 2 + 24 files changed, 2625 insertions(+), 6 deletions(-) create mode 100644 cursebreaker-parser/DATABASE_MIGRATION_GUIDE.md create mode 100644 cursebreaker-parser/diesel.toml create mode 100644 cursebreaker-parser/migrations/.diesel_lock create mode 100644 cursebreaker-parser/migrations/.keep create mode 100644 cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/down.sql create mode 100644 cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/up.sql create mode 100644 cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/down.sql create mode 100644 cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/up.sql create mode 100644 cursebreaker-parser/src/databases/db_helper.rs create mode 100644 cursebreaker-parser/src/databases/minimap_database.rs create mode 100644 cursebreaker-parser/src/image_processor.rs create mode 100644 cursebreaker-parser/src/schema.rs create mode 100644 cursebreaker-parser/src/types/cursebreaker/minimap_models.rs create mode 100644 cursebreaker-parser/src/types/cursebreaker/minimap_tile.rs create mode 100644 cursebreaker-parser/src/types/monobehaviours/map_icon.rs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8e2201b..2169335 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,15 @@ "Bash(xargs dirname:*)", "Bash(xargs -I {} find {} -name \"*.cs\")", "Bash(RUST_LOG=debug cargo run:*)", - "WebSearch" + "WebSearch", + "Bash(cargo search:*)", + "Bash(cargo install:*)", + "Bash(diesel setup:*)", + "Bash(diesel migration generate:*)", + "Bash(diesel migration run:*)", + "Bash(sqlite3:*)", + "Bash(diesel migration redo:*)", + "Bash(tree:*)" ], "additionalDirectories": [ "/home/connor/repos/CBAssets/" diff --git a/Cargo.lock b/Cargo.lock index 58a9a83..38e33b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,24 +17,168 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "atomic_refcell" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "cc" version = "1.2.51" @@ -36,6 +186,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -45,12 +197,70 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "cursebreaker-parser" version = "0.1.0" dependencies = [ "diesel", + "diesel_migrations", + "image", "inventory", + "libsqlite3-sys", "log", "quick-xml", "serde", @@ -59,6 +269,7 @@ dependencies = [ "sparsey", "thiserror 1.0.69", "unity-parser", + "webp", ] [[package]] @@ -131,6 +342,17 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel_migrations" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745fd255645f0f1135f9ec55c7b00e0882192af9683ab4731e4bba3da82b8f9c" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + [[package]] name = "diesel_table_macro_syntax" version = "0.3.0" @@ -172,18 +394,92 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -202,6 +498,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "glam" version = "0.29.3" @@ -211,6 +529,23 @@ dependencies = [ "serde", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -243,6 +578,46 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.0", + "zune-jpeg 0.5.8", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "2.12.1" @@ -255,6 +630,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "inventory" version = "0.3.21" @@ -264,12 +650,31 @@ dependencies = [ "rustversion", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "js-sys" version = "0.3.83" @@ -280,22 +685,64 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libsqlite3-sys" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" dependencies = [ + "cc", "pkg-config", "vcpkg", ] +[[package]] +name = "libwebp-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733" +dependencies = [ + "cc", + "glob", +] + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.12.5" @@ -305,36 +752,192 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "migrations_internals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -354,6 +957,49 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -372,6 +1018,111 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.17", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.12.2" @@ -401,6 +1152,12 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -471,6 +1228,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -490,6 +1256,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -520,6 +1301,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -577,6 +1364,20 @@ dependencies = [ "syn", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + [[package]] name = "time" version = "0.3.44" @@ -608,6 +1409,37 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -650,6 +1482,17 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -666,6 +1509,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.106" @@ -711,6 +1563,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c071456adef4aca59bf6a583c46b90ff5eb0b4f758fc347cea81290288f37ce1" +dependencies = [ + "image", + "libwebp-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi-util" version = "0.1.11" @@ -735,14 +1603,91 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" +dependencies = [ + "zune-core 0.5.0", +] diff --git a/cursebreaker-parser/Cargo.toml b/cursebreaker-parser/Cargo.toml index 5178794..1f32964 100644 --- a/cursebreaker-parser/Cargo.toml +++ b/cursebreaker-parser/Cargo.toml @@ -20,5 +20,11 @@ log = { version = "0.4", features = ["std"] } quick-xml = "0.37" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -diesel = { version = "2.2", features = ["sqlite"], optional = true } +diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } +libsqlite3-sys = { version = ">=0.17.2", features = ["bundled"] } +image = "0.25" +webp = "0.3" thiserror = "1.0" + +[dev-dependencies] +diesel_migrations = "2.2" diff --git a/cursebreaker-parser/DATABASE_MIGRATION_GUIDE.md b/cursebreaker-parser/DATABASE_MIGRATION_GUIDE.md new file mode 100644 index 0000000..11eb0da --- /dev/null +++ b/cursebreaker-parser/DATABASE_MIGRATION_GUIDE.md @@ -0,0 +1,273 @@ +# Database Migration Guide + +This guide shows how to update all databases to use actual SQL storage with Diesel instead of just `prepare_for_sql()`. + +## Status + +✅ **Completed**: ItemDatabase +✅ **Completed**: Database tables created (migration) +✅ **Completed**: Main.rs integration example + +⏳ **Remaining**: 9 databases need the same updates + +## Pattern to Follow + +For each database file in `src/databases/`, follow this pattern (using ItemDatabase as the reference): + +### Step 1: Add Diesel Imports + +At the top of the file, add: +```rust +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; +``` + +### Step 2: Add `save_to_db()` Method + +Replace or add after the `prepare_for_sql()` method: + +```rust +/// Save all [items/npcs/quests/etc] to SQLite database +pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::TABLE_NAME; // Replace TABLE_NAME + + let records: Vec<_> = self + .ITEMS_FIELD // Replace with actual field name (e.g., items, npcs, quests) + .iter() + .map(|item| { + let json = serde_json::to_string(item).unwrap_or_else(|_| "{}".to_string()); + ( + TABLE_NAME::id.eq(item.ID_FIELD), // Replace ID_FIELD + TABLE_NAME::name.eq(&item.NAME_FIELD), // Replace NAME_FIELD + TABLE_NAME::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(TABLE_NAME::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) +} +``` + +### Step 3: Add `load_from_db()` Method + +```rust +/// Load all [items/npcs/quests/etc] from SQLite database +pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::TABLE_NAME::dsl::*; // Replace TABLE_NAME + + #[derive(Queryable)] + struct Record { + id: Option, // Or Option for text keys + name: String, // Adjust based on schema + data: String, + } + + let records = TABLE_NAME.load::(conn)?; // Replace TABLE_NAME + + let mut loaded_items = Vec::new(); + for record in records { + if let Ok(item) = serde_json::from_str::(&record.data) { // Replace TYPE + loaded_items.push(item); + } + } + + let mut db = Self::new(); + db.add_ITEMS(loaded_items); // Replace add_ITEMS with actual method + Ok(db) +} +``` + +### Step 4: Mark `prepare_for_sql()` as Deprecated + +```rust +#[deprecated(note = "Use save_to_db() to save directly to SQLite database")] +pub fn prepare_for_sql(&self) -> Vec<...> { + // existing implementation +} +``` + +## Database-Specific Mappings + +### Simple Databases (id: i32, name: String, data: String) + +| Database | Table | Items Field | ID Field | Name Field | Type | +|----------|-------|-------------|----------|------------|------| +| NpcDatabase | `npcs` | `npcs` | `type_id` | `npc_name` | `Npc` | +| QuestDatabase | `quests` | `quests` | `id` | `name` | `Quest` | +| HarvestableDatabase | `harvestables` | `harvestables` | `type_id` | `name` | `Harvestable` | + +**Example for NpcDatabase:** +```rust +pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::npcs; + + let records: Vec<_> = self + .npcs + .iter() + .map(|npc| { + let json = serde_json::to_string(npc).unwrap_or_else(|_| "{}".to_string()); + ( + npcs::id.eq(npc.type_id), + npcs::name.eq(&npc.npc_name), + npcs::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(npcs::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) +} +``` + +### Text-Key Databases + +| Database | Table | Primary Key Field | Type | +|----------|-------|-------------------|------| +| LootDatabase | `loot_tables` | `table_id: String` | `LootTable` | +| MapDatabase | `maps` | `scene_id: String` | `Map` | + +**Example for LootDatabase:** +```rust +pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::loot_tables; + + let records: Vec<_> = self + .loot_tables // Check actual field name + .iter() + .map(|loot| { + let json = serde_json::to_string(loot).unwrap_or_else(|_| "{}".to_string()); + ( + loot_tables::table_id.eq(&loot.table_id), + loot_tables::npc_id.eq(loot.npc_id.as_ref()), // Optional field + loot_tables::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(loot_tables::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) +} +``` + +### Complex Databases (Multiple Columns) + +| Database | Table | Additional Columns | Notes | +|----------|-------|-------------------|-------| +| FastTravelDatabase | `fast_travel_locations` | `map_name: String` | Has map reference | +| PlayerHouseDatabase | `player_houses` | `map_id: i32` | Has map ID | +| TraitDatabase | `traits` | `description: Option`, `trainer_id: Option` | Multiple optional fields | +| ShopDatabase | `shops` | `unique_items: bool`, `item_count: usize` | Has metadata columns | + +**Example for ShopDatabase:** +```rust +pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::shops; + + let records: Vec<_> = self + .shops + .iter() + .map(|shop| { + let json = serde_json::to_string(shop).unwrap_or_else(|_| "{}".to_string()); + ( + shops::id.eq(shop.id), + shops::name.eq(&shop.name), + shops::unique_items.eq(if shop.unique_items { 1 } else { 0 }), + shops::item_count.eq(shop.items.len() as i32), + shops::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(shops::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) +} +``` + +## Usage in main.rs + +After loading all databases from XML, save them to SQL: + +```rust +// Establish database connection +let mut conn = SqliteConnection::establish("cursebreaker.db")?; + +// Save each database +match item_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} items to database", count), + Err(e) => warn!("⚠️ Failed to save items: {}", e), +} + +match npc_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} NPCs to database", count), + Err(e) => warn!("⚠️ Failed to save NPCs: {}", e), +} + +// ... repeat for all databases +``` + +## Testing + +After implementing for each database: + +1. **Build**: `cargo build` - Should compile without errors +2. **Run**: `cargo run` - Should show save confirmations +3. **Verify**: Check `cursebreaker.db` contains data + +## Implementation Order Recommendation + +1. ✅ ItemDatabase (DONE) +2. NpcDatabase (simple, same as items) +3. QuestDatabase (simple, same as items) +4. HarvestableDatabase (simple, same as items) +5. MapDatabase (text key, medium) +6. LootDatabase (text key with optional field, medium) +7. FastTravelDatabase (multiple columns, complex) +8. PlayerHouseDatabase (multiple columns, complex) +9. TraitDatabase (optional columns, complex) +10. ShopDatabase (boolean + count columns, complex) + +## Schema Reference + +The migration created these tables (see `src/schema.rs`): + +- `items(id, name, data)` +- `npcs(id, name, data)` +- `quests(id, name, data)` +- `harvestables(id, name, data)` +- `loot_tables(table_id, npc_id, data)` +- `maps(scene_id, name, data)` +- `fast_travel_locations(id, name, map_name, data)` +- `player_houses(id, name, map_id, data)` +- `traits(id, name, description, trainer_id, data)` +- `shops(id, name, unique_items, item_count, data)` + +All `data` columns store the full JSON-serialized object for complete data preservation. diff --git a/cursebreaker-parser/diesel.toml b/cursebreaker-parser/diesel.toml new file mode 100644 index 0000000..a0d61bf --- /dev/null +++ b/cursebreaker-parser/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "migrations" diff --git a/cursebreaker-parser/migrations/.diesel_lock b/cursebreaker-parser/migrations/.diesel_lock new file mode 100644 index 0000000..e69de29 diff --git a/cursebreaker-parser/migrations/.keep b/cursebreaker-parser/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/down.sql b/cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/down.sql new file mode 100644 index 0000000..64eeada --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/down.sql @@ -0,0 +1,5 @@ +-- Rollback migration for minimap_tiles table +DROP INDEX IF EXISTS idx_minimap_y; +DROP INDEX IF EXISTS idx_minimap_x; +DROP INDEX IF EXISTS idx_minimap_coords; +DROP TABLE IF EXISTS minimap_tiles; diff --git a/cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/up.sql b/cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/up.sql new file mode 100644 index 0000000..589f8da --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-044653-0000_create_minimap_tiles/up.sql @@ -0,0 +1,39 @@ +-- Minimap tiles table storing processed WebP images +CREATE TABLE minimap_tiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + -- Tile coordinates (matching file naming: x_y.png) + x INTEGER NOT NULL, + y INTEGER NOT NULL, + + -- Original PNG metadata + original_width INTEGER NOT NULL DEFAULT 512, + original_height INTEGER NOT NULL DEFAULT 512, + original_file_size INTEGER, + + -- WebP blobs at different resolutions + webp_512 BLOB NOT NULL, -- 512x512 WebP + webp_256 BLOB NOT NULL, -- 256x256 WebP + webp_128 BLOB NOT NULL, -- 128x128 WebP + webp_64 BLOB NOT NULL, -- 64x64 WebP + + -- Blob sizes for quick reference + webp_512_size INTEGER NOT NULL, + webp_256_size INTEGER NOT NULL, + webp_128_size INTEGER NOT NULL, + webp_64_size INTEGER NOT NULL, + + -- Processing metadata + processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + source_path TEXT NOT NULL, + + -- Ensure unique coordinate pairs + UNIQUE(x, y) +); + +-- Index for fast coordinate lookups +CREATE INDEX idx_minimap_coords ON minimap_tiles(x, y); + +-- Index for boundary queries +CREATE INDEX idx_minimap_x ON minimap_tiles(x); +CREATE INDEX idx_minimap_y ON minimap_tiles(y); diff --git a/cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/down.sql b/cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/down.sql new file mode 100644 index 0000000..152d0dd --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/down.sql @@ -0,0 +1,33 @@ +-- Drop all game data tables +DROP INDEX IF EXISTS idx_shops_name; +DROP TABLE IF EXISTS shops; + +DROP INDEX IF EXISTS idx_traits_trainer; +DROP INDEX IF EXISTS idx_traits_name; +DROP TABLE IF EXISTS traits; + +DROP INDEX IF EXISTS idx_player_houses_map; +DROP INDEX IF EXISTS idx_player_houses_name; +DROP TABLE IF EXISTS player_houses; + +DROP INDEX IF EXISTS idx_fast_travel_map; +DROP INDEX IF EXISTS idx_fast_travel_name; +DROP TABLE IF EXISTS fast_travel_locations; + +DROP INDEX IF EXISTS idx_maps_name; +DROP TABLE IF EXISTS maps; + +DROP INDEX IF EXISTS idx_loot_npc; +DROP TABLE IF EXISTS loot_tables; + +DROP INDEX IF EXISTS idx_harvestables_name; +DROP TABLE IF EXISTS harvestables; + +DROP INDEX IF EXISTS idx_quests_name; +DROP TABLE IF EXISTS quests; + +DROP INDEX IF EXISTS idx_npcs_name; +DROP TABLE IF EXISTS npcs; + +DROP INDEX IF EXISTS idx_items_name; +DROP TABLE IF EXISTS items; diff --git a/cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/up.sql b/cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/up.sql new file mode 100644 index 0000000..255b780 --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-10-051843-0000_create_game_data_tables/up.sql @@ -0,0 +1,98 @@ +-- Items table +CREATE TABLE items ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_items_name ON items(name); + +-- NPCs table +CREATE TABLE npcs ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_npcs_name ON npcs(name); + +-- Quests table +CREATE TABLE quests ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_quests_name ON quests(name); + +-- Harvestables table +CREATE TABLE harvestables ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_harvestables_name ON harvestables(name); + +-- Loot tables +CREATE TABLE loot_tables ( + table_id TEXT PRIMARY KEY, + npc_id TEXT, + data TEXT NOT NULL +); + +CREATE INDEX idx_loot_npc ON loot_tables(npc_id); + +-- Maps table +CREATE TABLE maps ( + scene_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_maps_name ON maps(name); + +-- Fast travel locations table +CREATE TABLE fast_travel_locations ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + map_name TEXT NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_fast_travel_name ON fast_travel_locations(name); +CREATE INDEX idx_fast_travel_map ON fast_travel_locations(map_name); + +-- Player houses table +CREATE TABLE player_houses ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + map_id INTEGER NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_player_houses_name ON player_houses(name); +CREATE INDEX idx_player_houses_map ON player_houses(map_id); + +-- Traits table +CREATE TABLE traits ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + trainer_id INTEGER, + data TEXT NOT NULL +); + +CREATE INDEX idx_traits_name ON traits(name); +CREATE INDEX idx_traits_trainer ON traits(trainer_id); + +-- Shops table +CREATE TABLE shops ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + unique_items INTEGER NOT NULL, -- boolean as 0/1 + item_count INTEGER NOT NULL, + data TEXT NOT NULL +); + +CREATE INDEX idx_shops_name ON shops(name); diff --git a/cursebreaker-parser/src/databases/db_helper.rs b/cursebreaker-parser/src/databases/db_helper.rs new file mode 100644 index 0000000..cbf499c --- /dev/null +++ b/cursebreaker-parser/src/databases/db_helper.rs @@ -0,0 +1,24 @@ +/// Helper module for database persistence operations +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; + +/// Establish a database connection +pub fn establish_connection(database_url: &str) -> Result { + SqliteConnection::establish(database_url) +} + +/// Generic record for simple id/name/data pattern +#[derive(Queryable)] +pub struct SimpleRecord { + pub id: Option, + pub name: String, + pub data: String, +} + +/// Generic record for text-based primary keys +#[derive(Queryable)] +pub struct TextKeyRecord { + pub key: Option, + pub secondary: Option, + pub data: String, +} diff --git a/cursebreaker-parser/src/databases/item_database.rs b/cursebreaker-parser/src/databases/item_database.rs index 33feac6..092a4ec 100644 --- a/cursebreaker-parser/src/databases/item_database.rs +++ b/cursebreaker-parser/src/databases/item_database.rs @@ -3,6 +3,8 @@ use crate::item_loader::{ }; use crate::types::Item; use crate::xml_parser::{parse_items_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::{HashMap, HashSet}; use std::path::Path; @@ -199,8 +201,8 @@ impl ItemDatabase { serde_json::to_string(&self.items) } - /// Prepare items for SQL insertion - /// Returns a vector of tuples (id, name, json_data) + /// Prepare items for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { self.items .iter() @@ -210,6 +212,59 @@ impl ItemDatabase { }) .collect() } + + /// Save all items to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::items; + + let records: Vec<_> = self + .items + .iter() + .map(|item| { + let json = serde_json::to_string(item).unwrap_or_else(|_| "{}".to_string()); + ( + items::id.eq(item.type_id), + items::name.eq(&item.item_name), + items::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(items::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all items from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::items::dsl::*; + + #[derive(Queryable)] + struct ItemRecord { + id: Option, + name: String, + data: String, + } + + let records = items.load::(conn)?; + + let mut loaded_items = Vec::new(); + for record in records { + if let Ok(item) = serde_json::from_str::(&record.data) { + loaded_items.push(item); + } + } + + let mut db = Self::new(); + db.add_items(loaded_items); + Ok(db) + } } impl Default for ItemDatabase { diff --git a/cursebreaker-parser/src/databases/minimap_database.rs b/cursebreaker-parser/src/databases/minimap_database.rs new file mode 100644 index 0000000..57079de --- /dev/null +++ b/cursebreaker-parser/src/databases/minimap_database.rs @@ -0,0 +1,290 @@ +use crate::types::{MinimapTileRecord, NewMinimapTile}; +use crate::image_processor::{ImageProcessor, ImageProcessingError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; +use std::path::{Path, PathBuf}; +use std::fs; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MinimapDatabaseError { + #[error("Database error: {0}")] + DatabaseError(#[from] diesel::result::Error), + + #[error("Image processing error: {0}")] + ImageError(#[from] ImageProcessingError), + + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Invalid filename format: {0}")] + InvalidFilename(String), + + #[error("Connection pool error: {0}")] + ConnectionError(String), +} + +/// Database for managing minimap tiles with actual SQLite storage +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), + } + } + + /// Establish database connection + fn establish_connection(&self) -> Result { + SqliteConnection::establish(&self.database_url) + .map_err(|e| MinimapDatabaseError::ConnectionError(e.to_string())) + } + + /// Load all PNG files from directory and process them into database + pub fn load_from_directory>( + &self, + minimap_dir: P, + ) -> Result { + use crate::schema::minimap_tiles; + + let mut conn = self.establish_connection()?; + let mut count = 0; + + // Find all PNG files + let png_files = self.find_minimap_pngs(&minimap_dir)?; + + 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)?; + + // Get original file size + 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 + let new_tile = NewMinimapTile { + x, + y, + original_width: 512, + original_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(""), + }; + + // Insert into database + diesel::insert_into(minimap_tiles::table) + .values(&new_tile) + .execute(&mut conn)?; + + count += 1; + } + + Ok(count) + } + + /// Find all minimap PNG files in directory + fn find_minimap_pngs>( + &self, + dir: P, + ) -> Result, MinimapDatabaseError> { + let mut png_files = Vec::new(); + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("png") { + // Check if filename matches x_y.png pattern + if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { + if stem.contains('_') && stem.chars().all(|c| c.is_numeric() || c == '_' || c == '-') { + png_files.push(path); + } + } + } + } + + Ok(png_files) + } + + /// Parse x,y coordinates from filename (e.g., "0_0.png" -> (0, 0)) + fn parse_coordinates>( + &self, + path: P, + ) -> Result<(i32, i32), MinimapDatabaseError> { + let filename = path + .as_ref() + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| { + MinimapDatabaseError::InvalidFilename(path.as_ref().display().to_string()) + })?; + + let parts: Vec<&str> = filename.split('_').collect(); + if parts.len() != 2 { + return Err(MinimapDatabaseError::InvalidFilename( + filename.to_string(), + )); + } + + let x = parts[0].parse::().map_err(|_| { + MinimapDatabaseError::InvalidFilename(filename.to_string()) + })?; + let y = parts[1].parse::().map_err(|_| { + MinimapDatabaseError::InvalidFilename(filename.to_string()) + })?; + + 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) + pub fn get_map_bounds( + &self, + ) -> Result<((i32, i32), (i32, i32)), MinimapDatabaseError> { + use crate::schema::minimap_tiles::dsl::*; + use diesel::dsl::{max, min}; + + 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_y, max_y): (Option, Option) = + minimap_tiles.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)), + )) + } + + /// Get count of processed tiles + pub fn count(&self) -> 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)?; + + Ok(total) + } + + /// Get total storage size statistics + pub fn get_storage_stats(&self) -> Result { + let mut conn = self.establish_connection()?; + + use crate::schema::minimap_tiles::dsl::*; + let tiles = minimap_tiles.load::(&mut conn)?; + + 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; + } + + Ok(stats) + } +} + +#[derive(Debug, Default)] +pub struct StorageStats { + pub tile_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, +} + +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 + } + + pub fn compression_ratio(&self) -> f64 { + if self.total_original_size == 0 { + return 0.0; + } + (self.total_webp_size() as f64 / self.total_original_size as f64) * 100.0 + } +} diff --git a/cursebreaker-parser/src/databases/mod.rs b/cursebreaker-parser/src/databases/mod.rs index 0924acf..9d40b9b 100644 --- a/cursebreaker-parser/src/databases/mod.rs +++ b/cursebreaker-parser/src/databases/mod.rs @@ -8,6 +8,7 @@ mod fast_travel_database; mod player_house_database; mod trait_database; mod shop_database; +mod minimap_database; pub use item_database::ItemDatabase; pub use npc_database::NpcDatabase; @@ -19,3 +20,4 @@ pub use fast_travel_database::FastTravelDatabase; pub use player_house_database::PlayerHouseDatabase; pub use trait_database::TraitDatabase; pub use shop_database::ShopDatabase; +pub use minimap_database::{MinimapDatabase, MinimapDatabaseError, StorageStats}; diff --git a/cursebreaker-parser/src/image_processor.rs b/cursebreaker-parser/src/image_processor.rs new file mode 100644 index 0000000..14f952f --- /dev/null +++ b/cursebreaker-parser/src/image_processor.rs @@ -0,0 +1,400 @@ +use image::{DynamicImage, ImageError, Rgba, RgbaImage}; +use std::collections::HashMap; +use std::path::Path; +use thiserror::Error; + +/// Configuration for outline drawing on images with alpha channels +#[derive(Debug, Clone)] +pub struct OutlineConfig { + /// Outline color (RGBA) + pub color: Rgba, + /// Outline thickness in pixels + pub thickness: u32, + /// Alpha threshold for edge detection (0-255) + /// Pixels with alpha >= threshold are considered solid + pub alpha_threshold: u8, +} + +impl OutlineConfig { + /// Create new outline config with custom color and thickness + pub fn new(color: Rgba, thickness: u32) -> Self { + Self { + color, + thickness, + alpha_threshold: 128, + } + } + + /// Create outline config with white color + pub fn white(thickness: u32) -> Self { + Self::new(Rgba([255, 255, 255, 255]), thickness) + } + + /// Create outline config with black color + pub fn black(thickness: u32) -> Self { + Self::new(Rgba([0, 0, 0, 255]), thickness) + } + + /// Set alpha threshold for edge detection + pub fn with_alpha_threshold(mut self, threshold: u8) -> Self { + self.alpha_threshold = threshold; + self + } +} + +impl Default for OutlineConfig { + fn default() -> Self { + Self::white(1) + } +} + +#[derive(Debug, Error)] +pub enum ImageProcessingError { + #[error("Failed to load image: {0}")] + ImageLoadError(#[from] ImageError), + + #[error("WebP encoding failed: {0}")] + WebPError(String), + + #[error("Invalid image dimensions: expected {expected_width}x{expected_height}, got {actual_width}x{actual_height}")] + InvalidDimensions { + expected_width: u32, + expected_height: u32, + actual_width: u32, + actual_height: u32, + }, + + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("No resolutions specified")] + NoResolutions, +} + +pub struct ImageProcessor { + quality: f32, // WebP quality (0.0-100.0) +} + +impl ImageProcessor { + /// Create new processor with specified WebP quality + pub fn new(quality: f32) -> Self { + Self { quality } + } + + /// Process image and generate WebP at multiple resolutions + /// + /// # Arguments + /// * `image_path` - Path to the source image + /// * `sizes` - Slice of desired output sizes (width/height in pixels) + /// * `validate_dimensions` - Optional (width, height) to validate source image dimensions + /// * `outline` - Optional outline configuration to add edges around transparent areas + /// + /// # Returns + /// ProcessedImages containing WebP blobs for each requested size + pub fn process_image>( + &self, + image_path: P, + sizes: &[u32], + validate_dimensions: Option<(u32, u32)>, + outline: Option<&OutlineConfig>, + ) -> Result { + if sizes.is_empty() { + return Err(ImageProcessingError::NoResolutions); + } + + // Load image + let mut img = image::open(image_path.as_ref())?; + + // Validate dimensions if requested + if let Some((expected_width, expected_height)) = validate_dimensions { + if img.width() != expected_width || img.height() != expected_height { + return Err(ImageProcessingError::InvalidDimensions { + expected_width, + expected_height, + actual_width: img.width(), + actual_height: img.height(), + }); + } + } + + // Apply outline if requested + if let Some(outline_config) = outline { + img = DynamicImage::ImageRgba8(self.apply_outline(img.to_rgba8(), outline_config)); + } + + // Generate WebP for each size + let mut images = HashMap::new(); + for &size in sizes { + let webp_data = self.encode_webp(&img, size, size)?; + images.insert(size, webp_data); + } + + Ok(ProcessedImages { images }) + } + + /// Load PNG and generate 4 WebP sizes specifically for minimap tiles (512x512 source) + /// + /// Convenience method that generates 512, 256, 128, and 64 pixel versions + pub fn process_minimap_png>( + &self, + png_path: P, + ) -> Result { + self.process_image(png_path, &[512, 256, 128, 64], Some((512, 512)), None) + } + + /// Apply outline effect to image based on alpha channel edges + fn apply_outline(&self, img: RgbaImage, config: &OutlineConfig) -> RgbaImage { + let (width, height) = img.dimensions(); + + // Create a mask of edge pixels that need outline + let mut edge_mask = vec![vec![false; height as usize]; width as usize]; + + // Detect edges: pixels that are transparent but adjacent to opaque pixels + for y in 0..height { + for x in 0..width { + let pixel = img.get_pixel(x, y); + + // Skip if pixel is already opaque + if pixel[3] >= config.alpha_threshold { + continue; + } + + // Check if any neighbor is opaque (this is an edge) + let is_edge = self.has_opaque_neighbor(&img, x, y, config.alpha_threshold); + + if is_edge { + edge_mask[x as usize][y as usize] = true; + } + } + } + + // Apply outline with thickness + let thickness = config.thickness as i32; + let mut outlined = img.clone(); + + for y in 0..height { + for x in 0..width { + if edge_mask[x as usize][y as usize] { + // Draw outline in a square pattern around this edge pixel + for dy in -thickness..=thickness { + for dx in -thickness..=thickness { + let nx = x as i32 + dx; + let ny = y as i32 + dy; + + // Check bounds + if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 { + let nx = nx as u32; + let ny = ny as u32; + + let current_pixel = outlined.get_pixel(nx, ny); + + // Only draw outline on transparent pixels + if current_pixel[3] < config.alpha_threshold { + outlined.put_pixel(nx, ny, config.color); + } + } + } + } + } + } + } + + outlined + } + + /// Check if a pixel has any opaque neighbor + fn has_opaque_neighbor( + &self, + img: &RgbaImage, + x: u32, + y: u32, + alpha_threshold: u8, + ) -> bool { + let (width, height) = img.dimensions(); + + // Check 8 surrounding pixels + for dy in -1..=1 { + for dx in -1..=1 { + if dx == 0 && dy == 0 { + continue; // Skip center pixel + } + + let nx = x as i32 + dx; + let ny = y as i32 + dy; + + // Check bounds + if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 { + let neighbor = img.get_pixel(nx as u32, ny as u32); + if neighbor[3] >= alpha_threshold { + return true; + } + } + } + } + + false + } + + /// Encode image to WebP at specified dimensions + fn encode_webp( + &self, + img: &DynamicImage, + width: u32, + height: u32, + ) -> Result, ImageProcessingError> { + // Resize if dimensions don't match original + let resized = if img.width() != width || img.height() != height { + img.resize_exact(width, height, image::imageops::FilterType::Lanczos3) + } else { + img.clone() + }; + + // Convert to RGBA8 + let rgba = resized.to_rgba8(); + let (w, h) = rgba.dimensions(); + + // Encode to WebP + let encoder = webp::Encoder::from_rgba(rgba.as_raw(), w, h); + + let webp_data = encoder.encode(self.quality); + Ok(webp_data.to_vec()) + } +} + +impl Default for ImageProcessor { + fn default() -> Self { + Self::new(85.0) // 85% quality default + } +} + +/// Container for processed WebP images at multiple resolutions +#[derive(Debug)] +pub struct ProcessedImages { + /// Map of size (in pixels) to WebP blob data + pub images: HashMap>, +} + +impl ProcessedImages { + /// Get WebP blob for a specific size + pub fn get(&self, size: u32) -> Option<&Vec> { + self.images.get(&size) + } + + /// Get total size of all WebP blobs in bytes + pub fn total_size(&self) -> usize { + self.images.values().map(|v| v.len()).sum() + } + + /// Get all available sizes + pub fn sizes(&self) -> Vec { + let mut sizes: Vec = self.images.keys().copied().collect(); + sizes.sort_unstable(); + sizes + } + + /// Get number of resolutions stored + pub fn len(&self) -> usize { + self.images.len() + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.images.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_processor_creation() { + let processor = ImageProcessor::default(); + assert_eq!(processor.quality, 85.0); + + let custom = ImageProcessor::new(90.0); + assert_eq!(custom.quality, 90.0); + } + + #[test] + fn test_processed_images() { + let mut images = HashMap::new(); + images.insert(512, vec![1, 2, 3]); + images.insert(256, vec![4, 5]); + + let processed = ProcessedImages { images }; + + assert_eq!(processed.len(), 2); + assert_eq!(processed.total_size(), 5); + assert_eq!(processed.get(512), Some(&vec![1, 2, 3])); + assert_eq!(processed.get(128), None); + + let sizes = processed.sizes(); + assert_eq!(sizes, vec![256, 512]); + } + + #[test] + fn test_outline_config_default() { + let config = OutlineConfig::default(); + assert_eq!(config.thickness, 1); + assert_eq!(config.color, Rgba([255, 255, 255, 255])); // White + assert_eq!(config.alpha_threshold, 128); + } + + #[test] + fn test_outline_config_custom() { + let red = Rgba([255, 0, 0, 255]); + let config = OutlineConfig::new(red, 2); + assert_eq!(config.thickness, 2); + assert_eq!(config.color, red); + assert_eq!(config.alpha_threshold, 128); + } + + #[test] + fn test_outline_config_builders() { + let white = OutlineConfig::white(3); + assert_eq!(white.color, Rgba([255, 255, 255, 255])); + assert_eq!(white.thickness, 3); + + let black = OutlineConfig::black(2).with_alpha_threshold(200); + assert_eq!(black.color, Rgba([0, 0, 0, 255])); + assert_eq!(black.thickness, 2); + assert_eq!(black.alpha_threshold, 200); + } + + #[test] + fn test_outline_edge_detection() { + let processor = ImageProcessor::default(); + + // Create a simple 3x3 image with a transparent pixel in the center + let mut img = RgbaImage::new(3, 3); + + // Fill with opaque white + for y in 0..3 { + for x in 0..3 { + img.put_pixel(x, y, Rgba([255, 255, 255, 255])); + } + } + + // Make center transparent + img.put_pixel(1, 1, Rgba([0, 0, 0, 0])); + + // Test that center pixel has opaque neighbors + assert!(processor.has_opaque_neighbor(&img, 1, 1, 128)); + + // Test a fully opaque pixel - should not have any transparent neighbors + // but the function checks if a pixel has opaque neighbors, not transparent ones + assert!(processor.has_opaque_neighbor(&img, 0, 0, 128)); + + // Create a new image that's fully transparent + let mut transparent_img = RgbaImage::new(3, 3); + for y in 0..3 { + for x in 0..3 { + transparent_img.put_pixel(x, y, Rgba([0, 0, 0, 0])); + } + } + + // A transparent pixel with all transparent neighbors should return false + assert!(!processor.has_opaque_neighbor(&transparent_img, 1, 1, 128)); + } +} diff --git a/cursebreaker-parser/src/lib.rs b/cursebreaker-parser/src/lib.rs index 560dc57..7427fc2 100644 --- a/cursebreaker-parser/src/lib.rs +++ b/cursebreaker-parser/src/lib.rs @@ -51,8 +51,10 @@ pub mod types; pub mod databases; +pub mod schema; mod xml_parser; mod item_loader; +mod image_processor; pub use databases::{ ItemDatabase, @@ -65,6 +67,9 @@ pub use databases::{ PlayerHouseDatabase, TraitDatabase, ShopDatabase, + MinimapDatabase, + MinimapDatabaseError, + StorageStats, }; pub use types::{ // Items @@ -109,5 +114,10 @@ pub use types::{ TraitTrainer, Shop, ShopItem, + // Minimap + MinimapTile, + MinimapTileRecord, + NewMinimapTile, }; pub use xml_parser::XmlParseError; +pub use image_processor::{ImageProcessor, ImageProcessingError, ProcessedImages, OutlineConfig}; diff --git a/cursebreaker-parser/src/main.rs b/cursebreaker-parser/src/main.rs index 26b7934..02bdfba 100644 --- a/cursebreaker-parser/src/main.rs +++ b/cursebreaker-parser/src/main.rs @@ -6,11 +6,13 @@ //! 3. Extracting typeId and transform positions //! 4. Writing resource data to an output file -use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase, InteractableResource}; +use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase, InteractableResource, MinimapDatabase}; use unity_parser::UnityProject; use std::path::Path; use unity_parser::log::DedupLogger; -use log::{info, error, LevelFilter}; +use log::{info, error, warn, LevelFilter}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; fn main() -> Result<(), Box> { @@ -45,6 +47,15 @@ fn main() -> Result<(), Box> { let loot_db = LootDatabase::load_from_xml(loot_path)?; info!("✅ Loaded {} loot tables", loot_db.len()); + // Save to SQLite database + info!("\n💾 Saving game data to SQLite database..."); + let mut conn = SqliteConnection::establish("cursebreaker.db")?; + + match item_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} items to database", count), + Err(e) => warn!("⚠️ Failed to save items: {}", e), + } + // Print statistics info!("\n📊 Game Data Statistics:"); info!(" Items:"); @@ -117,5 +128,32 @@ fn main() -> Result<(), Box> { log::logger().flush(); + // Process minimap tiles + 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) { + Ok(count) => { + info!("✅ Processed {} minimap tiles", count); + + 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()); + } + + if let Ok(bounds) = minimap_db.get_map_bounds() { + info!(" Map Bounds:"); + info!(" • Min (x,y): {:?}", bounds.0); + info!(" • Max (x,y): {:?}", bounds.1); + } + } + Err(e) => { + error!("Failed to process minimap tiles: {}", e); + } + } + Ok(()) } diff --git a/cursebreaker-parser/src/schema.rs b/cursebreaker-parser/src/schema.rs new file mode 100644 index 0000000..ec8c079 --- /dev/null +++ b/cursebreaker-parser/src/schema.rs @@ -0,0 +1,122 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + fast_travel_locations (id) { + id -> Nullable, + name -> Text, + map_name -> Text, + data -> Text, + } +} + +diesel::table! { + harvestables (id) { + id -> Nullable, + name -> Text, + data -> Text, + } +} + +diesel::table! { + items (id) { + id -> Nullable, + name -> Text, + data -> Text, + } +} + +diesel::table! { + loot_tables (table_id) { + table_id -> Nullable, + npc_id -> Nullable, + data -> Text, + } +} + +diesel::table! { + maps (scene_id) { + scene_id -> Nullable, + name -> Text, + data -> Text, + } +} + +diesel::table! { + minimap_tiles (id) { + id -> Nullable, + x -> Integer, + y -> Integer, + original_width -> Integer, + original_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, + processed_at -> Timestamp, + source_path -> Text, + } +} + +diesel::table! { + npcs (id) { + id -> Nullable, + name -> Text, + data -> Text, + } +} + +diesel::table! { + player_houses (id) { + id -> Nullable, + name -> Text, + map_id -> Integer, + data -> Text, + } +} + +diesel::table! { + quests (id) { + id -> Nullable, + name -> Text, + data -> Text, + } +} + +diesel::table! { + shops (id) { + id -> Nullable, + name -> Text, + unique_items -> Integer, + item_count -> Integer, + data -> Text, + } +} + +diesel::table! { + traits (id) { + id -> Nullable, + name -> Text, + description -> Nullable, + trainer_id -> Nullable, + data -> Text, + } +} + +diesel::allow_tables_to_appear_in_same_query!( + fast_travel_locations, + harvestables, + items, + loot_tables, + maps, + minimap_tiles, + npcs, + player_houses, + quests, + shops, + traits, +); diff --git a/cursebreaker-parser/src/types/cursebreaker/minimap_models.rs b/cursebreaker-parser/src/types/cursebreaker/minimap_models.rs new file mode 100644 index 0000000..dc63938 --- /dev/null +++ b/cursebreaker-parser/src/types/cursebreaker/minimap_models.rs @@ -0,0 +1,45 @@ +use diesel::prelude::*; +use crate::schema::minimap_tiles; + +/// Diesel queryable model (for SELECT queries) +#[derive(Queryable, Selectable, Debug, Clone)] +#[diesel(table_name = minimap_tiles)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct MinimapTileRecord { + pub id: Option, + pub x: i32, + pub y: i32, + pub original_width: i32, + pub original_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 processed_at: String, // SQLite TIMESTAMP as String + pub source_path: String, +} + +/// Diesel insertable model (for INSERT queries) +#[derive(Insertable, Debug)] +#[diesel(table_name = minimap_tiles)] +pub struct NewMinimapTile<'a> { + pub x: i32, + pub y: i32, + pub original_width: i32, + pub original_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 source_path: &'a str, +} diff --git a/cursebreaker-parser/src/types/cursebreaker/minimap_tile.rs b/cursebreaker-parser/src/types/cursebreaker/minimap_tile.rs new file mode 100644 index 0000000..c1a0ac4 --- /dev/null +++ b/cursebreaker-parser/src/types/cursebreaker/minimap_tile.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +/// Represents a single minimap tile with multi-resolution WebP data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimapTile { + /// X coordinate from filename + pub x: i32, + + /// Y coordinate from filename + pub y: i32, + + /// Original dimensions + pub original_width: i32, + pub original_height: i32, + + /// Source file path + pub source_path: String, + + /// WebP blob at 512x512 + #[serde(skip)] // Skip serialization for binary data + pub webp_512: Vec, + + /// WebP blob at 256x256 + #[serde(skip)] + pub webp_256: Vec, + + /// WebP blob at 128x128 + #[serde(skip)] + pub webp_128: Vec, + + /// WebP blob at 64x64 + #[serde(skip)] + pub webp_64: Vec, +} + +impl MinimapTile { + /// Create new tile from coordinates and source path + pub fn new(x: i32, y: i32, source_path: String) -> Self { + Self { + x, + y, + original_width: 512, + original_height: 512, + source_path, + webp_512: Vec::new(), + webp_256: Vec::new(), + webp_128: Vec::new(), + webp_64: Vec::new(), + } + } + + /// Get total size of all WebP blobs + pub fn total_webp_size(&self) -> usize { + self.webp_512.len() + self.webp_256.len() + self.webp_128.len() + self.webp_64.len() + } + + /// Check if tile has been processed (has WebP data) + pub fn is_processed(&self) -> bool { + !self.webp_512.is_empty() + } +} diff --git a/cursebreaker-parser/src/types/cursebreaker/mod.rs b/cursebreaker-parser/src/types/cursebreaker/mod.rs index acdd1c8..c65f2c1 100644 --- a/cursebreaker-parser/src/types/cursebreaker/mod.rs +++ b/cursebreaker-parser/src/types/cursebreaker/mod.rs @@ -8,6 +8,8 @@ mod fast_travel; mod player_house; mod r#trait; mod shop; +mod minimap_tile; +mod minimap_models; pub use item::{ // Main types @@ -40,3 +42,5 @@ pub use fast_travel::{FastTravelLocation, FastTravelType}; pub use player_house::PlayerHouse; pub use r#trait::{Trait, TraitTrainer}; pub use shop::{Shop, ShopItem}; +pub use minimap_tile::MinimapTile; +pub use minimap_models::{MinimapTileRecord, NewMinimapTile}; diff --git a/cursebreaker-parser/src/types/monobehaviours/map_icon.rs b/cursebreaker-parser/src/types/monobehaviours/map_icon.rs new file mode 100644 index 0000000..9fe5c69 --- /dev/null +++ b/cursebreaker-parser/src/types/monobehaviours/map_icon.rs @@ -0,0 +1,150 @@ +/// MapIcon component from Cursebreaker +/// +/// C# definition from MapIcon.cs: +/// ```csharp +/// public enum MapIconType +/// { +/// npc, +/// aggressiveNpc, +/// ally, +/// loot, +/// self, +/// player, +/// buildingAlly, +/// buildingEnemy, +/// path, +/// resource, +/// questmarker, +/// workbench, +/// door, +/// tree, +/// fish, +/// custom, +/// mapText, +/// worldMapText, +/// fastTravel, +/// fightingNpc, +/// worldMapIcon, +/// playerHouse, +/// task +/// } +/// +/// public class MapIcon : MonoBehaviour +/// { +/// public UI_Minimap.MapIconType iconType = UI_Minimap.MapIconType.custom; +/// public int iconSize = 24; +/// public string icon = "MinimapIcons/"; +/// public string text; +/// public int fontSize = 24; +/// public string hoverText; +/// } +/// ``` +use unity_parser::{UnityComponent, ComponentContext, EcsInsertable}; +use serde_yaml::Mapping; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MapIconType { + Npc = 0, + AggressiveNpc = 1, + Ally = 2, + Loot = 3, + Self_ = 4, + Player = 5, + BuildingAlly = 6, + BuildingEnemy = 7, + Path = 8, + Resource = 9, + Questmarker = 10, + Workbench = 11, + Door = 12, + Tree = 13, + Fish = 14, + Custom = 15, + MapText = 16, + WorldMapText = 17, + FastTravel = 18, + FightingNpc = 19, + WorldMapIcon = 20, + PlayerHouse = 21, + Task = 22, +} + +impl Default for MapIconType { + fn default() -> Self { + MapIconType::Custom + } +} + +impl MapIconType { + pub fn from_i64(value: i64) -> Self { + match value { + 0 => MapIconType::Npc, + 1 => MapIconType::AggressiveNpc, + 2 => MapIconType::Ally, + 3 => MapIconType::Loot, + 4 => MapIconType::Self_, + 5 => MapIconType::Player, + 6 => MapIconType::BuildingAlly, + 7 => MapIconType::BuildingEnemy, + 8 => MapIconType::Path, + 9 => MapIconType::Resource, + 10 => MapIconType::Questmarker, + 11 => MapIconType::Workbench, + 12 => MapIconType::Door, + 13 => MapIconType::Tree, + 14 => MapIconType::Fish, + 15 => MapIconType::Custom, + 16 => MapIconType::MapText, + 17 => MapIconType::WorldMapText, + 18 => MapIconType::FastTravel, + 19 => MapIconType::FightingNpc, + 20 => MapIconType::WorldMapIcon, + 21 => MapIconType::PlayerHouse, + 22 => MapIconType::Task, + _ => MapIconType::Custom, + } + } +} + +#[derive(Debug, Clone)] +pub struct MapIcon { + pub icon_type: MapIconType, + pub icon_size: i64, + pub icon: String, + pub text: String, + pub font_size: i64, + pub hover_text: String, +} + +impl UnityComponent for MapIcon { + fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option { + let icon_type_value = unity_parser::yaml_helpers::get_i64(yaml, "iconType").unwrap_or(15); + + Some(Self { + icon_type: MapIconType::from_i64(icon_type_value), + icon_size: unity_parser::yaml_helpers::get_i64(yaml, "iconSize").unwrap_or(24), + icon: unity_parser::yaml_helpers::get_string(yaml, "icon").unwrap_or_else(|| "MinimapIcons/".to_string()), + text: unity_parser::yaml_helpers::get_string(yaml, "text").unwrap_or_default(), + font_size: unity_parser::yaml_helpers::get_i64(yaml, "fontSize").unwrap_or(24), + hover_text: unity_parser::yaml_helpers::get_string(yaml, "hoverText").unwrap_or_default(), + }) + } +} + +impl EcsInsertable for MapIcon { + fn insert_into_world(self, world: &mut sparsey::World, entity: sparsey::Entity) { + world.insert(entity, (self,)); + } +} + +// Register component with inventory +inventory::submit! { + unity_parser::ComponentRegistration { + type_id: 114, + class_name: "MapIcon", + parse_and_insert: |yaml, ctx, world, entity| { + ::parse_and_insert(yaml, ctx, world, entity) + }, + register: |builder| builder.register::(), + } +} diff --git a/cursebreaker-parser/src/types/monobehaviours/mod.rs b/cursebreaker-parser/src/types/monobehaviours/mod.rs index 6663559..3d9b9a9 100644 --- a/cursebreaker-parser/src/types/monobehaviours/mod.rs +++ b/cursebreaker-parser/src/types/monobehaviours/mod.rs @@ -2,10 +2,12 @@ mod interactable_resource; mod interactable_teleporter; mod interactable_workbench; mod loot_spawner; +mod map_icon; mod map_name_changer; pub use interactable_resource::InteractableResource; pub use interactable_teleporter::InteractableTeleporter; pub use interactable_workbench::InteractableWorkbench; pub use loot_spawner::LootSpawner; +pub use map_icon::{MapIcon, MapIconType}; pub use map_name_changer::MapNameChanger;