diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..240c803 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +hoi4pe_config.toml +test_map.zip diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9a61155 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2040 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fe5e32de01730eb1f6b7f5b51c17e03e2325bf40a74f754f04f130043affff" + +[[package]] +name = "addr2line" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "andrew" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" +dependencies = [ + "bitflags", + "rusttype", + "walkdir", + "xdg", + "xml-rs", +] + +[[package]] +name = "android_glue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7815ea54e4d821e791162e078acbebfd6d8c8939cd559c9335dceb1c8ca7282" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.4.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "better-panic" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d12a680cc74d8c4a44ee08be4a00dedf671b089c2440b2e3fdaa776cd468476" +dependencies = [ + "backtrace", + "console", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bytemuck" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.10+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "calloop" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" +dependencies = [ + "log", + "nix 0.18.0", +] + +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" + +[[package]] +name = "ccl-fxhash" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef30ecbc8ff94c836ef0543827b2cd691f86485392256f3e3de674f48722c06" +dependencies = [ + "byteorder", + "cfg-if 0.1.10", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clicolors-control" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e" +dependencies = [ + "atty", + "lazy_static", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "cocoa" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54201c07dcf3a5ca33fececb8042aed767ee4bfd5a0235a8ceabcda956044b2" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation 0.9.1", + "core-graphics 0.22.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation 0.9.1", + "core-graphics 0.22.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags", + "block", + "core-foundation 0.9.1", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "console" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e0f3986890b3acbc782009e2629dfe2baa430ac091519ce3be26164a2ae6c0" +dependencies = [ + "clicolors-control", + "encode_unicode", + "lazy_static", + "libc", + "regex", + "termios", + "winapi 0.3.9", +] + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys 0.8.2", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags", + "core-foundation 0.7.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269f35f69b542b80e736a20a89a05215c0ce80c2c03c514abb2e318b78379d86" +dependencies = [ + "bitflags", + "core-foundation 0.9.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags", + "core-foundation 0.9.1", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "objc", +] + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" +dependencies = [ + "libloading 0.6.7", +] + +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading 0.7.0", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide 0.4.4", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "gif" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" + +[[package]] +name = "gl" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b411c7e0bfc599e3606412c190e786b5bb48cf00073e1635f9bb6f88fe7d84a" +dependencies = [ + "gl_generator 0.13.1", +] + +[[package]] +name = "gl_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca98bbde17256e02d17336a6bdb5a50f7d0ccacee502e191d3e3d0ec2f96f84a" +dependencies = [ + "khronos_api 3.1.0", + "log", + "xml-rs", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api 3.1.0", + "log", + "xml-rs", +] + +[[package]] +name = "glutin" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae1cbb9176b9151c4ce03f012e3cd1c6c18c4be79edeaeb3d99f5d8085c5fa3" +dependencies = [ + "android_glue", + "cgl", + "cocoa 0.23.0", + "core-foundation 0.9.1", + "glutin_egl_sys", + "glutin_emscripten_sys", + "glutin_gles2_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "lazy_static", + "libloading 0.6.7", + "log", + "objc", + "osmesa-sys", + "parking_lot", + "wayland-client", + "wayland-egl", + "winapi 0.3.9", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abb6aa55523480c4adc5a56bbaa249992e2dddb2fc63dc96e04a3355364c211" +dependencies = [ + "gl_generator 0.14.0", + "winapi 0.3.9", +] + +[[package]] +name = "glutin_emscripten_sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80de4146df76e8a6c32b03007bc764ff3249dcaeb4f675d68a06caf1bac363f1" + +[[package]] +name = "glutin_gles2_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094e708b730a7c8a1954f4f8a31880af00eb8a1c5b5bf85d28a0a3c6d69103" +dependencies = [ + "gl_generator 0.14.0", + "objc", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e393c8fc02b807459410429150e9c4faffdb312d59b8c038566173c81991351" +dependencies = [ + "gl_generator 0.14.0", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5951a1569dbab865c6f2a863efafff193a93caf05538d193e9e3816d21696" +dependencies = [ + "gl_generator 0.14.0", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "hoi4_province_editor" +version = "0.1.0" +dependencies = [ + "better-panic", + "ccl-fxhash", + "glutin", + "image", + "itertools", + "lazy_static", + "native-dialog", + "piston", + "piston2d-graphics", + "piston2d-opengl_graphics", + "pistoncore-glutin_window", + "rand", + "rusttype", + "serde", + "serde_multi", + "util_macros", + "vecmath", + "zip", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "interpolation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02" + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "itertools" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +dependencies = [ + "either", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "khronos_api" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037ab472c33f67b5fbd3e9163a2645319e5356fcd355efa6d4eb7fff4bbcb554" + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" + +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "lock_api" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "memmap2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio", + "slab", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "native-dialog" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d689b01017db3e350e0e9798d233cca9ad3bf810e7c02b9b55ec06b9ee7dbd8a" +dependencies = [ + "cocoa 0.24.0", + "dirs-next", + "objc", + "objc-foundation", + "objc_id", + "once_cell", + "raw-window-handle", + "thiserror", + "wfd", + "which", + "winapi 0.3.9", +] + +[[package]] +name = "ndk" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nix" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bc1d42047cf336f0f939c99e97183cf31551bf0f2865a2ec9c8d91fd4ffb5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + +[[package]] +name = "osmesa-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88cfece6e95d2e717e0872a7f53a8684712ad13822a7979bc760b9c77ec0013b" +dependencies = [ + "shared_library", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "piston" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6db9e4eb603719bef3900670326b949b374df616540a7417190b69c9710475" +dependencies = [ + "pistoncore-event_loop", + "pistoncore-input", + "pistoncore-window", +] + +[[package]] +name = "piston-float" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f900be47e312e126cc71d35548e8e31edd3901b92ab82d1c4c4757e6b5526564" + +[[package]] +name = "piston-graphics_api_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c10c0d7a00f671e717ab98aa2c7f7bf204082d0e6b246827dc17b68bbaf8d18" + +[[package]] +name = "piston-shaders_graphics2d" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a35f4d08d2b6fd7ff02baab63346d4b7d2fdd5ac3f2e091a5128c22c77a489a" + +[[package]] +name = "piston-texture" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c93564eef40a9920d026697f63d224efd7ac80981fb418fe1dad447c2d9bdd" + +[[package]] +name = "piston-viewport" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01abb19b781051290d0837b9294c26d419cc4156907c21ffe86705e219446798" +dependencies = [ + "piston-float", +] + +[[package]] +name = "piston2d-graphics" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb104512055b3bf44fc3d8dad9af88abda16a468d048626ae9b49f634802c67" +dependencies = [ + "fnv", + "interpolation", + "piston-texture", + "piston-viewport", + "read_color", + "rusttype", + "vecmath", +] + +[[package]] +name = "piston2d-opengl_graphics" +version = "0.78.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccbb5e2364ca23e3227efa2ddfe9957c8a8ffced876271e2d9ef8d8921433fb" +dependencies = [ + "fnv", + "gl", + "image", + "khronos_api 2.2.0", + "piston-shaders_graphics2d", + "piston-texture", + "piston-viewport", + "piston2d-graphics", + "shader_version", +] + +[[package]] +name = "pistoncore-event_loop" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d55aed01d7c6f883cc06afd5349b2cb5c1243e12a47dc4d3b798121136e9e" +dependencies = [ + "pistoncore-input", + "pistoncore-window", + "spin_sleep", +] + +[[package]] +name = "pistoncore-glutin_window" +version = "0.69.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66086130e496d440aba286ba47b7d61e33413208323bec3499bf7975fa72dc2e" +dependencies = [ + "gl", + "glutin", + "pistoncore-input", + "pistoncore-window", + "shader_version", +] + +[[package]] +name = "pistoncore-input" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bb41e7bdd8213f06b2da31e67ae9469f83094d0e45122ead52158915d9af30" +dependencies = [ + "bitflags", + "piston-viewport", + "serde", + "serde_derive", +] + +[[package]] +name = "pistoncore-window" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4932383d894231cdb3fcb0d26607c63b153b808b5621d911df6b929dd53b32" +dependencies = [ + "piston-graphics_api_version", + "pistoncore-input", +] + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-window-handle" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a441a7a6c80ad6473bd4b74ec1c9a4c951794285bf941c2126f607c72e48211" +dependencies = [ + "libc", +] + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "read_color" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4c8858baa4ad3c8bcc156ae91a0ffe22b76a3975c40c49b4f04c15c6bce0da" + +[[package]] +name = "redox_syscall" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rustc-demangle" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce" + +[[package]] +name = "rusttype" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_multi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a471c760378a595e7b755a196474071c590a2c1b2c338750fa96f304817f47" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "shader_version" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfadbf7574784ee97f062ace17e1008fb5e7f46dd714b7dd46baf6efebd30e26" +dependencies = [ + "piston-graphics_api_version", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "smithay-client-toolkit" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" +dependencies = [ + "andrew", + "bitflags", + "calloop", + "dlib 0.4.2", + "lazy_static", + "log", + "memmap2", + "nix 0.18.0", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "spin_sleep" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a98101bdc3833e192713c2af0b0dd2614f50d1cf1f7a97c5221b7aac052acc7" +dependencies = [ + "once_cell", + "winapi 0.3.9", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "syn" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "ttf-parser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "util_macros" +version = "0.1.0" +source = "git+https://github.com/ScottyThePilot/util_macros#7fa43d23086a61c7ba1c0203a6c623382ca95b84" + +[[package]] +name = "vecmath" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ae1e0d85bca567dee1dcf87fb1ca2e792792f66f87dced8381f99cd91156a" +dependencies = [ + "piston-float", +] + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wayland-client" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ca44d86554b85cf449f1557edc6cc7da935cc748c8e4bf1c507cbd43bae02c" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.20.0", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd75ae380325dbcff2707f0cd9869827ea1d2d6d534cff076858d3f0460fd5a" +dependencies = [ + "nix 0.20.0", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b37e5455ec72f5de555ec39b5c3704036ac07c2ecd50d0bffe02d5fe2d4e65ab" +dependencies = [ + "nix 0.20.0", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-egl" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9461a67930ec16da7a4fd8b50e9ffa23f4417240b43ec84008bd1b2c94421c94" +dependencies = [ + "wayland-client", + "wayland-sys", +] + +[[package]] +name = "wayland-protocols" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95df3317872bcf9eec096c864b69aa4769a1d5d6291a5b513f8ba0af0efbd52c" +dependencies = [ + "bitflags", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389d680d7bd67512dc9c37f39560224327038deb0f0e8d33f870900441b68720" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2907bd297eef464a95ba9349ea771611771aa285b932526c633dc94d5400a8e2" +dependencies = [ + "dlib 0.5.0", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "weezl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" + +[[package]] +name = "wfd" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e713040b67aae5bf1a0ae3e1ebba8cc29ab2b90da9aa1bff6e09031a8a41d7a8" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "which" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" +dependencies = [ + "either", + "libc", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winit" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4eda6fce0eb84bd0a33e3c8794eb902e1033d0a1d5a31bc4f19b1b4bbff597" +dependencies = [ + "bitflags", + "cocoa 0.24.0", + "core-foundation 0.9.1", + "core-graphics 0.22.2", + "core-video-sys", + "dispatch", + "instant", + "lazy_static", + "libc", + "log", + "mio", + "mio-extras", + "ndk", + "ndk-glue", + "ndk-sys", + "objc", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "smithay-client-toolkit", + "wayland-client", + "winapi 0.3.9", + "x11-dl", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "x11-dl" +version = "2.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" +dependencies = [ + "lazy_static", + "libc", + "maybe-uninit", + "pkg-config", +] + +[[package]] +name = "xcursor" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08" +dependencies = [ + "nom", +] + +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" + +[[package]] +name = "xml-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + +[[package]] +name = "zip" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c83dc9b784d252127720168abd71ea82bf8c3d96b17dc565b5e2a02854f2b27" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1df4a44 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "hoi4_province_editor" +version = "0.1.0" +authors = ["ScottyThePilot "] +edition = "2018" +description = "Map editor application for Hearts of Iron IV" +readme = "README.md" +repository = "https://github.com/ScottyThePilot/hoi4_province_editor" +license = "MIT" + +[dependencies] +better-panic = "0.2" +ccl-fxhash = "3.0" +glutin = "0.26" +image = { version = "0.23", default-features = false, features = ["bmp"] } +itertools = "0.10" +lazy_static = "1.4" +native-dialog = "0.5" +piston = "0.53" +piston2d-graphics = { version = "0.40", features = ["glyph_cache_rusttype"] } +piston2d-opengl_graphics = "0.78" +pistoncore-glutin_window = "0.69" +rand = { version = "0.8", features = ["small_rng"] } +rusttype = "0.9" +serde = { version = "1.0", features = ["derive"] } +serde_multi = { version = "0.1", features = ["toml"] } +# TODO: replace with thiserror +util_macros = { git = "https://github.com/ScottyThePilot/util_macros" } +vecmath = "1.0" +zip = "0.5" + +[profile.dev] +opt-level = 1 + +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70586a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 ScottyThePilot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..78f8e32 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# HOI4 Province Editor +This program is designed to simplify or replace needing to manually edit `provinces.bmp` and `definition.csv` when +editing HOI4 Maps. The idea behind this program is that it unifies editing both files in one place with a graphical +editor, as well as attempting to guarantee that all province maps created by this program will load correctly into +the game. + +This program is not a complete replacement for MapGen, it is intended to be used to edit a map you have already +generated with MapGen, or for making tweaks to an already complete map. + +![Province Map Mode](https://imgur.com/OP2NnHf.png) +![Terrain Map Mode](https://imgur.com/OnU2Mwf.png) + +If "HOI4 Province Editor" is too much of a mouthfull, just call it HOI4PE. + +## Features +- [x] Map viewing, editing and manupulation +- [x] Support for custom terrain types via configuration file +- [x] Calculating map errors/warnings +- [ ] Console for issuing more complex instructions +- [x] Saving/loading/exporting +- [x] Auto-generating which provinces are coastal +- [ ] Support for creating/editing adjacencies +- [ ] Support for +- [ ] Exporting terrain or land type view modes for MapGen/ProvGen +- [ ] Province selection and multiple province editing +- [ ] Preserving province IDs (in order to not break maps) + +## Controls +- `1` Color/province map view mode +- `2` Terrain/biome map view mode +- `3` Land type map view mode +- `4` Continents map view mode +- `5` Coastal provinces map view mode +- `Left-click` will draw with a color or map data while a color or some data is selected +- `Right-click` will grab and pan the camera around +- `Middle-click` will pick whatever color or map data that you are pointing at +- `Scroll` will zoom the map view +- `Shift-Scroll` will resize your brush when in color mode +- `Ctrl-Z` and `Ctrl-Y` are Undo and Redo, respectively +- `Ctrl-Shift-S` will Save-As, adding `Alt` will allow you to save as an archive +- `Ctrl-S` will Save, overwriting whatever map files you had imported +- `Ctrl-O` will let you open a `map` folder, adding `Alt` will allow you to select archives +- `Spacebar` will give you a new color/type/terrain/continent to paint with depending on map mode +- `Shift-C` will re-calculate coastal provinces +- `Shift-R` will randomly re-color all of the provinces on the map +- `Shift-P` will calculate and display symbols indicating map errors/warnings +- `H` resets the camera view +- The tilde key on QWERTY keyboards will open/close the console, though the console doesn't do anything yet + +## Extra info +To load a map, you can do one of the following: +- Drag a folder and it will look for a `provinces.bmp` and `definition.csv` inside that folder +- Drag a file and if its name is `provinces.bmp` or `definition.csv`, it will look in the same folder for the other file +- Drag a ZIP archive, and it will try to load `provinces.bmp` and `definition.csv` from the archive +- Use `Ctrl-O` or `Ctrl-Alt-O` to load a folder or archive using the file browser + +In the terrain/biome map mode, the colors are based on what MapGen/ProvGen takes as input for terrain maps. +In the coastal map mode, darker colors represent provinces that are not coastal, while lighter colors are coastal. + +When painting continent IDs, you cannot paint continent 0 on land, and sea can only have continent 0. + +## Building +1. [Install Rust](https://www.rust-lang.org/tools/install) +2. Clone this repository to a folder and navigate there in your terminal +3. Run `cargo build --release` in that folder, wait for it to complete +4. The resulting executable should be located in `/target/release` diff --git a/assets/Consolas.ttf b/assets/Consolas.ttf new file mode 100644 index 0000000..556d2fd Binary files /dev/null and b/assets/Consolas.ttf differ diff --git a/assets/hoi4pe_config_default.toml b/assets/hoi4pe_config_default.toml new file mode 100644 index 0000000..a86b37e --- /dev/null +++ b/assets/hoi4pe_config_default.toml @@ -0,0 +1,49 @@ +# The max number of history states to keep for undo/redo +max_undo_states = 24 + +# Whether the application should attempt to preserve the IDs and colors of the original map +# so as to not break states, strategic regions, etc. +preserve_ids = false + +# You can define custom terrains/biomes below, simply specify the name, the +# color you want it to be in the editor, and the type of terrain it is (land/sea/lake) + +[terrain.plains] +color = [0xff, 0x81, 0x42] +type = 'land' + +[terrain.desert] +color = [0xff, 0x3f, 0x00] +type = 'land' + +[terrain.forest] +color = [0x59, 0xc7, 0x55] +type = 'land' + +[terrain.hills] +color = [0xf8, 0xff, 0x99] +type = 'land' + +[terrain.jungle] +color = [0x7f, 0xbf, 0x00] +type = 'land' + +[terrain.marsh] +color = [0x4c, 0x60, 0x23] +type = 'land' + +[terrain.mountain] +color = [0x7c, 0x87, 0x7d] +type = 'land' + +[terrain.lakes] +color = [0x00, 0xff, 0xff] +type = 'lake' + +[terrain.ocean] +color = [0x00, 0x00, 0xff] +type = 'sea' + +[terrain.urban] +color = [0x9b, 0x00, 0xff] +type = 'land' diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..92d0b61 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,405 @@ +pub mod canvas; +mod console; +pub mod format; +pub mod map; + +use glutin::window::CursorIcon; +use graphics::types::Color; +use graphics::context::Context; +use graphics::glyph_cache::rusttype::GlyphCache; +use opengl_graphics::{GlGraphics, Filter, Texture, TextureSettings, TextureOp}; +use piston::input::{RenderArgs, UpdateArgs, ButtonArgs, Motion}; +use rusttype::Font; +use vecmath::Matrix2x3; + +use crate::config::Config; +use self::canvas::{Canvas, ViewMode}; +use self::console::ConsoleHandle; +use self::console::{Console, ConsoleAction}; +use self::map::Location; + +use std::convert::TryInto; +use std::path::PathBuf; +use std::time::Duration; +use std::rc::Rc; +use std::env; +use std::fmt; +use std::mem; + +const FONT_DATA: &[u8] = include_bytes!("../assets/Consolas.ttf"); +const NEUTRAL: Color = [0.25, 0.25, 0.25, 1.0]; + +#[cfg(not(debug_assertions))] +const STRING_LOAD_HELP: &str = "Drag a file, archive, or folder onto the application to load a map"; + +pub type TextureError = >::Error; +pub type FontGlyphCache = GlyphCache<'static, (), Texture>; + +pub struct App { + pub canvas: Option, + pub config: Rc, + pub console: Console, + pub cursor: CursorIcon, + pub glyph_cache: FontGlyphCache, + pub activate_console: bool, + pub painting: bool, + pub mod_shift: bool, + pub mod_ctrl: bool, + pub mod_alt: bool, +} + +impl App { + pub fn new(_gl: &mut GlGraphics) -> Self { + let config = Config::load().expect("unable to load config"); + let texture_settings = TextureSettings::new().mag(Filter::Nearest).min(Filter::Nearest); + let font = Font::try_from_bytes(FONT_DATA).expect("unable to load font"); + let glyph_cache = GlyphCache::from_font(font, (), texture_settings); + let console = Console::new(Duration::from_secs(5)); + + App { + canvas: None, + config: Rc::new(config), + console, + cursor: CursorIcon::Crosshair, + glyph_cache, + activate_console: false, + painting: false, + mod_shift: false, + mod_ctrl: false, + mod_alt: false + } + } + + #[cfg(debug_assertions)] + pub fn on_init(&mut self) { + // In debug mode, load the custom test map + self.open_map_location("./test_map.zip"); + } + + #[cfg(not(debug_assertions))] + pub fn on_init(&mut self) { + use std::env::args; + if let Some(path) = args().nth(1) { + self.open_map_location(path); + } else { + self.console.push_system(Ok(STRING_LOAD_HELP)); + }; + } + + pub fn on_render_event(&mut self, _args: RenderArgs, ctx: Context, gl: &mut GlGraphics) { + graphics::clear(NEUTRAL, gl); + + if let Some(canvas) = &self.canvas { + canvas.draw(ctx, &mut self.glyph_cache, !self.console.is_active(), gl); + }; + + self.draw_console(gl, ctx.transform); + } + + pub fn on_update_event(&mut self, _args: UpdateArgs) { + self.console.tick(); + //self.cursor = CursorIcon::Crosshair; + + if mem::replace(&mut self.activate_console, false) { + self.console.activate(); + self.mod_shift = false; + self.mod_ctrl = false; + self.mod_alt = false; + }; + } + + pub fn on_button_event(&mut self, args: ButtonArgs) { + use piston::input::{Key, MouseButton, Button}; + use piston::input::ButtonState::Press as Dn; + use piston::input::ButtonState::Release as Up; + const CONSOLE_KEY: Option = Some(41); + match (self.console.is_active(), &mut self.canvas, args.state, args.button) { + (true, _, Dn, _) if args.scancode == CONSOLE_KEY => self.deactivate_console(), + (false, _, Dn, _) if args.scancode == CONSOLE_KEY => self.activate_console(), + (true, _, Dn, Button::Keyboard(Key::Left)) => self.console.action(ConsoleAction::Left), + (true, _, Dn, Button::Keyboard(Key::Right)) => self.console.action(ConsoleAction::Right), + (true, _, Dn, Button::Keyboard(Key::Backspace)) => self.console.action(ConsoleAction::Backspace), + (true, _, Dn, Button::Keyboard(Key::Delete)) => self.console.action(ConsoleAction::Delete), + (true, _, Dn, Button::Keyboard(Key::Return)) => self.execute_command(), + (false, _, Dn, Button::Keyboard(Key::LShift)) => self.mod_shift = true, + (false, _, Dn, Button::Keyboard(Key::LCtrl)) => self.mod_ctrl = true, + (false, _, Dn, Button::Keyboard(Key::LAlt)) => self.mod_alt = true, + (false, _, Up, Button::Keyboard(Key::LShift)) => self.mod_shift = false, + (false, _, Up, Button::Keyboard(Key::LCtrl)) => self.mod_ctrl = false, + (false, _, Up, Button::Keyboard(Key::LAlt)) => self.mod_alt = false, + (false, Some(canvas), state, button) => match (state, button) { + (Dn, Button::Mouse(MouseButton::Left)) => self.start_painting(), + (Up, Button::Mouse(MouseButton::Left)) => self.stop_painting(), + (Dn, Button::Mouse(MouseButton::Right)) => canvas.camera.set_panning(true), + (Up, Button::Mouse(MouseButton::Right)) => canvas.camera.set_panning(false), + (Dn, Button::Mouse(MouseButton::Middle)) => canvas.pick_brush(self.console.handle()), + (Dn, Button::Keyboard(Key::Z)) if self.mod_ctrl => canvas.undo(), + (Dn, Button::Keyboard(Key::Y)) if self.mod_ctrl => canvas.redo(), + (Dn, Button::Keyboard(Key::S)) if self.mod_ctrl && self.mod_shift => self.save_map_as(self.mod_alt), + (Dn, Button::Keyboard(Key::S)) if self.mod_ctrl => self.save_map(), + (Dn, Button::Keyboard(Key::O)) if self.mod_ctrl => self.open_map(self.mod_alt), + (Dn, Button::Keyboard(Key::Space)) => canvas.cycle_brush(self.console.handle()), + (Dn, Button::Keyboard(Key::C)) if self.mod_shift => canvas.calculate_coastal_provinces(), + (Dn, Button::Keyboard(Key::R)) if self.mod_shift => canvas.calculate_recolor_map(), + (Dn, Button::Keyboard(Key::P)) if self.mod_shift => canvas.display_problems(self.console.handle()), + (Dn, Button::Keyboard(Key::H)) => canvas.camera.reset(), + (Dn, Button::Keyboard(Key::D1)) => canvas.set_view_mode(self.console.handle(), ViewMode::Color), + (Dn, Button::Keyboard(Key::D2)) => canvas.set_view_mode(self.console.handle(), ViewMode::Kind), + (Dn, Button::Keyboard(Key::D3)) => canvas.set_view_mode(self.console.handle(), ViewMode::Terrain), + (Dn, Button::Keyboard(Key::D4)) => canvas.set_view_mode(self.console.handle(), ViewMode::Continent), + (Dn, Button::Keyboard(Key::D5)) => canvas.set_view_mode(self.console.handle(), ViewMode::Coastal), + _ => () + }, + _ => () + }; + } + + pub fn on_motion_event(&mut self, motion: Motion) { + match (&mut self.canvas, motion) { + (Some(canvas), Motion::MouseCursor(pos)) => { + canvas.camera.on_mouse_position(Some(pos)); + if self.painting { + canvas.paint_brush(); + }; + }, + (Some(canvas), Motion::MouseRelative(rel)) => { + canvas.camera.on_mouse_relative(rel); + }, + (Some(canvas), Motion::MouseScroll([_, d])) => { + if self.mod_shift { + canvas.change_brush_radius(d); + } else { + canvas.camera.on_mouse_zoom(d); + }; + }, + _ => () + }; + } + + pub fn on_text_event(&mut self, string: String) { + self.console.action(ConsoleAction::Insert(string)); + } + + pub fn on_file_drop(&mut self, path: PathBuf) { + self.open_map_location(path); + } + + pub fn on_unfocus(&mut self) { + if let Some(canvas) = &mut self.canvas { + canvas.camera.on_mouse_position(None); + }; + } + + pub fn on_close(mut self) { + if self.is_canvas_modified() { + if msg_dialog_unsaved_changes_exit() { + self.save_map(); + }; + }; + } + + pub fn execute_command(&mut self) { + if let Some(_command) = self.console.enter_command() { + // TODO: Make the console actually work + }; + } + + fn is_canvas_modified(&self) -> bool { + if let Some(canvas) = &self.canvas { + canvas.modified() + } else { + false + } + } + + fn draw_console(&mut self, gl: &mut GlGraphics, transform: Matrix2x3) { + self.console.draw(transform, &mut self.glyph_cache, gl); + } + + fn deactivate_console(&mut self) { + self.console.deactivate(); + } + + fn activate_console(&mut self) { + self.activate_console = true; + if let Some(canvas) = &mut self.canvas { + canvas.camera.set_panning(false); + canvas.paint_stop(); + }; + self.painting = false; + self.mod_shift = false; + self.mod_ctrl = false; + self.mod_alt = false; + } + + fn start_painting(&mut self) { + self.painting = true; + if let Some(canvas) = &mut self.canvas { + canvas.paint_brush(); + }; + } + + fn stop_painting(&mut self) { + self.painting = false; + if let Some(canvas) = &mut self.canvas { + canvas.paint_stop(); + }; + } + + fn open_map(&mut self, archive: bool) { + if let Some(canvas) = &mut self.canvas { + if canvas.modified() { + if msg_dialog_unsaved_changes() { + self.save_map(); + }; + }; + }; + + if let Some(location) = file_dialog_open(archive) { + self.open_map_location(location); + }; + } + + fn save_map(&mut self) { + if let Some(canvas) = &self.canvas { + let location = canvas.location().clone(); + self.save_map_location(location); + }; + } + + fn save_map_as(&mut self, archive: bool) { + if self.canvas.is_some() { + if let Some(location) = file_dialog_save(archive) { + if self.save_map_location(location.clone()) { + self.canvas.as_mut().expect("infallible").set_location(location); + }; + }; + }; + } + + fn open_map_location(&mut self, location: L) -> bool + where L: TryInto, L::Error: fmt::Display { + let config = Rc::clone(&self.config); + if let Some(location) = location.try_into().report(self.console.handle()) { + let success_message = format!("Loaded map from {}", location); + let canvas = Canvas::load(location, config).report(self.console.handle()); + if let Some(canvas) = canvas { + self.console.push_system(Ok(success_message)); + self.canvas = Some(canvas); + true + } else { + false + } + } else { + false + } + } + + fn save_map_location(&mut self, location: L) -> bool + where L: TryInto, L::Error: fmt::Display { + let canvas = self.canvas.as_ref().expect("no canvas loaded"); + if let Some(location) = location.try_into().report(self.console.handle()) { + let success_message = format!("Saved map to {}", location); + if let Some(()) = canvas.save(&location).report(self.console.handle()) { + self.console.push_system(Ok(success_message)); + true + } else { + false + } + } else { + false + } + } +} + +trait Report { + type Return; + + fn report(self, handle: ConsoleHandle) -> Self::Return; +} + +impl Report for Result { + type Return = Option; + + fn report(self, mut handle: ConsoleHandle) -> Option { + match self { + Ok(value) => Some(value), + Err(err) => { + handle.push_system(Err(format!("{}", err))); + None + } + } + } +} + +impl Report for Option { + type Return = (); + + fn report(self, mut handle: ConsoleHandle) { + if let Some(string) = self { + handle.push_system(Ok(string)); + }; + } +} + +fn file_dialog_save(archive: bool) -> Option { + use native_dialog::FileDialog; + let root = env::current_dir() + .unwrap_or_else(|_| PathBuf::from("./")); + if archive { + FileDialog::new() + .set_location(&root) + .set_filename("map.zip") + .add_filter("ZIP Archive", &["zip"]) + .show_save_single_file() + .expect("error displaying file dialog") + .map(Location::Zip) + } else { + FileDialog::new() + .set_location(&root) + .show_open_single_dir() + .expect("error displaying file dialog") + .map(Location::Dir) + } +} + +fn file_dialog_open(archive: bool) -> Option { + use native_dialog::FileDialog; + let root = env::current_dir() + .unwrap_or_else(|_| PathBuf::from("./")); + if archive { + FileDialog::new() + .set_location(&root) + .set_filename("map.zip") + .add_filter("ZIP Archive", &["zip"]) + .show_open_single_file() + .expect("error displaying file dialog") + .map(Location::Zip) + } else { + FileDialog::new() + .set_location(&root) + .show_open_single_dir() + .expect("error displaying file dialog") + .map(Location::Dir) + } +} + +fn msg_dialog_unsaved_changes_exit() -> bool { + use native_dialog::{MessageDialog, MessageType}; + MessageDialog::new() + .set_title(crate::APPNAME) + .set_text("You have unsaved changes, would you like to save them before exiting?") + .set_type(MessageType::Warning) + .show_confirm() + .expect("error displaying file dialog") +} + +fn msg_dialog_unsaved_changes() -> bool { + use native_dialog::{MessageDialog, MessageType}; + MessageDialog::new() + .set_title(crate::APPNAME) + .set_text("You have unsaved changes, would you like to save them?") + .set_type(MessageType::Warning) + .show_confirm() + .expect("error displaying file dialog") +} diff --git a/src/app/canvas.rs b/src/app/canvas.rs new file mode 100644 index 0000000..284316d --- /dev/null +++ b/src/app/canvas.rs @@ -0,0 +1,445 @@ +use fxhash::FxHashSet; +use graphics::Transformed; +use graphics::context::Context; +use graphics::color::WHITE; +use graphics::ellipse::Ellipse; +use itertools::Itertools; +use opengl_graphics::{Filter, GlGraphics, Texture, TextureSettings}; +use vecmath::{Matrix2x3, Vector2}; + +use super::map::*; +use super::FontGlyphCache; +use super::format::DefinitionKind; +use crate::{WINDOW_WIDTH, WINDOW_HEIGHT}; +use crate::config::Config; +use super::console::{ConsoleHandle, FONT_SIZE}; +use crate::util::random::RandomHandle; +use crate::util::stringify_color; +use crate::error::Error; + +use std::rc::Rc; + +const ZOOM_SENSITIVITY: f64 = 0.125; +const WINDOW_CENTER: Vector2 = [WINDOW_WIDTH as f64 / 2.0, WINDOW_HEIGHT as f64 / 2.0]; + +pub struct Canvas { + bundle: Bundle, + history: History, + texture: Texture, + view_mode: ViewMode, + brush: BrushSettings, + problems: Vec, + unknown_terrains: Option>, + location: Location, + modified: bool, + pub camera: Camera +} + +impl Canvas { + pub fn load(location: Location, config: Rc) -> Result { + let history = History::new(config.max_undo_states); + let bundle = Bundle::load(&location, config, RandomHandle::new())?; + let texture_settings = TextureSettings::new().mag(Filter::Nearest); + let texture = Texture::from_image(&bundle.texture_buffer_color(), &texture_settings); + // The test map is very small with large ocean provinces, the 'too large box' errors go nuts + let problems = if cfg!(debug_assertions) { Vec::new() } else { bundle.generate_problems() }; + let unknown_terrains = bundle.search_unknown_terrains(); + let camera = Camera::new(&texture); + + if let Some(unknown_terrains) = &unknown_terrains { + let unknown_terrains = unknown_terrains.iter().map(|s| s.to_uppercase()).join(", "); + return Err(format!("Unknown terrains present, not found in config: {}", unknown_terrains).into()); + }; + + Ok(Canvas { + bundle, + history, + texture, + view_mode: ViewMode::Color, + brush: BrushSettings::default(), + problems, + unknown_terrains, + location, + modified: false, + camera + }) + } + + pub fn save(&self, location: &Location) -> Result<(), Error> { + self.bundle.save(location) + } + + pub fn modified(&self) -> bool { + self.modified + } + + pub fn location(&self) -> &Location { + &self.location + } + + pub fn set_location(&mut self, location: Location) { + self.location = location; + } + + pub fn draw(&self, ctx: Context, glyph_cache: &mut FontGlyphCache, camera_info: bool, gl: &mut GlGraphics) { + let transform = ctx.transform.append_transform(self.camera.display_matrix); + graphics::image(&self.texture, transform, gl); + + for problem in self.problems.iter() { + problem.draw(ctx, self.camera.display_matrix, gl); + }; + + if let (ViewMode::Color, Some(cursor_pos)) = (self.view_mode, self.camera.cursor_pos) { + let ellipse = Ellipse::new_border(WHITE, 0.5).resolution(16); + let radius = self.brush.radius * self.camera.scale_factor(); + let transform = ctx.transform.trans_pos(cursor_pos); + ellipse.draw_from_to([radius, radius], [-radius, -radius], &Default::default(), transform, gl); + }; + + if camera_info { + let camera_info = self.camera_info(); + let transform = ctx.transform.trans(8.0, WINDOW_HEIGHT as f64 - 8.0); + graphics::text(WHITE, FONT_SIZE, &camera_info, glyph_cache, transform, gl) + .expect("unable to draw text"); + }; + } + + pub fn undo(&mut self) { + if let Some(commit) = self.history.undo(&mut self.bundle.map) { + self.problems.clear(); + if self.view_mode == commit.view_mode { + self.refresh_selective(commit.extents); + } else { + self.view_mode = commit.view_mode; + self.refresh(); + }; + }; + } + + pub fn redo(&mut self) { + if let Some(commit) = self.history.redo(&mut self.bundle.map) { + self.problems.clear(); + if self.view_mode == commit.view_mode { + self.refresh_selective(commit.extents); + } else { + self.view_mode = commit.view_mode; + self.refresh(); + }; + }; + } + + pub fn calculate_coastal_provinces(&mut self) { + self.bundle.calculate_coastal_provinces(&mut self.history); + self.view_mode = ViewMode::Coastal; + self.refresh(); + } + + pub fn calculate_recolor_map(&mut self) { + self.bundle.calculate_recolor_map(&mut self.history); + self.view_mode = ViewMode::Color; + self.brush.color_brush = None; + self.refresh(); + } + + pub fn display_problems(&mut self, mut console: ConsoleHandle) { + self.problems = self.bundle.generate_problems(); + if self.problems.is_empty() { + console.push_system(Ok("No map problems detected")); + } else { + for problem in self.problems.iter() { + console.push_system(Ok(format!("Problem: {}", problem))); + }; + }; + } + + pub fn cycle_brush(&mut self, mut console: ConsoleHandle) { + match self.view_mode { + ViewMode::Color => { + let kind = self.brush.kind_brush + .map(ProvinceKind::from) + .or_else(|| { + let pos = self.camera.cursor_rel_int()?; + Some(self.bundle.map.get_province_at(pos).kind) + }) + .unwrap_or(ProvinceKind::Land); + let color = self.bundle.random_color_pure(kind); + self.brush.color_brush = Some(color); + console.push_system(Ok(format!("Brush set to color {}", stringify_color(color)))) + }, + ViewMode::Kind => { + let kind = self.brush.kind_brush; + let kind = self.bundle.config.cycle_kinds(kind); + self.brush.kind_brush = Some(kind); + console.push_system(Ok(format!("Brush set to type {}", kind.to_str().to_uppercase()))); + }, + ViewMode::Terrain => { + let terrain = self.brush.terrain_brush.as_deref(); + let terrain = self.bundle.config.cycle_terrains(terrain); + console.push_system(Ok(format!("Brush set to terrain {}", terrain.to_uppercase()))); + self.brush.terrain_brush = Some(terrain); + }, + ViewMode::Continent => { + let continent = self.brush.continent_brush; + let continent = self.bundle.config.cycle_continents(continent); + self.brush.continent_brush = Some(continent); + console.push_system(Ok(format!("Brush set to continent {}", continent))); + }, + ViewMode::Coastal => () + }; + } + + pub fn pick_brush(&mut self, mut console: ConsoleHandle) { + if let Some(pos) = self.camera.cursor_rel_int() { + let color = self.bundle.map.get_color_at(pos); + let province_data = self.bundle.map.get_province_at(pos); + match self.view_mode { + ViewMode::Color => { + self.brush.color_brush = Some(color); + console.push_system(Ok(format!("Picked color {}", stringify_color(color)))); + }, + ViewMode::Kind => if let Some(kind) = province_data.kind.to_definition_kind() { + self.brush.kind_brush = Some(kind); + console.push_system(Ok(format!("Picked type {}", kind.to_str().to_uppercase()))); + }, + ViewMode::Terrain => if province_data.terrain != "unknown" { + let terrain = province_data.terrain.as_str(); + self.brush.terrain_brush = Some(terrain.to_owned()); + console.push_system(Ok(format!("Picked terrain {}", terrain.to_uppercase()))); + }, + ViewMode::Continent => { + let continent = province_data.continent; + self.brush.continent_brush = Some(continent); + console.push_system(Ok(format!("Picked continent {}", continent))); + }, + ViewMode::Coastal => () + }; + }; + } + + pub fn paint_brush(&mut self) { + if let Some(pos) = self.camera.cursor_rel_int() { + if let (Some(color), ViewMode::Color) = (self.brush.color_brush, self.view_mode) { + let (pos, radius) = (self.camera.cursor_rel().expect("infallible"), self.brush.radius); + if let Some(extents) = self.bundle.paint_pixel_area(&mut self.history, pos, radius, color) { + self.problems.clear(); + self.modified = true; + self.refresh_selective(extents); + }; + } else if let (Some(kind), ViewMode::Kind) = (self.brush.kind_brush, self.view_mode) { + if let Some(extents) = self.bundle.paint_province_kind(&mut self.history, pos, kind) { + self.modified = true; + self.refresh_selective(extents); + }; + } else if let (Some(terrain), ViewMode::Terrain) = (&self.brush.terrain_brush, self.view_mode) { + if let Some(extents) = self.bundle.paint_province_terrain(&mut self.history, pos, terrain.clone()) { + self.modified = true; + self.refresh_selective(extents); + }; + } else if let (Some(continent), ViewMode::Continent) = (self.brush.continent_brush, self.view_mode) { + if let Some(extents) = self.bundle.paint_province_continent(&mut self.history, pos, continent) { + self.modified = true; + self.refresh_selective(extents); + }; + }; + }; + } + + pub fn paint_stop(&mut self) { + self.bundle.painting_stop(&mut self.history); + } + + pub fn change_brush_radius(&mut self, d: f64) { + const LIMIT: f64 = std::f64::consts::SQRT_2 / 2.0; + if let ViewMode::Color = self.view_mode { + let r = self.brush.radius; + let d = d * (1.0 + 0.025 * r); + self.brush.radius = (r + d).max(LIMIT); + }; + } + + pub fn set_view_mode(&mut self, mut console: ConsoleHandle, view_mode: ViewMode) { + if let (ViewMode::Terrain, Some(unknown_terrains)) = (view_mode, &self.unknown_terrains) { + let unknown_terrains = unknown_terrains.iter().map(|s| s.to_uppercase()).join(", "); + console.push_system(Err(format!("Terrain mode unavailable, unknown terrains present: {}", unknown_terrains))); + } else if view_mode != self.view_mode { + self.view_mode = view_mode; + self.refresh(); + }; + } + + fn refresh(&mut self) { + let buffer = match self.view_mode { + ViewMode::Color => self.bundle.texture_buffer_color(), + ViewMode::Kind => self.bundle.texture_buffer_kind(), + ViewMode::Terrain => self.bundle.texture_buffer_terrain(), + ViewMode::Continent => self.bundle.texture_buffer_continent(), + ViewMode::Coastal => self.bundle.texture_buffer_coastal() + }; + + self.texture.update(&buffer); + } + + fn refresh_selective(&mut self, extents: Extents) { + use opengl_graphics::{UpdateTexture, Format}; + let (offset, size) = extents.to_offset_size(); + let buffer = match self.view_mode { + ViewMode::Color => self.bundle.texture_buffer_selective_color(extents), + ViewMode::Kind => self.bundle.texture_buffer_selective_kind(extents), + ViewMode::Terrain => self.bundle.texture_buffer_selective_terrain(extents), + ViewMode::Continent => self.bundle.texture_buffer_selective_continent(extents), + ViewMode::Coastal => self.bundle.texture_buffer_selective_coastal(extents) + }; + + UpdateTexture::update(&mut self.texture, &mut (), Format::Rgba8, &buffer, offset, size) + .expect("unable to update texture"); + } + + fn brush_info(&self) -> String { + match self.view_mode { + ViewMode::Color => match self.brush.color_brush { + Some(color) => format!("color {}", stringify_color(color)), + None => "color (no brush)".to_owned() + }, + ViewMode::Kind => match self.brush.kind_brush { + Some(kind) => format!("type {}", kind.to_str().to_uppercase()), + None => "type (no brush)".to_owned() + }, + ViewMode::Terrain => match &self.brush.terrain_brush { + Some(terrain) => format!("terrain {}", terrain.to_uppercase()), + None => "terrain (no brush)".to_owned() + }, + ViewMode::Continent => match self.brush.continent_brush { + Some(continent) => format!("continent {}", continent), + None => "continent (no brush)".to_owned() + }, + ViewMode::Coastal => "coastal".to_owned() + } + } + + fn camera_info(&self) -> String { + let zoom_info = format!("{:.2}%", self.camera.scale_factor() * 100.0); + let cursor_info = self.camera.cursor_rel_int() + .map_or_else(String::new, |[x, y]| format!("{}, {} px", x, y)); + let brush_info = self.brush_info(); + format!("{:<24}{:<24}{}", cursor_info, zoom_info, brush_info) + } +} + + + +#[derive(Debug, Clone)] +pub struct BrushSettings { + color_brush: Option, + kind_brush: Option, + terrain_brush: Option, + continent_brush: Option, + radius: f64 +} + +impl Default for BrushSettings { + fn default() -> BrushSettings { + BrushSettings { + color_brush: None, + kind_brush: None, + terrain_brush: None, + continent_brush: None, + radius: 8.0 + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ViewMode { + Color, + Kind, + Terrain, + Continent, + Coastal +} + +#[derive(Debug)] +pub struct Camera { + pub cursor_pos: Option>, + pub texture_size: Vector2, + pub display_matrix: Matrix2x3, + pub panning: bool +} + +impl Camera { + fn new(texture: &Texture) -> Self { + use opengl_graphics::ImageSize; + let (width, height) = texture.get_size(); + let texture_size = [width as f64, height as f64]; + let display_matrix = vecmath::mat2x3_id() + .trans_pos(vecmath::vec2_scale(texture_size, -0.5)) + .trans_pos(WINDOW_CENTER); + Camera { + cursor_pos: None, + texture_size, + display_matrix, + panning: false + } + } + + pub fn on_mouse_position(&mut self, pos: Option>) { + self.cursor_pos = pos; + } + + pub fn on_mouse_relative(&mut self, rel: Vector2) { + if self.panning { + let rel = vecmath::vec2_scale(rel, self.scale_factor().recip()); + self.display_matrix = self.display_matrix.trans_pos(rel); + }; + } + + pub fn on_mouse_zoom(&mut self, dz: f64) { + let zoom = 2.0f64.powf(dz * ZOOM_SENSITIVITY); + let cursor_rel = self.cursor_rel().unwrap_or([0.0, 0.0]); + let cursor_rel_neg = vecmath::vec2_neg(cursor_rel); + self.display_matrix = self.display_matrix + .trans_pos(cursor_rel) + .zoom(zoom) + .trans_pos(cursor_rel_neg); + } + + pub fn reset(&mut self) { + self.display_matrix = vecmath::mat2x3_id() + .trans_pos(vecmath::vec2_scale(self.texture_size, -0.5)) + .trans_pos(WINDOW_CENTER); + } + + pub fn set_panning(&mut self, panning: bool) { + self.panning = panning; + } + + fn relative_position(&self, pos: Vector2) -> Vector2 { + vecmath::row_mat2x3_transform_pos2(self.display_matrix_inv(), pos) + } + + fn relative_position_int(&self, pos: Vector2) -> Option> { + let pos = self.relative_position(pos); + self.within_dimensions(pos) + .then(|| [pos[0] as u32, pos[1] as u32]) + } + + fn cursor_rel(&self) -> Option> { + self.cursor_pos.map(|cursor_pos| self.relative_position(cursor_pos)) + } + + fn cursor_rel_int(&self) -> Option> { + self.cursor_pos.and_then(|cursor_pos| self.relative_position_int(cursor_pos)) + } + + fn display_matrix_inv(&self) -> Matrix2x3 { + vecmath::mat2x3_inv(self.display_matrix) + } + + pub fn scale_factor(&self) -> f64 { + (self.display_matrix[0][0] + self.display_matrix[1][1]) / 2.0 + } + + fn within_dimensions(&self, pos: Vector2) -> bool { + (0.0..self.texture_size[0]).contains(&pos[0]) && + (0.0..self.texture_size[1]).contains(&pos[1]) + } +} diff --git a/src/app/console.rs b/src/app/console.rs new file mode 100644 index 0000000..49c91c8 --- /dev/null +++ b/src/app/console.rs @@ -0,0 +1,304 @@ +use graphics::Transformed; +use graphics::types::Color; +use opengl_graphics::GlGraphics; +use vecmath::Matrix2x3; + +use super::FontGlyphCache; +use crate::{WINDOW_HEIGHT, WINDOW_WIDTH}; + +use std::collections::VecDeque; +use std::time::{Instant, Duration}; + +const CURSOR: char = '\u{2588}'; +const MARGIN_SIZE: u32 = 8; +const LINE_SPACING: f64 = 1.10; +pub const FONT_SIZE: u32 = 10; +pub const TEXT_USER: Color = [0.8, 0.8, 0.8, 1.0]; +pub const TEXT_SYSTEM: Color = [1.0, 1.0, 1.0, 1.0]; +pub const TEXT_SYSTEM_ERROR: Color = [1.0, 0.2, 0.2, 1.0]; +pub const NEUTRAL: Color = [0.0, 0.0, 0.0, 0.5]; + +#[derive(Debug)] +pub struct Console { + epoch: Instant, + max_lifetime: Duration, + mode: Option, + messages: VecDeque +} + +impl Console { + pub fn new(max_lifetime: Duration) -> Self { + Console { + epoch: Instant::now(), + max_lifetime, + mode: None, + messages: VecDeque::new() + } + } + + pub(super) fn enter_command(&mut self) -> Option { + if let Some(active_console) = &mut self.mode { + let command = active_console.take(); + let entry = format!("> {}", command); + self.push(entry); + Some(command) + } else { + None + } + } + + pub(super) fn activate(&mut self) { + self.mode = Some(ActiveConsole::default()); + } + + pub(super) fn deactivate(&mut self) { + self.mode = None; + } + + pub fn is_active(&self) -> bool { + self.mode.is_some() + } + + pub(super) fn handle(&mut self) -> ConsoleHandle<'_> { + ConsoleHandle { inner: self } + } + + pub fn action(&mut self, action: ConsoleAction) { + if let Some(active_console) = &mut self.mode { + match action { + ConsoleAction::Insert(data) => active_console.insert(data), + ConsoleAction::Left => active_console.left(), + ConsoleAction::Right => active_console.right(), + ConsoleAction::Backspace => active_console.backspace(), + ConsoleAction::Delete => active_console.delete() + }; + }; + } + + fn iter_all(&self) -> impl Iterator { + self.messages.iter() + .rev() + .map(|m| (m.text.as_str(), m.color.get())) + } + + fn iter(&self, now: Instant) -> impl Iterator { + self.messages.iter() + .rev() + .filter(move |m| !m.dead(now)) + .map(move |m| (m.text.as_str(), m.color(now))) + } + + fn len(&self, now: Instant) -> usize { + self.messages.iter() + .filter(|m| !m.dead(now)) + .count() + } + + pub fn tick(&mut self) { + while self.messages.len() >= 48 { + self.messages.pop_front(); + }; + } + + pub fn push>(&mut self, text: S) { + self.messages.push_back(ConsoleMessage::new(text.into(), ConsoleColor::User, self.max_lifetime)); + } + + pub fn push_system>(&mut self, text: Result) { + let (text, color) = match text { + Ok(t) => (t.into(), ConsoleColor::System), + Err(t) => (t.into(), ConsoleColor::SystemError) + }; + + self.messages.push_back(ConsoleMessage::new(text, color, self.max_lifetime)); + } + + pub fn draw(&self, transform: Matrix2x3, glyph_cache: &mut FontGlyphCache, gl: &mut GlGraphics) { + draw_console(self, Instant::now(), transform, glyph_cache, gl) + } +} + +#[repr(transparent)] +pub struct ConsoleHandle<'c> { + inner: &'c mut Console +} + +impl<'c> ConsoleHandle<'c> { + pub fn push>(&mut self, text: S) { + self.inner.push(text) + } + + pub fn push_system>(&mut self, text: Result) { + self.inner.push_system(text) + } +} + +#[derive(Debug)] +struct ConsoleMessage { + text: String, + color: ConsoleColor, + epoch: Instant, + max_lifetime: Duration +} + +impl ConsoleMessage { + fn new(text: String, color: ConsoleColor, max_lifetime: Duration) -> Self { + ConsoleMessage { text, color, epoch: Instant::now(), max_lifetime } + } + + fn dead(&self, now: Instant) -> bool { + now.duration_since(self.epoch) > self.max_lifetime + } + + fn color(&self, now: Instant) -> Color { + let mut color = self.color.get(); + color[3] = self.alpha_lifetime(now); + color + } + + fn alpha_lifetime(&self, now: Instant) -> f32 { + let dt = now.duration_since(self.epoch).as_secs_f32(); + let dt = self.max_lifetime.as_secs_f32() - dt; + dt.min(1.0).max(0.0) + } +} + +#[derive(Debug)] +enum ConsoleColor { + User, System, SystemError +} + +impl ConsoleColor { + fn get(&self) -> Color { + match self { + ConsoleColor::User => TEXT_USER, + ConsoleColor::System => TEXT_SYSTEM, + ConsoleColor::SystemError => TEXT_SYSTEM_ERROR + } + } +} + +#[derive(Debug)] +pub enum ConsoleAction { + Insert(String), + Left, + Right, + Backspace, + Delete +} + +#[derive(Debug)] +struct ActiveConsole { + input: Vec, + cursor: usize +} + +impl ActiveConsole { + fn text(&self, blink: bool) -> String { + if blink { + self.input.iter().collect() + } else if self.cursor >= self.input.len() { + let mut input: String = self.input.iter().collect(); + input.push(CURSOR); + input + } else { + self.input.iter() + .enumerate() + .map(|(i, &ch)| match i == self.cursor { + true => CURSOR, + false => ch + }) + .collect() + } + } + + fn take(&mut self) -> String { + self.cursor = 0; + self.input.drain(..).collect() + } + + fn left(&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + + fn right(&mut self) { + self.cursor = (self.cursor + 1) + .min(self.input.len()); + } + + fn backspace(&mut self) { + if self.cursor > 0 { + self.cursor -= 1; + self.input.remove(self.cursor); + }; + } + + fn delete(&mut self) { + if self.input.len() > self.cursor { + self.input.remove(self.cursor); + }; + } + + fn insert(&mut self, data: String) { + for ch in data.chars() { + if ch != '\n' { + self.input.insert(self.cursor, ch); + self.cursor += 1; + }; + }; + } +} + +impl Default for ActiveConsole { + fn default() -> Self { + ActiveConsole { + input: Vec::new(), + cursor: 0 + } + } +} + +fn draw_console( + console: &Console, + now: Instant, + transform: Matrix2x3, + glyph_cache: &mut FontGlyphCache, + gl: &mut GlGraphics +) { + const PADDING: f64 = MARGIN_SIZE as f64; + const WINDOW_HEIGHT_F: f64 = WINDOW_HEIGHT as f64; + const WINDOW_WIDTH_F: f64 = WINDOW_WIDTH as f64; + const FONT_HEIGHT: f64 = (FONT_SIZE as f64 * LINE_SPACING * 1.5) as u32 as f64; + + let blink = now.duration_since(console.epoch).as_secs_f32(); + let blink = (blink * 2.0) as u32 % 2 == 0; + + if let Some(active_console) = &console.mode { + let height = WINDOW_HEIGHT_F - PADDING - FONT_HEIGHT; + + let pos = [WINDOW_WIDTH_F, WINDOW_HEIGHT_F]; + graphics::rectangle_from_to(NEUTRAL, [0.0, 0.0], pos, transform, gl); + + let t = transform.trans(PADDING, WINDOW_HEIGHT_F - PADDING); + let text = active_console.text(blink); + graphics::text(TEXT_USER, FONT_SIZE, &text, glyph_cache, t, gl) + .expect("unable to draw text"); + + for (i, (text, color)) in console.iter_all().enumerate() { + let y = height - i as f64 * FONT_HEIGHT; + let t = transform.trans(PADDING, y); + graphics::text(color, FONT_SIZE, text, glyph_cache, t, gl) + .expect("unable to draw text"); + }; + } else { + let height = console.len(now) as f64 * FONT_HEIGHT + PADDING; + let height = height.min(WINDOW_HEIGHT_F - PADDING); + + for (i, (text, color)) in console.iter(now).enumerate() { + let y = height - i as f64 * FONT_HEIGHT; + let t = transform.trans(PADDING, y); + graphics::text(color, FONT_SIZE, text, glyph_cache, t, gl) + .expect("unable to draw text"); + }; + }; +} diff --git a/src/app/format.rs b/src/app/format.rs new file mode 100644 index 0000000..7bad89d --- /dev/null +++ b/src/app/format.rs @@ -0,0 +1,18 @@ +//! Utilities for parsing text-based game data files +mod adjacency; +mod definition; + +pub use crate::util::csv::ParseCsv; +pub use self::adjacency::*; +pub use self::definition::*; + +use std::fmt; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct ParseError; + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unable to parse") + } +} diff --git a/src/app/format/adjacency.rs b/src/app/format/adjacency.rs new file mode 100644 index 0000000..d68f0d1 --- /dev/null +++ b/src/app/format/adjacency.rs @@ -0,0 +1,167 @@ +use serde::{Serialize, Deserialize}; + +use crate::util::csv::ParseCsv; +use super::ParseError; + +use std::str::FromStr; +use std::num::ParseIntError; +use std::cmp::{Ord, PartialOrd, Ordering}; +use std::convert::TryFrom; +use std::fmt; + +const HEADER_LINE: &str = "From;To;Type;Through;start_x;start_y;stop_x;stop_y;adjacency_rule_name;Comment"; + +/// I don't know what this line is supposed to do, but every `adjacencies.csv` I've looked at has it +const FOOTER_LINE: &str = "-1;-1;;-1;-1;-1;-1;-1;-1"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Adjacency { + /// First province ID + pub from_id: u32, + /// Second province ID + pub to_id: u32, + /// Adjacency type + pub kind: AdjacencyKind, + /// Defines a province that can block this adjacency, optional + pub through: Option, + /// Precise location of the beginning, optional + pub start: Option<[u32; 2]>, + /// Precise location of the end, optional + pub stop: Option<[u32; 2]>, + /// The name of this adjacency, for use in `adjacency_rules.txt`, optional + pub rule_name: String, + /// A comment describing this adjacency, optional + pub comment: String +} + +impl ParseCsv<10> for Adjacency { + const HEADER_LINE: Option<&'static str> = Some(HEADER_LINE); + const FOOTER_LINE: Option<&'static str> = Some(FOOTER_LINE); + + fn parse_line(line: [String; 10]) -> Option { + let [from_id, to_id, kind, through, start_x, start_y, stop_x, stop_y, rule_name, comment] = line; + + Some(Adjacency { + from_id: from_id.parse().ok()?, + to_id: to_id.parse().ok()?, + kind: kind.parse().ok()?, + through: parse_maybe_num(&through).ok()?, + start: parse_maybe_pos(&start_x, &start_y).ok()?, + stop: parse_maybe_pos(&stop_x, &stop_y).ok()?, + rule_name, + comment + }) + } +} + +impl PartialOrd for Adjacency { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(Self::cmp(self, other)) + } +} + +impl Ord for Adjacency { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.rule_name.cmp(&other.rule_name) + .then_with(|| self.comment.cmp(&other.comment)) + .then_with(|| self.kind.cmp(&other.kind)) + } +} + +impl fmt::Display for Adjacency { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{};{};{};{};{};{};{};{}", + self.from_id, + self.to_id, + self.kind.to_str(), + stringify_maybe_num(self.through), + stringify_maybe_pos(self.start), + stringify_maybe_pos(self.stop), + self.rule_name, + self.comment + ) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(into = "&str", try_from = "String")] +pub enum AdjacencyKind { + Land = 0, + River = 1, + LargeRiver = 2, + Sea = 3, + Impassable = 4 +} + +impl AdjacencyKind { + pub fn to_str(self) -> &'static str { + match self { + AdjacencyKind::Land => "", + AdjacencyKind::River => "river", + AdjacencyKind::LargeRiver => "large_river", + AdjacencyKind::Sea => "sea", + AdjacencyKind::Impassable => "impassable" + } + } +} + +impl FromStr for AdjacencyKind { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "" => Ok(AdjacencyKind::Land), + "river" => Ok(AdjacencyKind::River), + "large_river" => Ok(AdjacencyKind::LargeRiver), + "sea" => Ok(AdjacencyKind::Sea), + "impassable" => Ok(AdjacencyKind::Impassable), + _ => Err(ParseError) + } + } +} + +impl TryFrom for AdjacencyKind { + type Error = ParseError; + + fn try_from(string: String) -> Result { + AdjacencyKind::from_str(&string) + } +} + +impl Into<&'static str> for AdjacencyKind { + fn into(self) -> &'static str { + self.to_str() + } +} + +impl fmt::Display for AdjacencyKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_str()) + } +} + +fn parse_maybe_num(n: &str) -> Result, ParseIntError> { + Ok(if n == "-1" { None } else { Some(n.parse::()?) }) +} + +fn stringify_maybe_num(num: Option) -> String { + num.map_or("-1".to_owned(), |n| n.to_string()) +} + +fn parse_maybe_pos(x: &str, y: &str) -> Result, ParseIntError> { + if x == "-1" || y == "-1" { + Ok(None) + } else { + let x = x.parse::()?; + let y = y.parse::()?; + Ok(Some([x, y])) + } +} + +fn stringify_maybe_pos(pos: Option<[u32; 2]>) -> String { + pos.map_or("-1;-1".to_owned(), |[x, y]| format!("{};{}", x, y)) +} diff --git a/src/app/format/definition.rs b/src/app/format/definition.rs new file mode 100644 index 0000000..1f83eba --- /dev/null +++ b/src/app/format/definition.rs @@ -0,0 +1,130 @@ +use serde::{Serialize, Deserialize}; + +use crate::util::csv::ParseCsv; +use super::ParseError; + +use std::str::FromStr; +use std::cmp::{Ord, PartialOrd, Ordering}; +use std::convert::TryFrom; +use std::fmt; + +/// I don't know what this line does, but my map breaks if I remove it +const HEADER_LINE: &str = "0;0;0;0;land;false;unknown;0"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Definition { + /// Province ID; unlikely to go over 100,000 + pub id: u32, + /// Province color, corresponds with color on `provinces.bmp` + pub rgb: [u8; 3], + /// Province type + pub kind: DefinitionKind, + /// Whether this province is coastal or not + pub coastal: bool, + /// Province terrain, what type of 'biome' this province is; supports custom biomes + pub terrain: String, + /// Province continent ID + pub continent: u16 +} + +impl ParseCsv<8> for Definition { + const HEADER_LINE: Option<&'static str> = Some(HEADER_LINE); + const FOOTER_LINE: Option<&'static str> = None; + + fn parse_line(line: [String; 8]) -> Option { + let [id, r, g, b, kind, coastal, terrain, continent] = line; + + Some(Definition { + id: id.parse().ok()?, + rgb: [r.parse().ok()?, g.parse().ok()?, b.parse().ok()?], + kind: kind.parse().ok()?, + coastal: coastal.parse().ok()?, + terrain: terrain.to_lowercase(), + continent: continent.parse().ok()? + }) + } +} + +impl PartialOrd for Definition { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(Self::cmp(self, other)) + } +} + +impl Ord for Definition { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.kind.cmp(&other.kind) + .then_with(|| self.continent.cmp(&other.continent)) + .then_with(|| self.terrain.cmp(&other.terrain)) + } +} + +impl fmt::Display for Definition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{};{};{};{};{};{};{};{}", + self.id, + self.rgb[0], + self.rgb[1], + self.rgb[2], + self.kind, + self.coastal, + self.terrain, + self.continent + ) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(into = "&str", try_from = "String")] +pub enum DefinitionKind { + Land = 0, + Sea = 1, + Lake = 2 +} + +impl DefinitionKind { + pub fn to_str(self) -> &'static str { + match self { + DefinitionKind::Land => "land", + DefinitionKind::Sea => "sea", + DefinitionKind::Lake => "lake" + } + } +} + +impl FromStr for DefinitionKind { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "land" => Ok(DefinitionKind::Land), + "sea" => Ok(DefinitionKind::Sea), + "lake" => Ok(DefinitionKind::Lake), + _ => Err(ParseError) + } + } +} + +impl TryFrom for DefinitionKind { + type Error = ParseError; + + fn try_from(string: String) -> Result { + DefinitionKind::from_str(&string) + } +} + +impl Into<&'static str> for DefinitionKind { + fn into(self) -> &'static str { + self.to_str() + } +} + +impl fmt::Display for DefinitionKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_str()) + } +} diff --git a/src/app/map.rs b/src/app/map.rs new file mode 100644 index 0000000..f8e4f2b --- /dev/null +++ b/src/app/map.rs @@ -0,0 +1,687 @@ +//! An abstract representation of a map's data +mod history; +mod bridge; +mod problems; + +use image::{Rgb, RgbImage, Rgba, RgbaImage}; +use fxhash::{FxHashMap, FxHashSet}; +use serde::{Serialize, Deserialize}; +use vecmath::Vector2; + +use crate::config::Config; +use crate::util::fx_hash_map_with_capacity; +use crate::util::XYIter; +use crate::util::uord::UOrd; +use crate::util::random::RandomHandle; +use crate::app::format::*; +use crate::error::Error; +use self::history::*; + +pub use self::bridge::Location; +pub use self::history::History; +pub use self::problems::Problem; + +use std::convert::TryFrom; +use std::str::FromStr; +use std::rc::Rc; + +pub type Color = [u8; 3]; + +#[derive(Debug)] +pub struct Bundle { + pub map: Map, + pub config: Rc, + pub rng: RandomHandle +} + +impl Bundle { + pub fn load(location: &Location, config: Rc, rng: RandomHandle) -> Result { + self::bridge::load_bundle(location, config, rng) + } + + pub fn save(&self, location: &Location) -> Result<(), Error> { + self::bridge::save_bundle(location, self) + } + + pub fn generate_problems(&self) -> Vec { + self::problems::analyze(self) + } + + pub fn calculate_coastal_provinces(&mut self, history: &mut History) -> bool { + CalculateCoastalProvinces::new(self) + .map(|op| op.insert(&mut self.map, history)) + .is_some() + } + + pub fn calculate_recolor_map(&mut self, history: &mut History) -> bool { + CalculateRecolorMap::new(self) + .map(|op| op.insert(&mut self.map, history)) + .is_some() + } + + pub fn paint_province_kind(&mut self, history: &mut History, pos: Vector2, kind: impl Into) -> Option { + PaintProvinceMeta::new_change_kind(self, self.map.get_color_at(pos), kind.into()) + .map(|op| op.insert(&mut self.map, history)) + } + + pub fn paint_province_terrain(&mut self, history: &mut History, pos: Vector2, terrain: String) -> Option { + PaintProvinceMeta::new_change_terrain(self, self.map.get_color_at(pos), terrain) + .map(|op| op.insert(&mut self.map, history)) + } + + pub fn paint_province_continent(&mut self, history: &mut History, pos: Vector2, continent: u16) -> Option { + PaintProvinceContinent::new(self, self.map.get_color_at(pos), continent) + .map(|op| op.insert(&mut self.map, history)) + } + + pub fn paint_entire_province(&mut self, history: &mut History, pos: Vector2, fill_color: Color) -> Option { + PaintRecolorProvince::new(self, self.map.get_color_at(pos), fill_color) + .map(|op| op.insert(&mut self.map, history)) + } + + pub fn paint_pixel_area(&mut self, history: &mut History, pos: Vector2, radius: f64, color: Color) -> Option { + PaintPixelArea::new(self, pos, radius, color) + .map(|op| op.insert(&mut self.map, history)) + } + + pub fn paint_pixel(&mut self, history: &mut History, pos: Vector2, color: Color) -> Option { + PaintPixel::new(self, pos, color) + .map(|op| op.insert(&mut self.map, history)) + } + + pub fn painting_stop(&mut self, history: &mut History) { + if let Some(step) = history.last_step_mut() { + step.finish(); + }; + } + + pub fn texture_buffer_color(&self) -> RgbaImage { + self.map.texture_buffer(|which| which) + } + + pub fn texture_buffer_selective_color(&self, extents: Extents) -> RgbaImage { + self.map.texture_buffer_selective(extents, |which| which) + } + + pub fn texture_buffer_kind(&self) -> RgbaImage { + self.map.texture_buffer(|which| { + let kind = self.map.get_province(which).kind; + self.config.kind_color(kind) + }) + } + + pub fn texture_buffer_selective_kind(&self, extents: Extents) -> RgbaImage { + self.map.texture_buffer_selective(extents, |which| { + let kind = self.map.get_province(which).kind; + self.config.kind_color(kind) + }) + } + + pub fn texture_buffer_terrain(&self) -> RgbaImage { + self.map.texture_buffer(|which| { + let terrain = &self.map.get_province(which).terrain; + match self.config.terrain_color(terrain) { + None => panic!("unknown terrain type, color not found in config: {}", terrain), + Some(color) => color + } + }) + } + + pub fn texture_buffer_selective_terrain(&self, extents: Extents) -> RgbaImage { + self.map.texture_buffer_selective(extents, |which| { + let terrain = &self.map.get_province(which).terrain; + match self.config.terrain_color(terrain) { + None => panic!("unknown terrain type, color not found in config: {}", terrain), + Some(color) => color + } + }) + } + + pub fn texture_buffer_continent(&self) -> RgbaImage { + self.map.texture_buffer(|which| { + let continent = self.map.get_province(which).continent; + self.rng.sequence_color(continent as usize) + }) + } + + pub fn texture_buffer_selective_continent(&self, extents: Extents) -> RgbaImage { + self.map.texture_buffer_selective(extents, |which| { + let continent = self.map.get_province(which).continent; + self.rng.sequence_color(continent as usize) + }) + } + + pub fn texture_buffer_coastal(&self) -> RgbaImage { + self.map.texture_buffer(|which| { + let ProvinceData { coastal, kind, .. } = *self.map.get_province(which); + self.config.coastal_color(coastal, kind) + }) + } + + pub fn texture_buffer_selective_coastal(&self, extents: Extents) -> RgbaImage { + self.map.texture_buffer_selective(extents, |which| { + let ProvinceData { coastal, kind, .. } = *self.map.get_province(which); + self.config.coastal_color(coastal, kind) + }) + } + + /// Search for terrains that are not included in the config + pub fn search_unknown_terrains(&self) -> Option> { + let mut unknown_terrains = FxHashSet::default(); + for province_data in self.map.province_data_map.values() { + if !self.config.terrains.contains_key(&province_data.terrain) { + unknown_terrains.insert(province_data.terrain.clone()); + }; + }; + + if !unknown_terrains.is_empty() { + Some(unknown_terrains) + } else { + None + } + } + + pub fn random_color_pure(&self, kind: ProvinceKind) -> Color { + random_color_pure(&self.map.province_data_map, &self.rng, kind) + } +} + + + +#[derive(Debug)] +pub struct Map { + color_buffer: RgbImage, + province_data_map: FxHashMap, + connection_data_map: FxHashMap, ConnectionData>, + id_data: Option +} + +impl Map { + pub fn dimensions(&self) -> Vector2 { + [self.color_buffer.width(), self.color_buffer.height()] + } + + pub fn width(&self) -> u32 { + self.color_buffer.width() + } + + pub fn height(&self) -> u32 { + self.color_buffer.height() + } + + pub fn provinces_count(&self) -> usize { + self.province_data_map.len() + } + + pub fn connections_count(&self) -> usize { + self.connection_data_map.len() + } + + /// Generates a texture buffer, a buffer to be consumed by the canvas to display the map + pub fn texture_buffer(&self, f: F) -> RgbaImage + where F: Fn(Color) -> Color { + let (width, height) = self.color_buffer.dimensions(); + let mut buffer = RgbaImage::new(width, height); + for (x, y, pixel) in buffer.enumerate_pixels_mut() { + let color = f(self.get_color_at([x, y])); + *pixel = Rgba(p4(color)); + }; + + buffer + } + + /// Generates a fragment of a texture buffer, based on a bounding box + pub fn texture_buffer_selective(&self, extents: Extents, f: F) -> RgbaImage + where F: Fn(Color) -> Color { + let (offset, size) = extents.to_offset_size(); + let mut buffer = RgbaImage::new(size[0], size[1]); + for (x, y, pixel) in buffer.enumerate_pixels_mut() { + let pos = vecmath::vec2_add(offset, [x, y]); + let color = f(self.get_color_at(pos)); + *pixel = Rgba(p4(color)); + }; + + buffer + } + + /// Sets the color of a single pixel in `color_buffer` without any checks + fn put_pixel_raw(&mut self, pos: Vector2, color: Color) { + self.color_buffer.put_pixel(pos[0], pos[1], Rgb(color)); + } + + /// Sets the color of a single pixel in `color_buffer`, checks included + fn put_pixel(&mut self, pos: Vector2, color: Color) { + self.province_data_map.entry(color).or_default().pixel_count += 1; + + let previous_color = self.get_color_at(pos); + self.put_pixel_raw(pos, color); + + let previous_province = self.province_data_map.get_mut(&previous_color) + .expect("infallible"); + previous_province.pixel_count -= 1; + + if previous_province.pixel_count == 0 { + self.province_data_map.remove(&previous_color); + }; + } + + /// Sets the color of multiple pixels in `color_buffer`, checks included + fn put_many_pixels(&mut self, pixels: &[(Vector2, Color)]) { + for &(_, color) in pixels.iter() { + self.province_data_map.entry(color).or_default().pixel_count += 1; + }; + + let mut previous_colors = FxHashSet::default(); + for &(pos, color) in pixels { + let previous_color = self.get_color_at(pos); + if color != previous_color { + let previous_province = self.province_data_map + .get_mut(&previous_color) + .expect("infallible"); + previous_province.pixel_count -= 1; + previous_colors.insert(previous_color); + self.put_pixel_raw(pos, color); + }; + }; + + for previous_color in previous_colors { + let previous_province = self.province_data_map + .get_mut(&previous_color) + .expect("infallible"); + if previous_province.pixel_count == 0 { + self.province_data_map.remove(&previous_color); + }; + }; + } + + pub fn calculate_coastal_provinces(&self) -> FxHashMap> { + let mut coastal_provinces = self.province_data_map.keys() + .map(|&color| (color, Some(false))) + .collect::>>(); + + let coastal_neighbors = UOrd::new(ProvinceKind::Land, ProvinceKind::Sea); + for neighboring in self.calculate_neighbors() { + if neighboring.map(|n| self.get_province(n).kind) == coastal_neighbors { + let (a, b) = neighboring.into_tuple(); + coastal_provinces.insert(a, Some(true)); + coastal_provinces.insert(b, Some(true)); + }; + }; + + coastal_provinces + } + + /// Returns a hashset of uords describing which provinces are touching each other + fn calculate_neighbors(&self) -> FxHashSet> { + let mut neighbors = FxHashSet::default(); + let [width, height] = self.dimensions(); + + let mut check = |pos, pos_xm, pos_ym| { + let color = self.get_color_at(pos); + let color_xm = self.get_color_at(pos_xm); + let color_ym = self.get_color_at(pos_ym); + if color != color_xm { neighbors.insert(UOrd::new(color, color_xm)); }; + if color != color_ym { neighbors.insert(UOrd::new(color, color_ym)); }; + }; + + // Loop through the image, comparing pixels to each other to find adjacent sea and land pixels + for pos in XYIter::new(0..width-1, 0..height-1) { + check(pos, [pos[0] + 1, pos[1]], [pos[0], pos[1] + 1]); + }; + + // The above loop misses two comparisons with the bottom-right-most pixel, this calculates it manually + let pos = [width - 1, height - 1]; + check(pos, [pos[0] - 1, pos[1]], [pos[0], pos[1] - 1]); + + neighbors + } + + /// Replaces all of one color in `color_buffer` without any checks + fn replace_color_raw(&mut self, which: Color, color: Color) { + for Rgb(pixel) in self.color_buffer.pixels_mut() { + if *pixel == which { + *pixel = color; + }; + }; + } + + /// Replaces the key of one province with a new color in `province_data_map` without any checks + fn rekey_province_raw(&mut self, which: Color, color: Color) { + let province_data = self.province_data_map.remove(&which) + .expect("province not found with color"); + let result = self.province_data_map.insert(color, province_data); + debug_assert_eq!(result, None); + } + + /// Replaces the keys of all connections containing one color with another color without any checks + fn rekey_connections_raw(&mut self, which: Color, color: Color) { + if !self.connection_data_map.is_empty() { + let mut new_connection_data_map = fx_hash_map_with_capacity(self.connections_count()); + for (rel, connection_data) in self.connection_data_map.drain() { + new_connection_data_map.insert(rel.replace(which, color), connection_data); + }; + + self.connection_data_map = new_connection_data_map; + }; + } + + /// Completely replace all of one color in the map with another + fn recolor_province(&mut self, which: Color, color: Color) { + self.rekey_province_raw(which, color); + self.rekey_connections_raw(which, color); + self.replace_color_raw(which, color); + } + + pub fn get_color_at(&self, pos: Vector2) -> Color { + self.color_buffer.get_pixel(pos[0], pos[1]).0 + } + + pub fn get_province(&self, color: Color) -> &ProvinceData { + self.province_data_map.get(&color).expect("province not found with color") + } + + fn get_province_mut(&mut self, color: Color) -> &mut ProvinceData { + self.province_data_map.get_mut(&color).expect("province not found with color") + } + + pub fn get_province_at(&self, pos: Vector2) -> &ProvinceData { + self.get_province(self.get_color_at(pos)) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct IdData { + highest_id: u32 +} + +/// Represents a simple bounding box +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Extents { + // These bounds are inclusive + pub upper: Vector2, + pub lower: Vector2 +} + +impl Extents { + pub fn new(upper: Vector2, lower: Vector2) -> Self { + Extents { upper, lower } + } + + pub fn new_point(pos: Vector2) -> Self { + Extents { upper: pos, lower: pos } + } + + pub fn new_pos_radius(pos: Vector2, radius: f64, max: Vector2) -> Self { + let x_lower = ((pos[0] - radius).floor() as u32).min(max[0] - 1); + let y_lower = ((pos[1] - radius).floor() as u32).min(max[1] - 1); + let x_upper = ((pos[0] + radius).ceil() as u32).min(max[0] - 1); + let y_upper = ((pos[1] + radius).ceil() as u32).min(max[1] - 1); + Extents { upper: [x_upper, y_upper], lower: [x_lower, y_lower] } + } + + pub fn join(self, other: Self) -> Self { + Extents { + upper: [self.upper[0].max(other.upper[0]), self.upper[1].max(other.upper[1])], + lower: [self.lower[0].min(other.lower[0]), self.lower[1].min(other.lower[1])] + } + } + + pub fn join_point(self, pos: Vector2) -> Self { + Extents { + upper: [self.upper[0].max(pos[0]), self.upper[1].max(pos[1])], + lower: [self.lower[0].min(pos[0]), self.lower[1].min(pos[1])] + } + } + + pub fn to_offset_size(self) -> (Vector2, Vector2) { + (self.lower, [self.upper[0] - self.lower[0] + 1, self.upper[1] - self.lower[1] + 1]) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProvinceData { + pub preserved_id: Option, + pub kind: ProvinceKind, + pub terrain: String, + pub continent: u16, + pub coastal: Option, + pub pixel_count: u64 +} + +impl ProvinceData { + pub fn from_definition(definition: Definition) -> Self { + ProvinceData { + preserved_id: None, + kind: definition.kind.into(), + terrain: definition.terrain, + continent: definition.continent, + coastal: Some(definition.coastal), + pixel_count: 0 + } + } + + pub fn from_definition_config(definition: Definition, config: &Config) -> Self { + ProvinceData { + preserved_id: config.preserve_ids.then(|| definition.id), + kind: definition.kind.into(), + terrain: definition.terrain, + continent: definition.continent, + coastal: Some(definition.coastal), + pixel_count: 0 + } + } + + pub fn to_definition(&self, color: Color) -> Result { + Ok(Definition { + id: self.preserved_id.expect("no id provided for definition"), + rgb: color, + kind: self.kind.to_definition_kind() + .ok_or("province data exists with 'unknown' type")?, + coastal: self.coastal + .ok_or("province data exists with unknown coastal status")?, + terrain: match self.terrain.as_str() { + "unknown" => return Err("province data exists with unknown terrain"), + terrain => terrain.to_owned() + }, + continent: self.continent + }) + } + + pub fn to_definition_with_id(&self, color: Color, id: u32) -> Result { + Ok(Definition { + id, + rgb: color, + kind: self.kind.to_definition_kind() + .ok_or("Province exists with unknown type")?, + coastal: self.coastal + .ok_or("Province exists with unknown coastal status")?, + terrain: match self.terrain.as_str() { + "unknown" => return Err("Province exists with unknown terrain"), + terrain => terrain.to_owned() + }, + continent: self.continent + }) + } +} + +impl Default for ProvinceData { + fn default() -> ProvinceData { + ProvinceData { + preserved_id: None, + kind: ProvinceKind::Unknown, + terrain: "unknown".to_owned(), + continent: 0, + coastal: None, + pixel_count: 0 + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(into = "&str", try_from = "String")] +pub enum ProvinceKind { + Land = 0, + Sea = 1, + Lake = 2, + Unknown = 3 +} + +impl ProvinceKind { + pub fn to_str(self) -> &'static str { + match self { + ProvinceKind::Land => "land", + ProvinceKind::Sea => "sea", + ProvinceKind::Lake => "lake", + ProvinceKind::Unknown => "unknown" + } + } + + pub fn to_definition_kind(self) -> Option { + match self { + ProvinceKind::Land => Some(DefinitionKind::Land), + ProvinceKind::Sea => Some(DefinitionKind::Sea), + ProvinceKind::Lake => Some(DefinitionKind::Lake), + ProvinceKind::Unknown => None + } + } + + pub fn valid_continent_id(self, continent: u16) -> bool { + match self { + ProvinceKind::Land if continent == 0 => false, + ProvinceKind::Sea if continent != 0 => false, + // Lakes and Unknown can belong to any continent + _ => true + } + + } + + pub fn correct_continent_id(self, continent: u16) -> u16 { + match self { + ProvinceKind::Land if continent == 0 => 1, + ProvinceKind::Sea => 0, + _ => continent + } + } +} + +impl From for ProvinceKind { + fn from(kind: DefinitionKind) -> ProvinceKind { + match kind { + DefinitionKind::Land => ProvinceKind::Land, + DefinitionKind::Sea => ProvinceKind::Sea, + DefinitionKind::Lake => ProvinceKind::Lake + } + } +} + +impl FromStr for ProvinceKind { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "land" => Ok(ProvinceKind::Land), + "sea" => Ok(ProvinceKind::Sea), + "lake" => Ok(ProvinceKind::Lake), + "unknown" => Ok(ProvinceKind::Unknown), + _ => Err(ParseError) + } + } +} + +impl TryFrom for ProvinceKind { + type Error = ParseError; + + fn try_from(string: String) -> Result { + ProvinceKind::from_str(&string) + } +} + +impl Into<&'static str> for ProvinceKind { + fn into(self) -> &'static str { + self.to_str() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConnectionData { + pub kind: AdjacencyKind, + pub through: Option, + pub start: Option<[u32; 2]>, + pub stop: Option<[u32; 2]>, + pub rule_name: String, + pub comment: String +} + +impl ConnectionData { + pub fn from_adjacency(adjacency: Adjacency) -> Self { + ConnectionData { + kind: adjacency.kind, + through: adjacency.through, + start: adjacency.start, + stop: adjacency.stop, + rule_name: adjacency.rule_name, + comment: adjacency.comment + } + } + + pub fn to_adjacency(&self, rel: UOrd) -> Adjacency { + let (from_id, to_id) = rel.into_tuple(); + Adjacency { + from_id, + to_id, + kind: self.kind, + through: self.through, + start: self.start, + stop: self.stop, + rule_name: self.rule_name.clone(), + comment: self.comment.clone() + } + } +} + +fn p4(color: Color) -> [u8; 4] { + [color[0], color[1], color[2], 0xff] +} + +fn random_color(rng: &RandomHandle, kind: ProvinceKind) -> Color { + use crate::util::hsl::hsl_to_rgb; + + let lightness: f64 = match kind { + ProvinceKind::Unknown => return [rng.gen::(); 3], + ProvinceKind::Land => rng.gen_range(0.5..1.0), + ProvinceKind::Lake => rng.gen_range(0.2..0.5), + ProvinceKind::Sea => rng.gen_range(0.04..0.2) + }; + + let saturation = (lightness - 0.5).abs() + 0.5; + hsl_to_rgb([ + rng.gen_range(0.0..360.0), + rng.gen_range(saturation..1.0), + lightness + ]) +} + +fn random_color_pure(collection: &impl ColorKeyable, rng: &RandomHandle, kind: ProvinceKind) -> Color { + let mut color = random_color(rng, kind); + while collection.contains_color(color) || color == [0x00; 3] { + color = random_color(rng, kind); + }; + + color +} + +pub trait ColorKeyable { + fn contains_color(&self, color: Color) -> bool; +} + +impl ColorKeyable for FxHashMap { + fn contains_color(&self, color: Color) -> bool { + self.contains_key(&color) + } +} + +impl ColorKeyable for FxHashSet { + fn contains_color(&self, color: Color) -> bool { + self.contains(&color) + } +} diff --git a/src/app/map/bridge.rs b/src/app/map/bridge.rs new file mode 100644 index 0000000..4393c2a --- /dev/null +++ b/src/app/map/bridge.rs @@ -0,0 +1,367 @@ +//! Anything relating to loading or saving map data +use fxhash::FxHashMap; +use image::{Rgb, RgbImage, DynamicImage, ColorType}; +use image::codecs::bmp::{BmpDecoder, BmpEncoder}; +use zip::read::ZipArchive; +use zip::write::ZipWriter; + +use super::*; +use crate::app::format::{Adjacency, Definition, ParseCsv}; +use crate::config::Config; +use crate::error::Error; +use crate::util::{fx_hash_map_with_capacity, fx_hash_set_with_capacity}; +use crate::util::random::RandomHandle; +use crate::util::uord::UOrd; + +use std::convert::TryFrom; +use std::path::{Path, PathBuf}; +use std::collections::hash_map::Entry; +use std::io::{self, Cursor, Read, Write}; +use std::fs::File; +use std::rc::Rc; +use std::fmt; + +fn open_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + File::open(path).map_err(|err| format!("Unable to open {}: {}", path.display(), err)) +} + +fn create_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + File::create(path).map_err(|err| format!("Unable to create {}: {}", path.display(), err)) +} + +#[derive(Debug, Clone)] +pub enum Location { + Zip(PathBuf), + Dir(PathBuf) +} + +impl Location { + fn from_path(mut path: PathBuf) -> Result { + if let Some(ext) = path.extension() { + let name = path.file_name().expect("infallible"); + if ext == "zip" { + return Ok(Location::Zip(path)); + } else if name == "provinces.bmp" || name == "definition.csv" { + path.pop(); + return Ok(Location::Dir(path)); + }; + }; + + if path.is_dir() { + Ok(Location::Dir(path)) + } else { + Err("Invalid location".into()) + } + } +} + +impl TryFrom<&str> for Location { + type Error = Error; + + fn try_from(path: &str) -> Result { + Location::from_path(PathBuf::from(path)) + } +} + +impl TryFrom for Location { + type Error = Error; + + fn try_from(path: String) -> Result { + Location::from_path(PathBuf::from(path)) + } +} + +impl TryFrom<&Path> for Location { + type Error = Error; + + fn try_from(path: &Path) -> Result { + Location::from_path(path.to_owned()) + } +} + +impl TryFrom for Location { + type Error = Error; + + fn try_from(path: PathBuf) -> Result { + Location::from_path(path) + } +} + +impl fmt::Display for Location { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Location::Zip(path) => write!(f, "archive {}", path.display()), + Location::Dir(path) => write!(f, "directory {}", path.display()) + } + } +} + +pub(super) fn load_bundle(location: &Location, config: Rc, rng: RandomHandle) -> Result { + let (province_image, definition_table, adjacencies_table) = match location { + Location::Zip(path) => { + let mut zip = ZipArchive::new(open_file(path)?)?; + let province_image = read_province_image(zip.by_name("provinces.bmp")?)?; + let definition_table = read_definition_table(zip.by_name("definition.csv")?)?; + let adjacencies_table = zip.by_name("adjacencies.csv") + .map_or_else(|_| Ok(Vec::new()), read_adjacencies_table)?; + (province_image, definition_table, adjacencies_table) + }, + Location::Dir(path) => { + let province_image = read_province_image(open_file(path.join("provinces.bmp"))?)?; + let definition_table = read_definition_table(open_file(path.join("definition.csv"))?)?; + let adjacencies_table = maybe_not_found(File::open(path.join("adjacencies.csv")))? + .map_or_else(|| Ok(Vec::new()), read_adjacencies_table)?; + (province_image, definition_table, adjacencies_table) + } + }; + + Ok(construct_map_data(province_image, definition_table, adjacencies_table, config, rng)) +} + +fn construct_map_data( + province_image: RgbImage, + definition_table: Vec, + adjacencies_table: Vec, + config: Rc, + rng: RandomHandle +) -> Bundle { + let mut color_buffer = province_image; + + let mut highest_id = definition_table[0].id; + // Create a sparse array for mapping province ids to colors + let mut color_index = Vec::with_capacity(definition_table.len()); + for d in definition_table.iter() { + let len = color_index.len().max(d.id as usize + 1); + highest_id = highest_id.max(d.id); + color_index.resize(len, None); + color_index[d.id as usize] = Some(d.rgb); + }; + + // Initially convert the definition table into a province data map + let mut definition_map = definition_table.into_iter() + .map(|d| (d.rgb, ProvinceData::from_definition_config(d, &config))) + .collect::>(); + // Loop through every pixel in the color buffer, ensuring that the resulting province data map + // will be valid and will have no provinces mapping to colors not on the color buffer + let mut province_data_map = FxHashMap::default(); + for &Rgb(pixel) in color_buffer.pixels() { + // If this color isn't in the new province data map, but it is in the definition table, + // take it from the former and put it in the latter + match province_data_map.entry(pixel) { + Entry::Vacant(entry) => { + let mut province_data = definition_map.remove(&pixel).unwrap_or_default(); + province_data.pixel_count += 1; + entry.insert(province_data); + }, + Entry::Occupied(entry) => { + entry.into_mut().pixel_count += 1; + } + }; + }; + + province_data_map.shrink_to_fit(); + let _ = definition_map; + + // Loop through the entries in the adjacencies table, converting ids to colors using `color_index`, + // since the adjacencies map is indexed by color instead of id + let mut connection_data_map = fx_hash_map_with_capacity(adjacencies_table.len()); + for a in adjacencies_table.into_iter() { + if let Some(rel) = get_color_index(&color_index, [a.from_id, a.to_id]) { + connection_data_map.insert(rel, ConnectionData::from_adjacency(a)); + }; + }; + + connection_data_map.shrink_to_fit(); + let _ = color_index; + + // Recolor the entire map if `preserve_ids` is false + if !config.preserve_ids { + recolor_everything( + &mut color_buffer, + &mut province_data_map, + &mut connection_data_map, + &rng + ); + }; + + let id_data = config.preserve_ids.then(|| IdData { highest_id }); + + Bundle { + map: Map { + color_buffer, + province_data_map, + connection_data_map, + id_data + }, + config, + rng + } +} + +pub(super) fn recolor_everything( + color_buffer: &mut RgbImage, + province_data_map: &mut FxHashMap, + connection_data_map: &mut FxHashMap, ConnectionData>, + rng: &RandomHandle +) { + let mut colors_list = fx_hash_set_with_capacity(province_data_map.len()); + let mut replacement_map = fx_hash_map_with_capacity(province_data_map.len()); + + let mut new_province_data_map = fx_hash_map_with_capacity(province_data_map.len()); + for (previous_color, province_data) in province_data_map.drain() { + let color = random_color_pure(&colors_list, &rng, province_data.kind); + let opt = colors_list.insert(color); + debug_assert!(opt); + let opt = replacement_map.insert(previous_color, color); + debug_assert_eq!(opt, None); + let opt = new_province_data_map.insert(color, province_data); + debug_assert_eq!(opt, None); + }; + + *province_data_map = new_province_data_map; + + let mut new_connection_data_map = fx_hash_map_with_capacity(connection_data_map.len()); + for (previous_rel, connection_data) in connection_data_map.drain() { + let rel = previous_rel.map(|color| replacement_map[&color]); + // This operation should never overwrite an existing entry + let opt = new_connection_data_map.insert(rel, connection_data); + debug_assert_eq!(opt, None); + }; + + *connection_data_map = new_connection_data_map; + + for Rgb(pixel) in color_buffer.pixels_mut() { + *pixel = replacement_map[pixel]; + }; +} + +pub fn save_bundle(location: &Location, bundle: &Bundle) -> Result<(), Error> { + match location { + Location::Zip(path) => { + let (definition_table, adjacencies_table) = deconstruct_map_data(bundle)?; + let mut zip = ZipWriter::new(create_file(path)?); + zip.set_comment(format!("Generated by {}", crate::APPNAME)); + + zip.start_file("provinces.bmp", Default::default())?; + write_province_image(&mut zip, &bundle.map.color_buffer)?; + + zip.start_file("definition.csv", Default::default())?; + write_definition_table(&mut zip, definition_table)?; + + if !adjacencies_table.is_empty() { + zip.start_file("adjacencies.csv", Default::default())?; + write_adjacencies_table(&mut zip, adjacencies_table)?; + }; + + zip.finish()?; + }, + Location::Dir(path) => { + let (definition_table, adjacencies_table) = deconstruct_map_data(bundle)?; + + let file = create_file(path.join("provinces.bmp"))?; + write_province_image(file, &bundle.map.color_buffer)?; + + let file = create_file(path.join("definition.csv"))?; + write_definition_table(file, definition_table)?; + + if !adjacencies_table.is_empty() { + let file = create_file(path.join("adjacencies.csv"))?; + write_adjacencies_table(file, adjacencies_table)?; + }; + } + }; + + Ok(()) +} + +fn deconstruct_map_data(bundle: &Bundle) -> Result<(Vec, Vec), Error> { + if bundle.config.preserve_ids { + deconstruct_map_data_preserve_ids(bundle) + } else { + deconstruct_map_data_no_preserve_ids(bundle) + } +} + +fn deconstruct_map_data_preserve_ids(_bundle: &Bundle) -> Result<(Vec, Vec), Error> { + todo!("the preserve_id feature is not complete yet") +} + +fn deconstruct_map_data_no_preserve_ids(bundle: &Bundle) -> Result<(Vec, Vec), Error> { + let mut definitions_table = Vec::with_capacity(bundle.map.provinces_count()); + for (&color, province_data) in bundle.map.province_data_map.iter() { + definitions_table.push(province_data.to_definition_with_id(color, 0)?); + }; + + definitions_table.sort(); + + let mut id = 1; + let mut color_index = fx_hash_map_with_capacity(definitions_table.len()); + for definition in definitions_table.iter_mut() { + color_index.insert(definition.rgb, id); + definition.id = id; + id += 1; + }; + + let mut adjacencies_table = Vec::with_capacity(bundle.map.connections_count()); + for (&rel, connection_data) in &bundle.map.connection_data_map { + let rel = rel.map(|color| color_index[&color]); + adjacencies_table.push(connection_data.to_adjacency(rel)); + }; + + adjacencies_table.sort(); + + Ok((definitions_table, adjacencies_table)) +} + +fn read_province_image(reader: R) -> Result { + let decoder = BmpDecoder::new(read_all(reader)?)?; + let img = DynamicImage::from_decoder(decoder)?; + Ok(img.into_rgb8()) +} + +fn read_definition_table(reader: R) -> Result, Error> { + Definition::parse_all(read_all(reader)?).map_err(From::from) +} + +fn read_adjacencies_table(reader: R) -> Result, Error> { + Adjacency::parse_all(read_all(reader)?).map_err(From::from) +} + +fn write_province_image(mut writer: W, province_image: &RgbImage) -> Result<(), Error> { + let mut encoder = BmpEncoder::new(&mut writer); + let (width, height) = province_image.dimensions(); + encoder.encode(province_image.as_raw(), width, height, ColorType::Rgb8).map_err(From::from) +} + +fn write_definition_table(mut writer: W, definition_table: Vec) -> Result<(), Error> { + let data = Definition::stringify_all(&definition_table); + writer.write_all(data.as_bytes()).map_err(From::from) +} + +fn write_adjacencies_table(mut writer: W, adjacencies_table: Vec) -> Result<(), Error> { + let data = Adjacency::stringify_all(&adjacencies_table); + writer.write_all(data.as_bytes()).map_err(From::from) +} + +fn read_all(mut reader: R) -> io::Result>> { + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + Ok(Cursor::new(buf)) +} + +fn get_color_index(vec: &[Option], ids: [u32; 2]) -> Option> { + let a = if let Some(&Some(t)) = vec.get(ids[0] as usize) { t } else { return None }; + let b = if let Some(&Some(t)) = vec.get(ids[1] as usize) { t } else { return None }; + Some(UOrd::new(a, b)) +} + +fn maybe_not_found(err: io::Result) -> io::Result> { + use std::io::ErrorKind; + match err { + Ok(value) => Ok(Some(value)), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), + Err(err) => Err(err) + } +} diff --git a/src/app/map/history.rs b/src/app/map/history.rs new file mode 100644 index 0000000..8daea01 --- /dev/null +++ b/src/app/map/history.rs @@ -0,0 +1,692 @@ +//! Structures for managing the history state and abstracting changes applied to the map +use fxhash::FxHashMap; +use image::Rgb; +use vecmath::Vector2; + +use crate::app::canvas::ViewMode; +use crate::util::XYIter; +use crate::util::{fx_hash_set_with_capacity, fx_hash_map_with_capacity}; +use super::{Bundle, Extents, Map, ProvinceKind, Color}; +use super::random_color_pure; + +use std::collections::VecDeque; +use std::cmp::PartialEq; +use std::mem; + + + +pub trait MapOperation: Sized { + /// Apply this operation to the map + fn apply(&self, map: &mut Map); + + /// Apply this operation to the map, generating an operation that would undo this one + fn apply_generate(&self, map: &mut Map) -> Self; + + /// Apply this operation, inserting it as a step in the history + fn insert(self, map: &mut Map, history: &mut History) -> Extents + where Step: From> { + let before = self.apply_generate(map); + let extents = self.extents(); + history.push(MapChange::new(before, self)); + extents + } + + fn view_mode(&self) -> ViewMode; + + fn extents(&self) -> Extents; +} + +pub trait MapOperationPartial: Sized { + /// Attempt to insert this partial map operation into the history, applying it to the map if successful + fn insert(self, map: &mut Map, history: &mut History) -> Extents; +} + +#[derive(Debug, Clone)] +pub struct MapChange { + before: Op, + after: Op, + done: bool +} + +impl MapChange { + fn new(before: Op, after: Op) -> Self { + MapChange { before, after, done: false } + } +} + +impl MapChange { + fn undo(&self, map: &mut Map) -> Commit { + self.before.apply(map); + Commit { + view_mode: self.before.view_mode(), + extents: self.before.extents(), + } + } + + fn redo(&self, map: &mut Map) -> Commit { + self.after.apply(map); + Commit { + view_mode: self.after.view_mode(), + extents: self.after.extents(), + } + } +} + + + +#[derive(Debug)] +pub struct History { + steps: VecDeque, + position: usize, + capacity: usize +} + +impl History { + pub fn new(capacity: usize) -> Self { + History { + steps: VecDeque::new(), + position: 0, + capacity + } + } + + pub fn undo(&mut self, map: &mut Map) -> Option { + if self.position != 0 { + let commit = self.steps[self.position - 1].undo(map); + self.position -= 1; + Some(commit) + } else { + None + } + } + + pub fn redo(&mut self, map: &mut Map) -> Option { + if self.position < self.steps.len() { + let commit = self.steps[self.position].redo(map); + self.position += 1; + Some(commit) + } else { + None + } + } + + pub(super) fn push(&mut self, step: impl Into) { + self.steps.truncate(self.position); + self.steps.push_back(step.into()); + self.position += 1; + if self.steps.len() > self.capacity { + self.steps.pop_front(); + self.position -= 1; + }; + } + + pub(super) fn last_step_mut(&mut self) -> Option<&mut Step> { + self.steps.back_mut() + } +} + +/// Represents a change that the history has made to the map +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Commit { + pub view_mode: ViewMode, + pub extents: Extents +} + + + +#[derive(Debug, Clone)] +pub enum Step { + CalculateCoastalProvinces(MapChange), + CalculateRecolorMap(MapChange), + PaintProvinceMeta(MapChange), + PaintProvinceContinent(MapChange), + PaintEntireProvince(MapChange), + PaintPixels(MapChange) +} + +impl Step { + pub(super) fn undo(&self, map: &mut Map) -> Commit { + match self { + Step::CalculateCoastalProvinces(change) => change.undo(map), + Step::CalculateRecolorMap(change) => change.undo(map), + Step::PaintProvinceMeta(change) => change.undo(map), + Step::PaintProvinceContinent(change) => change.undo(map), + Step::PaintEntireProvince(change) => change.undo(map), + Step::PaintPixels(change) => change.undo(map) + } + } + + pub(super) fn redo(&self, map: &mut Map) -> Commit { + match self { + Step::CalculateCoastalProvinces(change) => change.redo(map), + Step::CalculateRecolorMap(change) => change.redo(map), + Step::PaintProvinceMeta(change) => change.redo(map), + Step::PaintProvinceContinent(change) => change.redo(map), + Step::PaintEntireProvince(change) => change.redo(map), + Step::PaintPixels(change) => change.redo(map) + } + } + + pub(super) fn finish(&mut self) { + if let Step::PaintPixels(change) = self { + change.done = true; + }; + } +} + +impl From> for Step { + fn from(change: MapChange) -> Step { + Step::CalculateCoastalProvinces(change) + } +} + +impl From> for Step { + fn from(change: MapChange) -> Step { + Step::CalculateRecolorMap(change) + } +} + +impl From> for Step { + fn from(change: MapChange) -> Step { + Step::PaintProvinceMeta(change) + } +} + +impl From> for Step { + fn from(change: MapChange) -> Step { + Step::PaintProvinceContinent(change) + } +} + +impl From> for Step { + fn from(change: MapChange) -> Step { + Step::PaintEntireProvince(change) + } +} + +impl From> for Step { + fn from(change: MapChange) -> Step { + Step::PaintPixels(change) + } +} + +impl From> for Step { + fn from(change: MapChange) -> Step { + let extents = Extents::new_point(change.before.pos); + let before = PaintPixels { affected_pixels: vec![change.before.into()], extents }; + let after = PaintPixels { affected_pixels: vec![change.after.into()], extents }; + Step::PaintPixels(MapChange::new(before, after)) + } +} + + + +#[derive(Debug, Clone)] +pub struct CalculateCoastalProvinces { + pub(super) coastal_provinces: FxHashMap>, + pub(super) extents: Extents +} + +impl CalculateCoastalProvinces { + pub(super) fn new(bundle: &Bundle) -> Option { + let coastal_provinces = bundle.map.calculate_coastal_provinces(); + let is_not_pointless = coastal_provinces.iter() + .any(|(&which, &coastal)| bundle.map.get_province(which).coastal != coastal); + if is_not_pointless { + let [width, height] = bundle.map.dimensions(); + let extents = Extents::new([width - 1, height - 1], [0, 0]); + Some(CalculateCoastalProvinces { coastal_provinces, extents }) + } else { + None + } + } +} + +impl MapOperation for CalculateCoastalProvinces { + fn apply(&self, map: &mut Map) { + for (&color, province_data) in map.province_data_map.iter_mut() { + province_data.coastal = self.coastal_provinces[&color]; + }; + } + + fn apply_generate(&self, map: &mut Map) -> Self { + let mut coastal_provinces = fx_hash_map_with_capacity(self.coastal_provinces.len()); + for (&color, province_data) in map.province_data_map.iter_mut() { + let coastal = self.coastal_provinces[&color]; + let coastal = mem::replace(&mut province_data.coastal, coastal); + coastal_provinces.insert(color, coastal); + }; + + let extents = self.extents; + CalculateCoastalProvinces { coastal_provinces, extents } + } + + fn view_mode(&self) -> ViewMode { + ViewMode::Coastal + } + + fn extents(&self) -> Extents { + self.extents + } +} + +#[derive(Debug, Clone)] +pub struct CalculateRecolorMap { + pub(super) replacement_map: FxHashMap, + pub(super) extents: Extents +} + +impl CalculateRecolorMap { + pub(super) fn new(bundle: &Bundle) -> Option { + let mut colors_list = fx_hash_set_with_capacity(bundle.map.provinces_count()); + let mut replacement_map = fx_hash_map_with_capacity(bundle.map.provinces_count()); + for (&previous_color, province_data) in bundle.map.province_data_map.iter() { + let color = random_color_pure(&colors_list, &bundle.rng, province_data.kind); + let opt = colors_list.insert(color); + debug_assert!(opt); + let opt = replacement_map.insert(previous_color, color); + debug_assert_eq!(opt, None); + }; + + let [width, height] = bundle.map.dimensions(); + let extents = Extents::new([width - 1, height - 1], [0, 0]); + Some(CalculateRecolorMap { replacement_map, extents }) + } +} + +impl MapOperation for CalculateRecolorMap { + fn apply(&self, map: &mut Map) { + let mut new_province_data_map = fx_hash_map_with_capacity(map.provinces_count()); + for (previous_color, province_data) in map.province_data_map.drain() { + let color = self.replacement_map[&previous_color]; + let opt = new_province_data_map.insert(color, province_data); + debug_assert_eq!(opt, None); + }; + + map.province_data_map = new_province_data_map; + + let mut new_connection_data_map = fx_hash_map_with_capacity(map.connection_data_map.len()); + for (previous_rel, connection_data) in map.connection_data_map.drain() { + let rel = previous_rel.map(|color| self.replacement_map[&color]); + // This operation should never overwrite an existing entry + let opt = new_connection_data_map.insert(rel, connection_data); + debug_assert_eq!(opt, None); + }; + + map.connection_data_map = new_connection_data_map; + + for Rgb(pixel) in map.color_buffer.pixels_mut() { + *pixel = self.replacement_map[pixel]; + }; + } + + fn apply_generate(&self, map: &mut Map) -> Self { + self.apply(map); + let mut replacement_map = fx_hash_map_with_capacity(self.replacement_map.len()); + for (&which, &color) in self.replacement_map.iter() { + replacement_map.insert(color, which); + }; + + let extents = self.extents; + CalculateRecolorMap { replacement_map, extents } + } + + fn view_mode(&self) -> ViewMode { + ViewMode::Color + } + + fn extents(&self) -> Extents { + self.extents + } +} + +#[derive(Debug, Clone)] +pub struct PaintProvinceMeta { + pub(super) which: Color, + pub(super) repaint: Option, + pub(super) kind: ProvinceKind, + pub(super) terrain: String, + pub(super) continent: u16, + pub(super) view_mode: ViewMode, + pub(super) extents: Extents +} + +impl PaintProvinceMeta { + pub(super) fn new_change_kind(bundle: &Bundle, which: Color, kind: ProvinceKind) -> Option { + let province_data = bundle.map.get_province(which); + if province_data.kind != kind && kind != ProvinceKind::Unknown { + let terrain = bundle.config.default_terrain(kind); + //let terrain = bundle.config.default_terrain(kind); + let continent = kind.correct_continent_id(province_data.continent); + // Because the type changed, a repaint is always necessary + let repaint = Some(bundle.random_color_pure(kind)); + Some(PaintProvinceMeta { + which, repaint, + kind, terrain, continent, + view_mode: ViewMode::Kind, + extents: get_color_extents(&bundle.map, which) + }) + } else { + None + } + } + + pub(super) fn new_change_terrain(bundle: &Bundle, which: Color, terrain: String) -> Option { + let province_data = bundle.map.get_province(which); + if province_data.terrain != terrain { + let kind = bundle.config.terrain_kind(&terrain) + .unwrap_or(ProvinceKind::Unknown); + let continent = kind.correct_continent_id(province_data.continent); + // If the type changed, generate a new color for it + let repaint = (province_data.kind != kind) + .then(|| bundle.random_color_pure(kind)); + Some(PaintProvinceMeta { + which, repaint, + kind, terrain, continent, + view_mode: ViewMode::Terrain, + extents: get_color_extents(&bundle.map, which) + }) + } else { + None + } + } +} + +impl MapOperation for PaintProvinceMeta { + fn apply(&self, map: &mut Map) { + let province_data = map.get_province_mut(self.which); + province_data.kind = self.kind; + province_data.terrain = self.terrain.clone(); + province_data.continent = self.continent; + if let Some(fill_color) = self.repaint { + map.recolor_province(self.which, fill_color); + }; + } + + fn apply_generate(&self, map: &mut Map) -> Self { + let province_data = map.get_province_mut(self.which); + let kind = mem::replace(&mut province_data.kind, self.kind); + let terrain = mem::replace(&mut province_data.terrain, self.terrain.clone()); + let continent = mem::replace(&mut province_data.continent, self.continent); + let (which, repaint) = if let Some(fill_color) = self.repaint { + map.recolor_province(self.which, fill_color); + (fill_color, Some(self.which)) + } else { + (self.which, None) + }; + + PaintProvinceMeta { + which, + repaint, + kind, + terrain, + continent, + view_mode: self.view_mode, + extents: self.extents + } + } + + fn view_mode(&self) -> ViewMode { + self.view_mode + } + + fn extents(&self) -> Extents { + self.extents + } +} + +#[derive(Debug, Clone)] +pub struct PaintProvinceContinent { + pub(super) which: Color, + pub(super) continent: u16, + pub(super) extents: Extents +} + +impl PaintProvinceContinent { + pub(super) fn new(bundle: &Bundle, which: Color, continent: u16) -> Option { + let province_data = bundle.map.get_province(which); + let valid_continent = province_data.kind.valid_continent_id(continent); + if province_data.continent != continent && valid_continent { + let extents = get_color_extents(&bundle.map, which); + Some(PaintProvinceContinent { which, continent, extents }) + } else { + None + } + } +} + +impl MapOperation for PaintProvinceContinent { + fn apply(&self, map: &mut Map) { + map.get_province_mut(self.which).continent = self.continent; + } + + fn apply_generate(&self, map: &mut Map) -> Self { + let province_data = map.get_province_mut(self.which); + let continent = mem::replace(&mut province_data.continent, self.continent); + PaintProvinceContinent { + which: self.which, + continent, + extents: self.extents + } + } + + fn view_mode(&self) -> ViewMode { + ViewMode::Continent + } + + fn extents(&self) -> Extents { + self.extents + } +} + +#[derive(Debug, Clone)] +pub struct PaintRecolorProvince { + pub(super) which: Color, + pub(super) fill_color: Color, + pub(super) extents: Extents +} + +impl PaintRecolorProvince { + pub(super) fn new(bundle: &Bundle, which: Color, fill_color: Color) -> Option { + if which != fill_color { + let extents = get_color_extents(&bundle.map, which); + Some(PaintRecolorProvince { which, fill_color, extents }) + } else { + None + } + } +} + +impl MapOperation for PaintRecolorProvince { + fn apply(&self, map: &mut Map) { + map.recolor_province(self.which, self.fill_color); + } + + fn apply_generate(&self, map: &mut Map) -> Self { + map.recolor_province(self.which, self.fill_color); + // Simply swapping `which` and `fill_color` gives us the previous state + PaintRecolorProvince { + which: self.fill_color, + fill_color: self.which, + extents: self.extents + } + } + + fn view_mode(&self) -> ViewMode { + ViewMode::Color + } + + fn extents(&self) -> Extents { + self.extents + } +} + +#[derive(Debug, Clone)] +pub struct PaintPixels { + pub(super) affected_pixels: Vec<(Vector2, Color)>, + pub(super) extents: Extents +} + +impl MapOperation for PaintPixels { + fn apply(&self, map: &mut Map) { + map.put_many_pixels(&self.affected_pixels); + } + + fn apply_generate(&self, map: &mut Map) -> Self { + let affected_pixels = self.affected_pixels.iter() + .map(|&(pos, _)| (pos, map.get_color_at(pos))) + .collect(); + map.put_many_pixels(&self.affected_pixels); + PaintPixels { + affected_pixels, + extents: self.extents + } + } + + fn view_mode(&self) -> ViewMode { + ViewMode::Color + } + + fn extents(&self) -> Extents { + self.extents + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PaintPixelArea { + pub(super) pos: Vector2, + pub(super) radius: f64, + pub(super) color: Color, + pub(super) extents: Extents +} + +impl PaintPixelArea { + pub(super) fn new(bundle: &Bundle, pos: Vector2, radius: f64, color: Color) -> Option { + if !is_pixel_area_pointless(&bundle.map, pos, radius, color) { + let extents = Extents::new_pos_radius(pos, radius, bundle.map.dimensions()); + Some(PaintPixelArea { pos, radius, color, extents }) + } else { + None + } + } +} + +impl MapOperationPartial for PaintPixelArea { + fn insert(self, map: &mut Map, history: &mut History) -> Extents { + let [before, after] = pixel_area(map, self.pos, self.radius, self.color); + map.put_many_pixels(&after); + match history.steps.back_mut() { + Some(Step::PaintPixels(op)) if !op.done => { + let extents = op.before.extents.join(self.extents); + op.before.affected_pixels.extend(before); + op.before.extents = extents; + op.after.affected_pixels.extend(after); + op.after.extents = extents; + }, + _ => history.push(MapChange { + before: PaintPixels { + affected_pixels: before, + extents: self.extents + }, + after: PaintPixels { + affected_pixels: after, + extents: self.extents + }, + done: false + }) + }; + + self.extents + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaintPixel { + pos: Vector2, + color: Color +} + +impl PaintPixel { + pub(super) fn new(bundle: &Bundle, pos: Vector2, color: Color) -> Option { + if bundle.map.get_color_at(pos) != color { + Some(PaintPixel { pos, color }) + } else { + None + } + } +} + +impl MapOperationPartial for PaintPixel { + fn insert(self, map: &mut Map, history: &mut History) -> Extents { + let color = map.get_color_at(self.pos); + map.put_pixel(self.pos, self.color); + let extents = Extents::new_point(self.pos); + let (before, after) = (PaintPixel { pos: self.pos, color }, self); + match history.steps.back_mut() { + Some(Step::PaintPixels(op)) if !op.done => { + let combined_extents = op.before.extents.join(extents); + op.before.affected_pixels.push(before.into()); + op.before.extents = combined_extents; + op.after.affected_pixels.push(after.into()); + op.after.extents = combined_extents; + }, + _ => history.push(MapChange::new(before, after)) + }; + + extents + } +} + +impl Into<(Vector2, Color)> for PaintPixel { + fn into(self) -> (Vector2, Color) { + (self.pos, self.color) + } +} + +impl PartialEq<(Vector2, Color)> for PaintPixel { + fn eq(&self, (pos, color): &(Vector2, Color)) -> bool { + self.pos == *pos && self.color == *color + } +} + +fn pixel_area(map: &Map, pos: Vector2, radius: f64, color: Color) -> [Vec<(Vector2, Color)>; 2] { + let mut before = Vec::new(); + let mut after = Vec::new(); + for [x, y] in XYIter::from_extents(Extents::new_pos_radius(pos, radius, map.dimensions())) { + let distance = f64::hypot(x as f64 + 0.5 - pos[0], y as f64 + 0.5 - pos[1]); + let previous_color = map.get_color_at([x, y]); + if distance < radius && color != previous_color { + before.push(([x, y], previous_color)); + after.push(([x, y], color)); + }; + }; + + [before, after] +} + +fn is_pixel_area_pointless(map: &Map, pos: Vector2, radius: f64, color: Color) -> bool { + for [x, y] in XYIter::from_extents(Extents::new_pos_radius(pos, radius, map.dimensions())) { + let distance = f64::hypot(x as f64 + 0.5 - pos[0], y as f64 + 0.5 - pos[1]); + let previous_color = map.get_color_at([x, y]); + if distance < radius && color != previous_color { + return false; + }; + }; + + true +} + +fn get_color_extents(map: &Map, which: Color) -> Extents { + let mut out: Option = None; + + for (x, y, &Rgb(pixel)) in map.color_buffer.enumerate_pixels() { + if pixel == which { + out = Some(out.map_or_else( + || Extents::new_point([x, y]), + |extents| extents.join_point([x, y]) + )); + }; + }; + + out.expect("color not found in map") +} diff --git a/src/app/map/problems.rs b/src/app/map/problems.rs new file mode 100644 index 0000000..e7b5408 --- /dev/null +++ b/src/app/map/problems.rs @@ -0,0 +1,159 @@ +use fxhash::FxHashMap; +use graphics::color::RED; +use graphics::context::Context; +use graphics::rectangle::Rectangle; +use graphics::ellipse::Ellipse; +use opengl_graphics::GlGraphics; +use vecmath::{Matrix2x3, Vector2}; + +use super::{Bundle, Map, Extents}; +use crate::util::XYIter; + +use std::collections::hash_map::Entry; +use std::fmt; + + + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Problem { + InvalidXCrossing(Vector2), + TooLargeBox(Extents), + TooFewPixels(u64, Vector2), + InvalidWidth, + InvalidHeight, +} + +impl Problem { + pub fn draw(&self, ctx: Context, display_matrix: Matrix2x3, gl: &mut GlGraphics) { + match *self { + Problem::InvalidXCrossing(pos) => { + let pos = vec2_u32_to_f64(pos); + draw_cross(pos, ctx.transform, display_matrix, gl); + }, + Problem::TooLargeBox(extents) => { + let lower = vec2_u32_to_f64(extents.lower); + let upper = vec2_u32_to_f64(extents.upper); + let upper = vecmath::vec2_add(upper, [1.0; 2]); + draw_box([lower, upper], ctx.transform, display_matrix, gl); + }, + Problem::TooFewPixels(_, pos) => { + let pos = vecmath::vec2_add(pos, [0.5; 2]); + draw_dot(pos, ctx.transform, display_matrix, gl); + }, + _ => () + } + } +} + +impl fmt::Display for Problem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Problem::InvalidXCrossing(pos) => { + write!(f, "Invalid X crossing at {:?}", pos) + }, + Problem::TooLargeBox(extents) => { + write!(f, "Province has too large box from {:?} to {:?}", extents.upper, extents.lower) + }, + Problem::TooFewPixels(count, pos) => { + write!(f, "Province has only {} pixels around {:?}", count, pos) + }, + Problem::InvalidWidth => { + write!(f, "Map texture width is not a multiple of 64") + }, + Problem::InvalidHeight => { + write!(f, "Map texture height is not a multiple of 64") + } + } + } +} + +fn draw_cross(pos: Vector2, transform: Matrix2x3, display_matrix: Matrix2x3, gl: &mut GlGraphics) { + let [x, y] = vecmath::row_mat2x3_transform_pos2(display_matrix, pos); + graphics::line_from_to(RED, 2.0, [x - 8.0, y - 8.0], [x + 8.0, y + 8.0], transform, gl); + graphics::line_from_to(RED, 2.0, [x - 8.0, y + 8.0], [x + 8.0, y - 8.0], transform, gl); +} + +fn draw_dot(pos: Vector2, transform: Matrix2x3, display_matrix: Matrix2x3, gl: &mut GlGraphics) { + let [x, y] = vecmath::row_mat2x3_transform_pos2(display_matrix, pos); + Ellipse::new(RED).draw_from_to([x - 4.0, y - 4.0], [x + 4.0, y + 4.0], &Default::default(), transform, gl); +} + +fn draw_box(bounds: [Vector2; 2], transform: Matrix2x3, display_matrix: Matrix2x3, gl: &mut GlGraphics) { + let lower = vecmath::row_mat2x3_transform_pos2(display_matrix, bounds[0]); + let upper = vecmath::row_mat2x3_transform_pos2(display_matrix, bounds[1]); + Rectangle::new_border(RED, 1.0).draw_from_to(lower, upper, &Default::default(), transform, gl); +} + + + +pub fn analyze(bundle: &Bundle) -> Vec { + struct ProblemData { + extents: Extents, + position_sum: Vector2 + } + + let [width, height] = bundle.map.dimensions(); + let mut problems = Vec::new(); + let mut problem_data_map = FxHashMap::default(); + + for pos in XYIter::new(0..width, 0..height) { + if pos[0] != width - 1 && pos[1] != height - 1 { + if is_crossing_at(&bundle.map, pos) { + let pos = [pos[0] + 1, pos[1] + 1]; + problems.push(Problem::InvalidXCrossing(pos)); + }; + }; + + match problem_data_map.entry(bundle.map.get_color_at(pos)) { + Entry::Vacant(entry) => { + entry.insert(ProblemData { + extents: Extents::new_point(pos), + position_sum: [pos[0] as f64, pos[1] as f64] + }); + }, + Entry::Occupied(entry) => { + let entry = entry.into_mut(); + entry.extents = entry.extents.join_point(pos); + entry.position_sum[0] += pos[0] as f64; + entry.position_sum[1] += pos[1] as f64; + } + }; + }; + + for (color, problem_data) in problem_data_map { + let pixel_count = bundle.map.get_province(color).pixel_count; + if pixel_count <= 8 { + let pos = problem_data.position_sum; + let pos = [pos[0] / pixel_count as f64, pos[1] / pixel_count as f64]; + problems.push(Problem::TooFewPixels(pixel_count, pos)); + }; + + let extents = problem_data.extents; + let (_, [province_width, province_height]) = extents.to_offset_size(); + if province_width > width / 8 || province_height > height / 8 { + problems.push(Problem::TooLargeBox(extents)); + }; + }; + + if width % 64 != 0 { + problems.push(Problem::InvalidWidth); + }; + + if height % 64 != 0 { + problems.push(Problem::InvalidHeight); + }; + + problems +} + +fn is_crossing_at(map: &Map, [x, y]: Vector2) -> bool { + let a = map.get_color_at([x, y]); + let b = map.get_color_at([x + 1, y]); + let c = map.get_color_at([x, y + 1]); + let d = map.get_color_at([x + 1, y + 1]); + a != b && c != d && b != d && a != c && a != d && b != c +} + +fn vec2_u32_to_f64(pos: Vector2) -> Vector2 { + [pos[0] as f64, pos[1] as f64] +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..19c743b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,166 @@ +use fxhash::FxHashMap; +use itertools::Itertools; +use serde::{Serialize, Deserialize}; +use serde_multi::formats::toml; +use util_macros::error_enum; + +use crate::app::map::Color; +use crate::app::map::ProvinceKind; +use crate::app::format::DefinitionKind; +use crate::util::fx_hash_map_with_capacity; + +use std::fs; + +const DEFAULT_CONFIG: &[u8] = include_bytes!("../assets/hoi4pe_config_default.toml"); + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct Config { + pub max_undo_states: usize, + #[serde(alias = "recolour_provinces")] + pub preserve_ids: bool, + #[serde(rename = "terrain", skip_serializing_if = "FxHashMap::is_empty")] + pub terrains: FxHashMap +} + +impl Config { + pub fn load() -> Result { + use std::io::ErrorKind; + let mut config = match fs::read("hoi4pe_config.toml") { + Ok(data) => toml::from_slice::(&data)?, + Err(err) if err.kind() == ErrorKind::NotFound => { + fs::write("hoi4pe_config.toml", DEFAULT_CONFIG)?; + Config::default() + }, + Err(err) => return Err(err.into()) + }; + + add_default_terrains(&mut config.terrains); + Ok(config) + } + + pub fn terrain_color(&self, terrain: &str) -> Option { + self.terrains.get(terrain).map(|t| t.color) + } + + pub fn terrain_kind(&self, terrain: &str) -> Option { + self.terrains.get(terrain).map(|t| t.kind) + } + + pub fn cycle_kinds

(&self, kind: Option

) -> DefinitionKind + where P: Into { + match kind.map(P::into) { + Some(ProvinceKind::Land) => DefinitionKind::Sea, + Some(ProvinceKind::Sea) => DefinitionKind::Lake, + Some(ProvinceKind::Lake) => DefinitionKind::Land, + Some(ProvinceKind::Unknown) | None => DefinitionKind::Land + } + } + + pub fn cycle_terrains(&self, terrain: Option<&str>) -> String { + if let Some(target_terrain) = terrain { + for (terrain, next_terrain) in self.terrains.keys().tuple_windows() { + if terrain == target_terrain { + return next_terrain.clone(); + }; + }; + }; + + self.terrains.keys().next() + .expect("infallible") + .clone() + } + + pub fn cycle_continents(&self, continent: Option) -> u16 { + continent.map_or(0, |continent| (continent + 1) % 16) + } + + pub fn kind_color(&self, kind: impl Into) -> Color { + match kind.into() { + ProvinceKind::Land => [0x0a, 0xae, 0x3d], + ProvinceKind::Sea => [0x00, 0x4c, 0x9e], + ProvinceKind::Lake => [0x24, 0xab, 0xff], + ProvinceKind::Unknown => [0x22, 0x22, 0x22] + } + } + + pub fn coastal_color(&self, coastal: Option, kind: impl Into) -> Color { + match (coastal, kind.into()) { + (Some(false), ProvinceKind::Land) => [0x00, 0x33, 0x11], + (Some(true), ProvinceKind::Land) => [0x33, 0x99, 0x55], + (Some(false), ProvinceKind::Sea) => [0x00, 0x11, 0x33], + (Some(true), ProvinceKind::Sea) => [0x33, 0x55, 0x99], + (Some(false), ProvinceKind::Lake) => [0x00, 0x33, 0x33], + (Some(true), ProvinceKind::Lake) => [0x33, 0x99, 0x99], + _ => [0x22, 0x22, 0x22] + } + } + + pub fn default_terrain(&self, kind: impl Into) -> String { + match kind.into() { + ProvinceKind::Unknown => "unknown".to_owned(), + ProvinceKind::Land => "plains".to_owned(), + ProvinceKind::Sea => "ocean".to_owned(), + ProvinceKind::Lake => "lakes".to_owned() + } + } +} + +impl Default for Config { + fn default() -> Config { + Config { + max_undo_states: 24, + preserve_ids: false, + terrains: default_terrains() + } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct Terrain { + #[serde(alias = "colour")] + pub color: Color, + #[serde(rename = "type")] + pub kind: ProvinceKind +} + +error_enum!{ + pub enum LoadConfigError { + IoError(std::io::Error), + FormatError(serde_multi::Error), + Custom(String) + } +} + +fn default_terrains() -> FxHashMap { + let mut terrains = fx_hash_map_with_capacity(DEFAULT_TERRAINS.len()); + for &(color, name, kind) in DEFAULT_TERRAINS { + terrains.insert(name.to_owned(), Terrain { color, kind }); + }; + + terrains +} + +fn add_default_terrains(terrains: &mut FxHashMap) { + // The 'unknown' terrain should not be user-overloadable + terrains.remove("unknown"); + for &(color, name, kind) in DEFAULT_TERRAINS { + let terrain = Terrain { color, kind }; + terrains.entry(name.to_owned()) + .or_insert(terrain); + }; +} + +const DEFAULT_TERRAINS: &[(Color, &str, ProvinceKind)] = &[ + ([0x00, 0x00, 0x00], "unknown", ProvinceKind::Unknown), + ([0xff, 0x81, 0x42], "plains", ProvinceKind::Land), + ([0xff, 0x3f, 0x00], "desert", ProvinceKind::Land), + ([0x59, 0xc7, 0x55], "forest", ProvinceKind::Land), + ([0xf8, 0xff, 0x99], "hills", ProvinceKind::Land), + ([0x7f, 0xbf, 0x00], "jungle", ProvinceKind::Land), + ([0x4c, 0x60, 0x23], "marsh", ProvinceKind::Land), + ([0x7c, 0x87, 0x7d], "mountain", ProvinceKind::Land), + ([0x00, 0xff, 0xff], "lakes", ProvinceKind::Lake), + ([0x00, 0x00, 0xff], "ocean", ProvinceKind::Sea), + ([0x9b, 0x00, 0xff], "urban", ProvinceKind::Land) +]; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..763fbb2 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,19 @@ +use util_macros::error_enum; + +error_enum!{ + pub enum Error { + Io(std::io::Error), + Zip(zip::result::ZipError), + Image(image::ImageError), + Csv(crate::util::csv::CsvError), + Custom(String) + } +} + +impl From<&str> for Error { + fn from(s: &str) -> Error { + Error::Custom(s.to_owned()) + } +} + +pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5e0ba3b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,88 @@ +extern crate better_panic; +extern crate fxhash; +extern crate image; +extern crate native_dialog; +extern crate opengl_graphics; +extern crate glutin; +extern crate glutin_window; +extern crate graphics; +extern crate piston; +extern crate rand; +extern crate rusttype; +extern crate serde; +extern crate serde_multi; +extern crate util_macros; +extern crate vecmath; +extern crate zip; + +pub mod app; +pub mod config; +pub mod error; +pub mod util; + +use glutin::window::CursorIcon; +use glutin_window::GlutinWindow; +use opengl_graphics::{GlGraphics, OpenGL}; +use piston::event_loop::{EventSettings, Events}; +use piston::input::{Event, Loop, Input, FileDrag}; +use piston::window::{Size, WindowSettings}; + +use crate::app::App; + +const WINDOW_WIDTH: u32 = 1280; +const WINDOW_HEIGHT: u32 = 720; +const SCREEN: Size = Size { + width: WINDOW_WIDTH as f64, + height: WINDOW_HEIGHT as f64 +}; + +pub const APPNAME: &str = concat!("HOI4 Province Map Editor v", env!("CARGO_PKG_VERSION")); + +fn main() { + better_panic::install(); + + let opengl = OpenGL::V3_2; + let mut window: GlutinWindow = WindowSettings::new(APPNAME, SCREEN) + .graphics_api(opengl).resizable(false).vsync(true) + .build().expect("unable to initialize window"); + + let mut gl = GlGraphics::new(opengl); + let mut app = App::new(&mut gl); + let mut cursor = CursorIcon::Default; + let mut init = true; + + let mut events = Events::new(EventSettings::new()); + while let Some(event) = events.next(&mut window) { + match event { + Event::Loop(loop_event) => match loop_event { + Loop::Update(args) => app.on_update_event(args), + Loop::Render(args) => gl.draw(args.viewport(), |ctx, gl| { + app.on_render_event(args, ctx, gl); + }), + Loop::AfterRender(_) if init => { + app.on_init(); + init = false; + }, + _ => () + }, + Event::Input(event, _) => match event { + Input::Button(args) => app.on_button_event(args), + Input::Move(motion) => app.on_motion_event(motion), + Input::Text(string) => app.on_text_event(string), + Input::FileDrag(FileDrag::Drop(path)) => app.on_file_drop(path), + Input::Focus(false) | Input::Cursor(false) => app.on_unfocus(), + Input::Close(_) => { + app.on_close(); + break; + }, + _ => () + }, + _ => () + }; + + if app.cursor != cursor { + cursor = app.cursor; + window.ctx.window().set_cursor_icon(cursor); + }; + }; +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..274ee36 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,79 @@ +pub mod csv; +pub mod hsl; +pub mod random; +pub mod uord; + +use fxhash::{FxHashMap, FxHashSet, FxBuildHasher}; +use vecmath::Vector2; + +use crate::app::map::{Color, Extents}; + +use std::ops::Range; + +pub struct XYIter { + x_range: Range, + y_range: Range, + x: u32, + y: u32, + done: bool +} + +impl XYIter { + pub fn new(x: Range, y: Range) -> Self { + XYIter { + x: x.start, + y: y.start, + x_range: x, + y_range: y, + done: false + } + } + + pub fn from_extents(extents: Extents) -> Self { + let x = extents.lower[0]..(extents.upper[0] + 1); + let y = extents.lower[1]..(extents.upper[1] + 1); + XYIter::new(x, y) + } +} + +impl Iterator for XYIter { + type Item = Vector2; + + fn next(&mut self) -> Option { + if self.done { + None + } else { + let item = [self.x, self.y]; + self.x += 1; + if self.x >= self.x_range.end { + self.y += 1; + self.x = self.x_range.start; + if self.y >= self.y_range.end { + self.done = true; + self.y = self.y_range.start; + }; + }; + + Some(item) + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.x_range.len() * self.y_range.len(); + (len, Some(len)) + } +} + +impl ExactSizeIterator for XYIter {} + +pub fn stringify_color(color: Color) -> String { + format!("({}, {}, {})", color[0], color[1], color[2]) +} + +pub fn fx_hash_map_with_capacity(capacity: usize) -> FxHashMap { + FxHashMap::with_capacity_and_hasher(capacity, FxBuildHasher::default()) +} + +pub fn fx_hash_set_with_capacity(capacity: usize) -> FxHashSet { + FxHashSet::with_capacity_and_hasher(capacity, FxBuildHasher::default()) +} diff --git a/src/util/csv.rs b/src/util/csv.rs new file mode 100644 index 0000000..0bf9541 --- /dev/null +++ b/src/util/csv.rs @@ -0,0 +1,105 @@ +//! CSV parsing and serialization utilities designed specifically for HOI4 +use std::io::prelude::*; +use std::io; +use std::fmt; + +pub const LINE_BREAK: &str = "\r\n"; +pub const SEPARATOR: char = ';'; + +fn split_line(line: &str) -> Result<[String; COLUMNS], CsvError> { + use std::convert::TryInto; + let line = line.split(SEPARATOR) + .map(str::to_owned) + .collect::>(); + match line.try_into() { + Ok(line) => Ok(line), + Err(_) => Err(CsvError::IncorrectColumnCount) + } +} + +fn should_ignore_line, const COLUMNS: usize>(s: &str, i: usize) -> bool { + i == 0 || s.is_empty() || Some(s) == P::HEADER_LINE || Some(s) == P::FOOTER_LINE +} + +pub trait ParseCsv: Sized + ToString { + const HEADER_LINE: Option<&'static str>; + const FOOTER_LINE: Option<&'static str>; + + fn parse_line(line: [String; COLUMNS]) -> Option; + + fn parse_all(reader: R) -> Result, CsvError> { + let mut out = Vec::with_capacity(COLUMNS); + for (i, raw_line) in reader.lines().enumerate() { + let raw_line = raw_line?; + if should_ignore_line::(&raw_line, i) { + continue; + } else { + let line = split_line::(&raw_line)?; + let line = Self::parse_line(line) + .ok_or(CsvError::ParsingFailed(raw_line))?; + out.push(line); + }; + }; + + Ok(out) + } + + fn stringify_line(&self) -> String { + let mut line = self.to_string(); + line.push_str(LINE_BREAK); + line + } + + fn stringify_all(entries: &[Self]) -> String { + let mut out = String::new(); + + if let Some(header) = Self::HEADER_LINE { + out.push_str(header); + out.push_str(LINE_BREAK); + }; + + for entry in entries { + out.push_str(&entry.stringify_line()); + }; + + if let Some(footer) = Self::FOOTER_LINE { + out.push_str(footer); + out.push_str(LINE_BREAK); + }; + + out + } +} + +#[derive(Debug)] +pub enum CsvError { + IoError(io::Error), + ParsingFailed(String), + IncorrectColumnCount +} + +impl fmt::Display for CsvError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CsvError::IoError(err) => fmt::Display::fmt(err, f), + CsvError::ParsingFailed(line) => write!(f, "failed to parse csv line: {:?}", line), + CsvError::IncorrectColumnCount => write!(f, "too few or too many columns were found") + } + } +} + +impl std::error::Error for CsvError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + CsvError::IoError(err) => err.source(), + CsvError::ParsingFailed(_) => None, + CsvError::IncorrectColumnCount => None + } + } +} + +impl From for CsvError { + fn from(err: io::Error) -> CsvError { + CsvError::IoError(err) + } +} diff --git a/src/util/hsl.rs b/src/util/hsl.rs new file mode 100644 index 0000000..e060324 --- /dev/null +++ b/src/util/hsl.rs @@ -0,0 +1,50 @@ +// Code borrowed from https://github.com/DerivedMate/hsl-ish + +pub fn hsl_to_rgb(hsl: [f64; 3]) -> [u8; 3] { + fn hue2rgb(p: f64, q: f64, t: f64) -> f64 { + let mut t = t; + if t < 0.0 { + t += 1.0; + } else if t > 1.0 { + t -= 1.0; + }; + + if t < 1.0 / 6.0 { + p + (q - p) * 6.0 * t + } else if t < 1.0 / 2.0 { + q + } else if t < 2.0 / 3.0 { + p + (q - p) * (2.0 / 3.0 - t) * 6.0 + } else { + p + } + } + + let (h, s, l) = (hsl[0] / 360.0, hsl[1], hsl[2]); + let r; + let g; + let b; + + if s == 0.0 { + r = l; + g = l; + b = l; + } else { + let q = if l < 0.5 { + l * (1.0 + s) + } else { + l + s - l * s + }; + + let p = 2.0 * l - q; + r = hue2rgb(p, q, h + 1.0 / 3.0); + g = hue2rgb(p, q, h + 0.0 / 3.0); + b = hue2rgb(p, q, h - 1.0 / 3.0); + }; + + [ + (r * 255.0).round() as u8, + (g * 255.0).round() as u8, + (b * 255.0).round() as u8 + ] +} diff --git a/src/util/random.rs b/src/util/random.rs new file mode 100644 index 0000000..3b6f705 --- /dev/null +++ b/src/util/random.rs @@ -0,0 +1,73 @@ +use rand::{Rng, SeedableRng}; +use rand::rngs::SmallRng; +use rand::distributions::{Distribution, Standard}; +use rand::distributions::uniform::{SampleRange, SampleUniform}; + +use crate::app::map::Color; +use crate::util::hsl::hsl_to_rgb; + +use std::cell::RefCell; +use std::rc::Rc; + +/// An RNG utility struct that can be multiple-owned +#[derive(Debug)] +pub struct RandomHandle { + ptr: Rc> +} + +impl RandomHandle { + pub fn new() -> Self { + let rng = SmallRng::from_entropy(); + let sequence_rng = SmallRng::seed_from_u64(0x938b902e4f56bf5b); + let inner = RandomHandleInner { rng, sequence_rng, sequence: vec![[0; 3]] }; + RandomHandle { ptr: Rc::new(RefCell::new(inner)) } + } + + pub fn reseed_entropy(&self) { + self.ptr.borrow_mut().rng = SmallRng::from_entropy(); + } + + pub fn reseed(&self, seed: u64) { + self.ptr.borrow_mut().rng = SmallRng::seed_from_u64(seed); + } + + pub fn gen(&self) -> T + where Standard: Distribution { + self.ptr.borrow_mut().rng.gen::() + } + + pub fn gen_range(&self, range: R) -> T + where T: SampleUniform, R: SampleRange { + self.ptr.borrow_mut().rng.gen_range::(range) + } + + pub fn sequence_color(&self, index: usize) -> Color { + self.ptr.borrow_mut().sequence_color(index) + } +} + +impl Clone for RandomHandle { + fn clone(&self) -> Self { + RandomHandle { ptr: Rc::clone(&self.ptr) } + } +} + +#[derive(Debug)] +struct RandomHandleInner { + rng: SmallRng, + sequence_rng: SmallRng, + sequence: Vec +} + +impl RandomHandleInner { + fn sequence_color(&mut self, index: usize) -> Color { + while self.sequence.len() < index + 1 { + let h = self.sequence_rng.gen_range(0.0..360.0); + let l = self.sequence_rng.gen_range(0.25..0.75); + let color = hsl_to_rgb([h, 1.0, l]); + self.sequence.push(color); + }; + + self.sequence[index] + } +} diff --git a/src/util/uord.rs b/src/util/uord.rs new file mode 100644 index 0000000..40d2b21 --- /dev/null +++ b/src/util/uord.rs @@ -0,0 +1,101 @@ +use std::cmp::Ordering; +use std::borrow::Borrow; +use std::hash::{Hash, Hasher}; +use std::fmt; + +pub struct UOrd { + a: T, + b: T +} + +impl UOrd { + #[inline(always)] + pub const fn new(a: T, b: T) -> UOrd { + UOrd { a, b } + } + + #[inline] + pub fn contains(&self, x: &Q) -> bool + where T: Borrow, Q: Eq { + self.a.borrow() == x || self.b.borrow() == x + } + + pub fn replace(self, from: T, to: T) -> Self + where T: PartialEq + Copy { + let a = if self.a == from { to } else { self.a }; + let b = if self.b == from { to } else { self.b }; + UOrd { a, b } + } + + pub fn as_tuple(&self) -> (&T, &T) + where T: Ord { + let UOrd { a, b } = self; + match T::cmp(&a, &b) { + Ordering::Less | Ordering::Equal => (a, b), + Ordering::Greater => (b, a) + } + } + + pub fn into_tuple(self) -> (T, T) + where T: Ord { + let UOrd { a, b } = self; + match T::cmp(&a, &b) { + Ordering::Less | Ordering::Equal => (a, b), + Ordering::Greater => (b, a) + } + } + + pub fn map(self, mut f: F) -> UOrd + where F: FnMut(T) -> U { + UOrd::new(f(self.a), f(self.b)) + } +} + +impl fmt::Debug for UOrd { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.as_tuple(), f) + } +} + +impl Copy for UOrd {} + +impl Clone for UOrd { + #[inline(always)] + fn clone(&self) -> UOrd { + UOrd { + a: self.a.clone(), + b: self.b.clone() + } + } +} + +impl From<(T, T)> for UOrd { + #[inline(always)] + fn from(value: (T, T)) -> UOrd { + UOrd { a: value.0, b: value.1 } + } +} + +impl Into<(T, T)> for UOrd { + #[inline(always)] + fn into(self) -> (T, T) { + self.into_tuple() + } +} + +impl PartialEq for UOrd { + #[inline] + fn eq(&self, other: &UOrd) -> bool { + (self.a == other.a && self.b == other.b) || + (self.a == other.b && self.b == other.a) + } +} + +impl Eq for UOrd {} + +impl Hash for UOrd { + #[inline] + fn hash(&self, state: &mut H) { + self.as_tuple().hash(state); + } +}