├── .cargo └── config.toml ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── README.md ├── assets └── thumb_header.dat ├── build.bat ├── build.rs ├── desktop.ini ├── save.ico └── src ├── asset.rs ├── code_form.rs ├── compiler.rs ├── delphi.rs ├── events.rs ├── font_render.rs ├── ide.rs ├── lib.rs ├── list.rs ├── load.rs ├── regular.rs ├── regular ├── extension_watcher.rs └── project_watcher.rs ├── save.rs ├── save_exe.rs └── stub.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target="i686-pc-windows-msvc" 3 | 4 | [target.i686-pc-windows-msvc] 5 | rustflags = ["-Ctarget-feature=+crt-static"] 6 | 7 | [target.win7-pc-windows-msvc] 8 | rustflags = ["-Ctarget-feature=+crt-static"] 9 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: {} 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: windows-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: dtolnay/rust-toolchain@nightly 22 | with: 23 | components: rust-src 24 | - name: Build 25 | run: cargo build -Zbuild-std="std,panic_abort" --release --verbose --target=i686-win7-windows-msvc 26 | - name: Upload artifact 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: gm82save 30 | path: target/i686-win7-windows-msvc/release/gm82save.dll 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea/* 3 | /.vscode/ 4 | *.iml 5 | *.dll -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2024" # rust 2 | max_width = 120 # line width 3 | 4 | # Error if cannot get line into `max_width` boundaries. 5 | error_on_line_overflow = true 6 | 7 | # Error if cannot get comments/string literals into `max_width`. 8 | error_on_unformatted = true 9 | 10 | # Trail commas *on blocks* in match arms as well as single line expressions (default). 11 | match_block_trailing_comma = true 12 | 13 | # If they're separated, it's probably for good reason. 14 | merge_derives = false 15 | 16 | # Very non-intrusive. 17 | imports_granularity = "Crate" 18 | 19 | # Reformat all lines to LF (\n) in case they aren't for whatever reason. 20 | newline_style = "Unix" 21 | 22 | # Lets structs/arrays/blocks/macros etc. overflow when last argument in an expr list. 23 | # For example: foo(bar, baz, &[ 24 | # OVERFLOWING, 25 | # EXPRESSION, 26 | # ]) 27 | overflow_delimited_expr = true 28 | 29 | # Forces typedefs and consts to come before macros and methods in impl blocks. 30 | reorder_impl_items = true 31 | 32 | # Puts grouped (no extra newline, adjacent) imports in alphabetical order. 33 | reorder_imports = true 34 | 35 | # For break, continue, return... 36 | trailing_semicolon = false 37 | 38 | # Enables the usage of "unstable" features. 39 | unstable_features = true 40 | 41 | # Use different formatting for items/expr if they satisfy "small". 42 | # A good example of this is needlessly breaking vec.iter().map(|x| y).collect() into 4 lines, 43 | # or breaking up a function call with simple expressions but many arguments overall. 44 | use_small_heuristics = "Max" 45 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.4.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 25 | 26 | [[package]] 27 | name = "bitflags" 28 | version = "1.3.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "2.9.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 37 | 38 | [[package]] 39 | name = "byteorder" 40 | version = "1.5.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "crc32fast" 52 | version = "1.4.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 55 | dependencies = [ 56 | "cfg-if", 57 | ] 58 | 59 | [[package]] 60 | name = "crossbeam-channel" 61 | version = "0.5.15" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 64 | dependencies = [ 65 | "crossbeam-utils", 66 | ] 67 | 68 | [[package]] 69 | name = "crossbeam-deque" 70 | version = "0.8.6" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 73 | dependencies = [ 74 | "crossbeam-epoch", 75 | "crossbeam-utils", 76 | ] 77 | 78 | [[package]] 79 | name = "crossbeam-epoch" 80 | version = "0.9.18" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 83 | dependencies = [ 84 | "crossbeam-utils", 85 | ] 86 | 87 | [[package]] 88 | name = "crossbeam-utils" 89 | version = "0.8.21" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 92 | 93 | [[package]] 94 | name = "ctor" 95 | version = "0.4.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5" 98 | dependencies = [ 99 | "ctor-proc-macro", 100 | "dtor", 101 | ] 102 | 103 | [[package]] 104 | name = "ctor-proc-macro" 105 | version = "0.0.5" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" 108 | 109 | [[package]] 110 | name = "deranged" 111 | version = "0.4.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 114 | dependencies = [ 115 | "powerfmt", 116 | ] 117 | 118 | [[package]] 119 | name = "dtor" 120 | version = "0.0.6" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" 123 | dependencies = [ 124 | "dtor-proc-macro", 125 | ] 126 | 127 | [[package]] 128 | name = "dtor-proc-macro" 129 | version = "0.0.5" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" 132 | 133 | [[package]] 134 | name = "either" 135 | version = "1.15.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 138 | 139 | [[package]] 140 | name = "fdeflate" 141 | version = "0.3.7" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 144 | dependencies = [ 145 | "simd-adler32", 146 | ] 147 | 148 | [[package]] 149 | name = "filetime" 150 | version = "0.2.25" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 153 | dependencies = [ 154 | "cfg-if", 155 | "libc", 156 | "libredox", 157 | "windows-sys", 158 | ] 159 | 160 | [[package]] 161 | name = "flate2" 162 | version = "1.1.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 165 | dependencies = [ 166 | "crc32fast", 167 | "libz-rs-sys", 168 | "miniz_oxide", 169 | ] 170 | 171 | [[package]] 172 | name = "fsevent-sys" 173 | version = "4.1.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" 176 | dependencies = [ 177 | "libc", 178 | ] 179 | 180 | [[package]] 181 | name = "gm82save" 182 | version = "0.1.0" 183 | dependencies = [ 184 | "byteorder", 185 | "crossbeam-channel", 186 | "ctor", 187 | "flate2", 188 | "itertools", 189 | "lazy_static", 190 | "notify", 191 | "once_cell", 192 | "parking_lot", 193 | "png", 194 | "rayon", 195 | "regex", 196 | "sysinfo", 197 | "time", 198 | "unicase", 199 | ] 200 | 201 | [[package]] 202 | name = "inotify" 203 | version = "0.11.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" 206 | dependencies = [ 207 | "bitflags 2.9.1", 208 | "inotify-sys", 209 | "libc", 210 | ] 211 | 212 | [[package]] 213 | name = "inotify-sys" 214 | version = "0.1.5" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 217 | dependencies = [ 218 | "libc", 219 | ] 220 | 221 | [[package]] 222 | name = "itertools" 223 | version = "0.14.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 226 | dependencies = [ 227 | "either", 228 | ] 229 | 230 | [[package]] 231 | name = "kqueue" 232 | version = "1.1.1" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" 235 | dependencies = [ 236 | "kqueue-sys", 237 | "libc", 238 | ] 239 | 240 | [[package]] 241 | name = "kqueue-sys" 242 | version = "1.0.4" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" 245 | dependencies = [ 246 | "bitflags 1.3.2", 247 | "libc", 248 | ] 249 | 250 | [[package]] 251 | name = "lazy_static" 252 | version = "1.5.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 255 | 256 | [[package]] 257 | name = "libc" 258 | version = "0.2.172" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 261 | 262 | [[package]] 263 | name = "libredox" 264 | version = "0.1.3" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 267 | dependencies = [ 268 | "bitflags 2.9.1", 269 | "libc", 270 | "redox_syscall", 271 | ] 272 | 273 | [[package]] 274 | name = "libz-rs-sys" 275 | version = "0.5.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" 278 | dependencies = [ 279 | "zlib-rs", 280 | ] 281 | 282 | [[package]] 283 | name = "lock_api" 284 | version = "0.4.13" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 287 | dependencies = [ 288 | "autocfg", 289 | "scopeguard", 290 | ] 291 | 292 | [[package]] 293 | name = "log" 294 | version = "0.4.27" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 297 | 298 | [[package]] 299 | name = "memchr" 300 | version = "2.7.4" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 303 | 304 | [[package]] 305 | name = "miniz_oxide" 306 | version = "0.8.8" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 309 | dependencies = [ 310 | "adler2", 311 | "simd-adler32", 312 | ] 313 | 314 | [[package]] 315 | name = "mio" 316 | version = "1.0.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 319 | dependencies = [ 320 | "libc", 321 | "log", 322 | "wasi", 323 | "windows-sys", 324 | ] 325 | 326 | [[package]] 327 | name = "notify" 328 | version = "8.0.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" 331 | dependencies = [ 332 | "bitflags 2.9.1", 333 | "filetime", 334 | "fsevent-sys", 335 | "inotify", 336 | "kqueue", 337 | "libc", 338 | "log", 339 | "mio", 340 | "notify-types", 341 | "walkdir", 342 | "windows-sys", 343 | ] 344 | 345 | [[package]] 346 | name = "notify-types" 347 | version = "2.0.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" 350 | 351 | [[package]] 352 | name = "ntapi" 353 | version = "0.4.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 356 | dependencies = [ 357 | "winapi", 358 | ] 359 | 360 | [[package]] 361 | name = "num-conv" 362 | version = "0.1.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 365 | 366 | [[package]] 367 | name = "objc2-core-foundation" 368 | version = "0.3.1" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" 371 | dependencies = [ 372 | "bitflags 2.9.1", 373 | ] 374 | 375 | [[package]] 376 | name = "objc2-io-kit" 377 | version = "0.3.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" 380 | dependencies = [ 381 | "libc", 382 | "objc2-core-foundation", 383 | ] 384 | 385 | [[package]] 386 | name = "once_cell" 387 | version = "1.21.3" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 390 | 391 | [[package]] 392 | name = "parking_lot" 393 | version = "0.12.4" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 396 | dependencies = [ 397 | "lock_api", 398 | "parking_lot_core", 399 | ] 400 | 401 | [[package]] 402 | name = "parking_lot_core" 403 | version = "0.9.11" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 406 | dependencies = [ 407 | "cfg-if", 408 | "libc", 409 | "redox_syscall", 410 | "smallvec", 411 | "windows-targets", 412 | ] 413 | 414 | [[package]] 415 | name = "png" 416 | version = "0.17.16" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 419 | dependencies = [ 420 | "bitflags 1.3.2", 421 | "crc32fast", 422 | "fdeflate", 423 | "flate2", 424 | "miniz_oxide", 425 | ] 426 | 427 | [[package]] 428 | name = "powerfmt" 429 | version = "0.2.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 432 | 433 | [[package]] 434 | name = "proc-macro2" 435 | version = "1.0.95" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 438 | dependencies = [ 439 | "unicode-ident", 440 | ] 441 | 442 | [[package]] 443 | name = "quote" 444 | version = "1.0.40" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 447 | dependencies = [ 448 | "proc-macro2", 449 | ] 450 | 451 | [[package]] 452 | name = "rayon" 453 | version = "1.10.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 456 | dependencies = [ 457 | "either", 458 | "rayon-core", 459 | ] 460 | 461 | [[package]] 462 | name = "rayon-core" 463 | version = "1.12.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 466 | dependencies = [ 467 | "crossbeam-deque", 468 | "crossbeam-utils", 469 | ] 470 | 471 | [[package]] 472 | name = "redox_syscall" 473 | version = "0.5.12" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 476 | dependencies = [ 477 | "bitflags 2.9.1", 478 | ] 479 | 480 | [[package]] 481 | name = "regex" 482 | version = "1.11.1" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 485 | dependencies = [ 486 | "aho-corasick", 487 | "memchr", 488 | "regex-automata", 489 | "regex-syntax", 490 | ] 491 | 492 | [[package]] 493 | name = "regex-automata" 494 | version = "0.4.9" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 497 | dependencies = [ 498 | "aho-corasick", 499 | "memchr", 500 | "regex-syntax", 501 | ] 502 | 503 | [[package]] 504 | name = "regex-syntax" 505 | version = "0.8.5" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 508 | 509 | [[package]] 510 | name = "same-file" 511 | version = "1.0.6" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 514 | dependencies = [ 515 | "winapi-util", 516 | ] 517 | 518 | [[package]] 519 | name = "scopeguard" 520 | version = "1.2.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 523 | 524 | [[package]] 525 | name = "serde" 526 | version = "1.0.219" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 529 | dependencies = [ 530 | "serde_derive", 531 | ] 532 | 533 | [[package]] 534 | name = "serde_derive" 535 | version = "1.0.219" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 538 | dependencies = [ 539 | "proc-macro2", 540 | "quote", 541 | "syn", 542 | ] 543 | 544 | [[package]] 545 | name = "simd-adler32" 546 | version = "0.3.7" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 549 | 550 | [[package]] 551 | name = "smallvec" 552 | version = "1.15.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 555 | 556 | [[package]] 557 | name = "syn" 558 | version = "2.0.101" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 561 | dependencies = [ 562 | "proc-macro2", 563 | "quote", 564 | "unicode-ident", 565 | ] 566 | 567 | [[package]] 568 | name = "sysinfo" 569 | version = "0.35.1" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a" 572 | dependencies = [ 573 | "libc", 574 | "memchr", 575 | "ntapi", 576 | "objc2-core-foundation", 577 | "objc2-io-kit", 578 | "windows", 579 | ] 580 | 581 | [[package]] 582 | name = "time" 583 | version = "0.3.41" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 586 | dependencies = [ 587 | "deranged", 588 | "num-conv", 589 | "powerfmt", 590 | "serde", 591 | "time-core", 592 | ] 593 | 594 | [[package]] 595 | name = "time-core" 596 | version = "0.1.4" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 599 | 600 | [[package]] 601 | name = "unicase" 602 | version = "2.8.1" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 605 | 606 | [[package]] 607 | name = "unicode-ident" 608 | version = "1.0.18" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 611 | 612 | [[package]] 613 | name = "walkdir" 614 | version = "2.5.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 617 | dependencies = [ 618 | "same-file", 619 | "winapi-util", 620 | ] 621 | 622 | [[package]] 623 | name = "wasi" 624 | version = "0.11.0+wasi-snapshot-preview1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 627 | 628 | [[package]] 629 | name = "winapi" 630 | version = "0.3.9" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 633 | dependencies = [ 634 | "winapi-i686-pc-windows-gnu", 635 | "winapi-x86_64-pc-windows-gnu", 636 | ] 637 | 638 | [[package]] 639 | name = "winapi-i686-pc-windows-gnu" 640 | version = "0.4.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 643 | 644 | [[package]] 645 | name = "winapi-util" 646 | version = "0.1.9" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 649 | dependencies = [ 650 | "windows-sys", 651 | ] 652 | 653 | [[package]] 654 | name = "winapi-x86_64-pc-windows-gnu" 655 | version = "0.4.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 658 | 659 | [[package]] 660 | name = "windows" 661 | version = "0.61.1" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" 664 | dependencies = [ 665 | "windows-collections", 666 | "windows-core", 667 | "windows-future", 668 | "windows-link", 669 | "windows-numerics", 670 | ] 671 | 672 | [[package]] 673 | name = "windows-collections" 674 | version = "0.2.0" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 677 | dependencies = [ 678 | "windows-core", 679 | ] 680 | 681 | [[package]] 682 | name = "windows-core" 683 | version = "0.61.2" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 686 | dependencies = [ 687 | "windows-implement", 688 | "windows-interface", 689 | "windows-link", 690 | "windows-result", 691 | "windows-strings", 692 | ] 693 | 694 | [[package]] 695 | name = "windows-future" 696 | version = "0.2.1" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 699 | dependencies = [ 700 | "windows-core", 701 | "windows-link", 702 | "windows-threading", 703 | ] 704 | 705 | [[package]] 706 | name = "windows-implement" 707 | version = "0.60.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 710 | dependencies = [ 711 | "proc-macro2", 712 | "quote", 713 | "syn", 714 | ] 715 | 716 | [[package]] 717 | name = "windows-interface" 718 | version = "0.59.1" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 721 | dependencies = [ 722 | "proc-macro2", 723 | "quote", 724 | "syn", 725 | ] 726 | 727 | [[package]] 728 | name = "windows-link" 729 | version = "0.1.1" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 732 | 733 | [[package]] 734 | name = "windows-numerics" 735 | version = "0.2.0" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 738 | dependencies = [ 739 | "windows-core", 740 | "windows-link", 741 | ] 742 | 743 | [[package]] 744 | name = "windows-result" 745 | version = "0.3.4" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 748 | dependencies = [ 749 | "windows-link", 750 | ] 751 | 752 | [[package]] 753 | name = "windows-strings" 754 | version = "0.4.2" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 757 | dependencies = [ 758 | "windows-link", 759 | ] 760 | 761 | [[package]] 762 | name = "windows-sys" 763 | version = "0.59.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 766 | dependencies = [ 767 | "windows-targets", 768 | ] 769 | 770 | [[package]] 771 | name = "windows-targets" 772 | version = "0.52.6" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 775 | dependencies = [ 776 | "windows_aarch64_gnullvm", 777 | "windows_aarch64_msvc", 778 | "windows_i686_gnu", 779 | "windows_i686_gnullvm", 780 | "windows_i686_msvc", 781 | "windows_x86_64_gnu", 782 | "windows_x86_64_gnullvm", 783 | "windows_x86_64_msvc", 784 | ] 785 | 786 | [[package]] 787 | name = "windows-threading" 788 | version = "0.1.0" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 791 | dependencies = [ 792 | "windows-link", 793 | ] 794 | 795 | [[package]] 796 | name = "windows_aarch64_gnullvm" 797 | version = "0.52.6" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 800 | 801 | [[package]] 802 | name = "windows_aarch64_msvc" 803 | version = "0.52.6" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 806 | 807 | [[package]] 808 | name = "windows_i686_gnu" 809 | version = "0.52.6" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 812 | 813 | [[package]] 814 | name = "windows_i686_gnullvm" 815 | version = "0.52.6" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 818 | 819 | [[package]] 820 | name = "windows_i686_msvc" 821 | version = "0.52.6" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 824 | 825 | [[package]] 826 | name = "windows_x86_64_gnu" 827 | version = "0.52.6" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 830 | 831 | [[package]] 832 | name = "windows_x86_64_gnullvm" 833 | version = "0.52.6" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 836 | 837 | [[package]] 838 | name = "windows_x86_64_msvc" 839 | version = "0.52.6" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 842 | 843 | [[package]] 844 | name = "zlib-rs" 845 | version = "0.5.0" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" 848 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gm82save" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.88" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [features] 11 | default = ["smooth_progress_bar"] 12 | smooth_progress_bar = ["crossbeam-channel"] 13 | 14 | [profile.release] 15 | codegen-units = 1 16 | lto = true 17 | strip = true 18 | panic = "abort" 19 | 20 | [build-dependencies] 21 | time = "0.3" 22 | 23 | [dependencies] 24 | byteorder = "1.5" 25 | crossbeam-channel = { version = "0.5", optional = true } 26 | ctor = "0.4" 27 | flate2 = { version = "1.1", default-features = false, features = ["zlib-rs"] } 28 | itertools = "0.14" 29 | lazy_static = "1.5" 30 | notify = "8.0" 31 | once_cell = "1.21" 32 | parking_lot = "0.12.3" 33 | png = "0.17" 34 | rayon = "1.10" 35 | regex = { version = "1.11", default-features = false, features = ["std"] } 36 | sysinfo = "0.35" 37 | time = "0.3" 38 | unicase = "2.8" 39 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | default-target = "i686-pc-windows-gnu" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gm82save 2 | This is a mod for GameMaker 8.1 with many new features, chief among them being a new .gm82 project format, which plays nicer with git. 3 | 4 | * New features: 5 | * Compatible with version control systems such as Git, SVN, and Mercurial. Never deal with email chains or Dropbox crashes again! 6 | * Scale and colour individual instances and tiles using [gm82room](https://github.com/GM82Project/gm82room), the new room editor. Instances can also be rotated! 7 | * Vastly improved load, save, and build times 8 | * Exported games use more efficient compression 9 | * When a project is modified by external programs while GameMaker is open, a warning will be shown, allowing you to reload the project or overwite the external changes 10 | * Many bugs, crashes, and memory leaks from GameMaker 8.1 have been fixed 11 | * Potential pitfalls: 12 | * This **may still contain bugs**, so keep a backup of your .gm81 if you're converting a project to this. Let me know about any bugs you find. 13 | * If saving fails partway through, **your files may be inconsistent**. It shouldn't completely crash the IDE (usually), but stay safe and commit often! 14 | * Saving gm82 projects to a Dropbox folder is currently **not recommended**. I've had at least one report of this somehow crashing Game Maker entirely, and it's not easy to replicate. 15 | * This format **does not save instance IDs or tile IDs**. If your game relies on these having exact values, rework your game to use them via fields in gm82room, or don't use this format. The ordering of instances, and the ordering of tiles within layers, is preserved, however. 16 | * The format relies on **every asset having a unique name**. You can't have the same name but in uppercase either. You can have a sprite called `player` and an object called `player`, but you can't have two sprites both called `player`, or two timelines called `player` and `PlAyEr`. If this isn't the case, saving will fail. Pro tip: click the broom icon next to the Debug button to scan the project for duplicate names. 17 | * Minor quirks: 18 | * When using the Save As dialog to save a new .gm82 project, it will create a new folder and save into that. Behaviour for saving .gm81 projects is unchanged. 19 | * I recommend **adding antivirus exceptions** to your GameMaker and project directories. I'm not gonna knock you for being cautious, but antivirus can make saving and loading take quite a lot longer. 20 | * **Included files** stored outside your project **will be copied into it**, even if "Store in the editable gmk file" is unchecked. 21 | * **Timestamps** are currently **not preserved**. In practice, all this means is that the "Keep Last Changed" option won't work correctly when importing resources. 22 | -------------------------------------------------------------------------------- /assets/thumb_header.dat: -------------------------------------------------------------------------------- 1 | BM66(  -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | del gm82save.dll 2 | 3 | cargo build --release 4 | 5 | pause -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use time::OffsetDateTime; 2 | 3 | fn main() -> Result<(), Box> { 4 | let date = OffsetDateTime::now_utc(); 5 | println!("cargo:rustc-env=ABOUT_BUILD_DATE={} {}, {}", date.month(), date.day(), date.year()); 6 | println!( 7 | "cargo:rustc-env=ERROR_BUILD_DATE={}{:02}{:02}T{:02}{:02}", 8 | date.year(), 9 | u8::from(date.month()), 10 | date.day(), 11 | date.hour(), 12 | date.minute() 13 | ); 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /desktop.ini: -------------------------------------------------------------------------------- 1 | [.ShellClassInfo] 2 | IconResource=.\save.ico,0 3 | [ViewState] 4 | Mode= 5 | Vid= 6 | FolderType=Generic 7 | -------------------------------------------------------------------------------- /save.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GM82Project/gm82save/8355707c2570437d5dd3f70e81421802c2ad2d07/save.ico -------------------------------------------------------------------------------- /src/asset.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use crate::{ 4 | delphi::{DelphiBox, TBitmap, TFont, TMemoryStream, UStr}, 5 | delphi_box, delphi_call, 6 | list::DelphiList, 7 | }; 8 | use std::slice; 9 | 10 | #[repr(C)] 11 | pub struct Trigger { 12 | vmt: u32, 13 | pub name: UStr, 14 | pub condition: UStr, 15 | pub constant_name: UStr, 16 | pub kind: u32, 17 | } 18 | 19 | impl Trigger { 20 | pub fn new() -> DelphiBox { 21 | unsafe { delphi_box!(0x62d200, 0x62cf48) } 22 | } 23 | } 24 | 25 | #[repr(C)] 26 | pub struct Sound { 27 | vmt: u32, 28 | pub kind: u32, 29 | pub extension: UStr, 30 | pub effects: u32, 31 | pub source: UStr, 32 | pub padding: u32, // align f64 to 8 bytes 33 | pub volume: f64, 34 | pub pan: f64, 35 | pub preload: bool, 36 | pub data: Option>, 37 | } 38 | 39 | impl Sound { 40 | pub fn new() -> DelphiBox { 41 | unsafe { delphi_box!(0x64fb70, 0x64f674) } 42 | } 43 | } 44 | 45 | unsafe impl Sync for Sound {} 46 | 47 | #[repr(C)] 48 | pub struct Frame { 49 | vmt: u32, 50 | pub width: u32, 51 | pub height: u32, 52 | pub data: *const u8, 53 | } 54 | 55 | impl Frame { 56 | pub fn new() -> DelphiBox { 57 | unsafe { delphi_box!(0x701bf8, 0x700d94) } 58 | } 59 | 60 | pub unsafe fn load_from_file(&mut self, file: &UStr) { 61 | let _: u32 = delphi_call!(0x7026A4, self, file); 62 | } 63 | 64 | pub unsafe fn assign_from_bitmap(&mut self, bitmap: &TBitmap) { 65 | let _: u32 = delphi_call!(0x702324, self, bitmap); 66 | } 67 | 68 | pub fn get_data(&self) -> &[u8] { 69 | unsafe { slice::from_raw_parts(self.data, (self.width * self.height * 4) as usize) } 70 | } 71 | 72 | pub fn thumb(&self, out: &mut [u8], flip: bool, bg_col: [u8; 3]) { 73 | // note: this assumes output format == input format 74 | // these are stored as BGRA8 so make sure to double check what the output should be 75 | use itertools::Itertools; 76 | let data = self.get_data(); 77 | let (width, height) = if self.width > self.height { 78 | (16, (self.height * 16 / self.width) as usize) 79 | } else { 80 | ((self.width * 16 / self.height) as usize, 16) 81 | }; 82 | let (hoffset, voffset) = (8 - width / 2, 8 - height / 2); 83 | for (y, row) in out.chunks_exact_mut(16 * 4).enumerate() { 84 | // vertical flip for BMP 85 | let y = if flip { 15 - y } else { y }; 86 | if y < voffset || y >= voffset + height { 87 | row.fill(0); 88 | continue 89 | } 90 | let y = y - voffset; 91 | for (x, px) in row.chunks_exact_mut(4).enumerate() { 92 | if x < hoffset || x >= hoffset + width { 93 | px.fill(0); 94 | continue 95 | } 96 | let x = x - hoffset; 97 | // get sample points 98 | let ox = (x as f64) / (width as f64) * f64::from(self.width); 99 | let ox2 = ox + 0.5 / (width as f64) * f64::from(self.width); 100 | let oy = (y as f64) / (height as f64) * f64::from(self.height); 101 | let oy2 = oy + 0.5 / (height as f64) * f64::from(self.height); 102 | // sum all pixels 103 | let mut px_count = 0.0; 104 | let sum_px = [ox, ox2] 105 | .iter() 106 | .map(|x| x.floor() as usize) 107 | .cartesian_product([oy, oy2].iter().map(|x| x.floor() as usize)) 108 | .fold([0.0f64; 4], |mut px, (ox, oy)| { 109 | let offset = (oy * self.width as usize + ox) * 4; 110 | let in_px = &data[offset..offset + 4]; 111 | if in_px[3] != 0 { 112 | px_count += 1.0; 113 | px.iter_mut().zip(in_px).for_each(|(o, i)| *o += f64::from(*i)); 114 | } 115 | px 116 | }); 117 | if px_count != 0.0 { 118 | // average and place into output 119 | px[..3].iter_mut().zip(sum_px).for_each(|(o, i)| *o = (i / px_count).floor() as u8); 120 | // blend semi-transparent to white 121 | let alpha = sum_px[3] / 4.0 / 255.0; 122 | if alpha != 1.0 { 123 | px[..3] 124 | .iter_mut() 125 | .zip(&bg_col) 126 | .for_each(|(c, &b)| *c = (f64::from(b) * (1.0 - alpha) + f64::from(*c) * alpha) as u8); 127 | } 128 | px[3] = 255; 129 | } else { 130 | // fully transparent 131 | px.fill(0); 132 | } 133 | } 134 | } 135 | } 136 | 137 | #[allow(non_snake_case)] 138 | unsafe fn register_thumb_raw(icon: &[u8; 16 * 16 * 4], mask: &[u8; 16 * 16 / 8]) -> i32 { 139 | let CreateBitmap: extern "stdcall" fn(u32, u32, u32, u32, *const u8) -> usize = std::mem::transmute(0x40e008); 140 | let ImageList_Add: extern "stdcall" fn(usize, usize, usize) -> i32 = std::mem::transmute(0x40f8ec); 141 | let DeleteObject: extern "stdcall" fn(usize) -> bool = std::mem::transmute(0x40e098); 142 | let bitmap = CreateBitmap(16, 16, 1, 32, icon.as_ptr()); 143 | let mask_bitmap = CreateBitmap(16, 16, 1, 1, mask.as_ptr()); 144 | let thumb = ImageList_Add((0x789b38 as *const *const usize).read().add(16).read(), bitmap, mask_bitmap); 145 | DeleteObject(bitmap); 146 | DeleteObject(mask_bitmap); 147 | thumb 148 | } 149 | 150 | pub fn register_blank_thumb() -> i32 { 151 | unsafe { Self::register_thumb_raw(&[0; 16 * 16 * 4], &[0xff; 16 * 16 / 8]) } 152 | } 153 | 154 | pub fn register_thumb(&self, bg_col: [u8; 3]) -> i32 { 155 | // i'm initializing them all safely anyway, the extra MaybeUninit dancing is unnecessary 156 | #[allow(invalid_value)] 157 | unsafe { 158 | let mut icon: [u8; 16 * 16 * 4] = std::mem::MaybeUninit::uninit().assume_init(); 159 | self.thumb(&mut icon, false, bg_col); 160 | let mut mask: [u8; 16 * 16 / 8] = std::mem::MaybeUninit::uninit().assume_init(); 161 | for (dst, src) in mask.iter_mut().zip(icon.chunks_exact(8 * 4)) { 162 | *dst = u8::from(src[3] == 0) << 7 163 | | u8::from(src[3 + 4] == 0) << 6 164 | | u8::from(src[3 + 4 * 2] == 0) << 5 165 | | u8::from(src[3 + 4 * 3] == 0) << 4 166 | | u8::from(src[3 + 4 * 4] == 0) << 3 167 | | u8::from(src[3 + 4 * 5] == 0) << 2 168 | | u8::from(src[3 + 4 * 6] == 0) << 1 169 | | u8::from(src[3 + 4 * 7] == 0); 170 | } 171 | Self::register_thumb_raw(&icon, &mask) 172 | } 173 | } 174 | 175 | pub fn duplicate(&self) -> DelphiBox { 176 | unsafe { delphi_box!(0x701cf4, 0x700d94, self) } 177 | } 178 | 179 | pub fn prepare_for_export(&mut self) { 180 | unsafe { 181 | let _: u32 = delphi_call!(0x5b0c74, self); 182 | } 183 | } 184 | } 185 | 186 | #[repr(C)] 187 | pub struct Sprite { 188 | vmt: u32, 189 | pub frame_count: usize, 190 | pub origin_x: i32, 191 | pub origin_y: i32, 192 | pub collision_shape: u32, 193 | pub alpha_tolerance: u32, 194 | pub per_frame_colliders: bool, 195 | pub bbox_type: u32, 196 | pub bbox_left: i32, 197 | pub bbox_top: i32, 198 | pub bbox_right: i32, 199 | pub bbox_bottom: i32, 200 | frames: DelphiList, 0x5b2754>, 201 | } 202 | 203 | impl Sprite { 204 | pub fn new() -> DelphiBox { 205 | unsafe { delphi_box!(0x5b325c, 0x5b27dc) } 206 | } 207 | 208 | pub unsafe fn get_icon(&self) -> *const TBitmap { 209 | delphi_call!(0x5b401c, self) 210 | } 211 | 212 | pub fn alloc_frames(&mut self, count: usize) -> &mut [DelphiBox] { 213 | self.frames.alloc_fill(count, Frame::new); 214 | self.frame_count = count; 215 | unsafe { self.frames.get_unchecked_mut(..self.frame_count) } 216 | } 217 | 218 | pub fn get_frames(&self) -> &[DelphiBox] { 219 | unsafe { self.frames.get_unchecked(..self.frame_count as usize) } 220 | } 221 | 222 | pub unsafe fn register_thumb(&self, bg_col: [u8; 3]) -> i32 { 223 | if let Some(frame) = self.get_frames().get(0).filter(|f| f.width != 0 && f.height != 0) { 224 | frame.register_thumb(bg_col) 225 | } else { 226 | Frame::register_blank_thumb() 227 | } 228 | } 229 | } 230 | 231 | #[repr(C)] 232 | pub struct Background { 233 | vmt: u32, 234 | pub frame: DelphiBox, 235 | pub is_tileset: bool, 236 | pub tile_width: u32, 237 | pub tile_height: u32, 238 | pub h_offset: u32, 239 | pub v_offset: u32, 240 | pub h_sep: u32, 241 | pub v_sep: u32, 242 | } 243 | 244 | impl Background { 245 | pub fn new() -> DelphiBox { 246 | unsafe { delphi_box!(0x062dba4, 0x62d408) } 247 | } 248 | 249 | pub unsafe fn get_icon(&self) -> *const TBitmap { 250 | delphi_call!(0x62e5e8, self) 251 | } 252 | 253 | pub unsafe fn register_thumb(&self, bg_col: [u8; 3]) -> i32 { 254 | if self.frame.width != 0 && self.frame.height != 0 { 255 | self.frame.register_thumb(bg_col) 256 | } else { 257 | Frame::register_blank_thumb() 258 | } 259 | } 260 | } 261 | 262 | unsafe impl Sync for Background {} 263 | 264 | #[repr(C)] 265 | pub struct PathPoint { 266 | pub x: f64, 267 | pub y: f64, 268 | pub speed: f64, 269 | } 270 | 271 | #[repr(C)] 272 | pub struct Path { 273 | vmt: u32, 274 | points: DelphiList, 275 | pub point_count: usize, 276 | pub connection: u32, 277 | pub closed: bool, 278 | pub precision: u32, 279 | padding: [u8; 16], 280 | pub path_editor_room_background: i32, 281 | pub snap_x: u32, 282 | pub snap_y: u32, 283 | } 284 | 285 | impl Path { 286 | pub fn new() -> DelphiBox { 287 | unsafe { delphi_box!(0x5357b0, 0x534924) } 288 | } 289 | 290 | pub fn assign(&mut self, other: &Self) { 291 | unsafe { 292 | let _: u32 = delphi_call!(0x535828, self, other); 293 | } 294 | } 295 | 296 | pub fn commit(&mut self) { 297 | unsafe { 298 | let _: u32 = delphi_call!(0x53578c, self); 299 | } 300 | } 301 | 302 | pub fn alloc_points(&mut self, count: usize) -> &mut [PathPoint] { 303 | self.point_count = count; 304 | self.points.alloc(count); 305 | unsafe { self.points.get_unchecked_mut(..count) } 306 | } 307 | 308 | pub fn get_points(&self) -> &[PathPoint] { 309 | unsafe { self.points.get_unchecked(..self.point_count) } 310 | } 311 | } 312 | 313 | #[repr(C)] 314 | pub struct Script { 315 | vmt: u32, 316 | pub source: UStr, 317 | } 318 | 319 | impl Script { 320 | pub fn new() -> DelphiBox { 321 | unsafe { delphi_box!(0x652860, 0x65267c) } 322 | } 323 | } 324 | 325 | #[repr(C)] 326 | pub struct Font { 327 | vmt: u32, 328 | pub sys_name: UStr, 329 | pub size: u32, 330 | pub bold: bool, 331 | pub italic: bool, 332 | pub range_start: u32, 333 | pub range_end: u32, 334 | pub charset: u32, 335 | /// This is 1 less than what you'll see saved in .gmk or .exe or .gm81 or whatever 336 | pub aa_level: u32, 337 | pub s_x: [u32; 256], 338 | pub s_y: [u32; 256], 339 | pub s_w: [u32; 256], 340 | pub s_h: [u32; 256], 341 | pub s_shift: [u32; 256], 342 | pub s_offset: [u32; 256], 343 | pub s_chr: [u32; 256], 344 | pub s_bw: u32, 345 | pub s_bh: u32, 346 | pub s_bytes: DelphiList, 347 | } 348 | 349 | unsafe impl Sync for Font {} 350 | 351 | impl Font { 352 | pub fn new() -> DelphiBox { 353 | unsafe { delphi_box!(0x5a8760, 0x5a6628) } 354 | } 355 | 356 | pub unsafe fn make_tfont(&self) -> DelphiBox { 357 | DelphiBox::from_ptr(delphi_call!(0x5a86b4, self)) 358 | } 359 | } 360 | 361 | #[repr(C)] 362 | pub struct Action { 363 | vmt: u32, 364 | pub lib_id: u32, 365 | pub id: u32, 366 | pub action_kind: u32, 367 | pub can_be_relative: bool, 368 | pub is_condition: bool, 369 | pub applies_to_something: bool, 370 | pub execution_type: u32, 371 | pub fn_name: UStr, 372 | pub fn_code: UStr, 373 | pub param_count: u32, 374 | pub param_types: [u32; 8], 375 | pub applies_to: i32, 376 | pub is_relative: bool, 377 | pub param_strings: [UStr; 8], 378 | pub invert_condition: bool, 379 | } 380 | 381 | impl Action { 382 | pub unsafe fn fill_in(&mut self, lib_id: u32, act_id: u32) { 383 | let _: u32 = delphi_call!(0x710544, self, lib_id, act_id); 384 | } 385 | } 386 | 387 | #[repr(C)] 388 | pub struct Event { 389 | vmt: u32, 390 | actions: DelphiList, 0x5a4be4>, 391 | pub action_count: u32, 392 | } 393 | 394 | impl Event { 395 | pub fn new() -> DelphiBox { 396 | unsafe { delphi_box!(0x5a5048, 0x5a4c6c) } 397 | } 398 | 399 | pub fn add_action(&mut self, libid: u32, actid: u32) -> &mut Action { 400 | unsafe { 401 | let action: *mut Action = delphi_call!(0x5a51d4, self, libid, actid); 402 | &mut *action 403 | } 404 | } 405 | 406 | pub fn get_actions(&self) -> &[DelphiBox] { 407 | unsafe { self.actions.get_unchecked(..self.action_count as usize) } 408 | } 409 | 410 | pub fn get_actions_mut(&mut self) -> &mut [DelphiBox] { 411 | unsafe { self.actions.get_unchecked_mut(..self.action_count as usize) } 412 | } 413 | } 414 | 415 | #[repr(C)] 416 | pub struct Timeline { 417 | vmt: u32, 418 | pub moment_events: DelphiList, 0x5ad8c8>, 419 | pub moment_times: DelphiList, 420 | pub moment_count: usize, 421 | } 422 | 423 | impl Timeline { 424 | pub fn new() -> DelphiBox { 425 | unsafe { delphi_box!(0x5adf3c, 0x5ad98c) } 426 | } 427 | 428 | pub unsafe fn alloc(&mut self, count: usize) -> (&mut [DelphiBox], &mut [u32]) { 429 | self.moment_count = count; 430 | self.moment_events.alloc_fill(count, Event::new); 431 | self.moment_times.alloc(count); 432 | (self.moment_events.get_unchecked_mut(..count), self.moment_times.get_unchecked_mut(..count)) 433 | } 434 | 435 | pub fn get_events(&self) -> &[DelphiBox] { 436 | unsafe { self.moment_events.get_unchecked(..self.moment_count) } 437 | } 438 | 439 | pub fn get_times(&self) -> &[u32] { 440 | unsafe { self.moment_times.get_unchecked(..self.moment_count) } 441 | } 442 | } 443 | 444 | #[repr(C)] 445 | pub struct Object { 446 | vmt: u32, 447 | pub sprite_index: i32, 448 | pub solid: bool, 449 | pub visible: bool, 450 | pub depth: i32, 451 | pub persistent: bool, 452 | pub parent_index: i32, 453 | pub mask_index: i32, 454 | pub events: [DelphiList, 0x5ad8c8>; 12], 455 | } 456 | 457 | impl Object { 458 | pub fn new() -> DelphiBox { 459 | unsafe { delphi_box!(0x7049a8, 0x704428) } 460 | } 461 | 462 | pub fn has_direct_event(&self, ev_type: usize, ev_numb: usize) -> bool { 463 | self.events.get(ev_type).and_then(|x| x.get(ev_numb)).filter(|x| x.action_count > 0).is_some() 464 | } 465 | 466 | pub fn get_event(&mut self, ev_type: usize, ev_numb: usize) -> &mut Event { 467 | unsafe { 468 | let event: *mut Event = delphi_call!(0x704d74, self, ev_type, ev_numb); 469 | &mut *event 470 | } 471 | } 472 | } 473 | 474 | #[repr(C)] 475 | pub struct RoomBackground { 476 | pub visible_on_start: bool, 477 | pub is_foreground: bool, 478 | pub source_bg: i32, 479 | pub xoffset: i32, 480 | pub yoffset: i32, 481 | pub tile_horz: bool, 482 | pub tile_vert: bool, 483 | pub hspeed: i32, 484 | pub vspeed: i32, 485 | pub stretch: bool, 486 | } 487 | 488 | #[repr(C)] 489 | pub struct View { 490 | pub visible: bool, 491 | pub source_x: i32, 492 | pub source_y: i32, 493 | pub source_w: u32, 494 | pub source_h: u32, 495 | pub port_x: i32, 496 | pub port_y: i32, 497 | pub port_w: u32, 498 | pub port_h: u32, 499 | pub following_hborder: i32, 500 | pub following_vborder: i32, 501 | pub following_hspeed: i32, 502 | pub following_vspeed: i32, 503 | pub following_target: i32, 504 | } 505 | 506 | #[repr(C)] 507 | pub struct Instance { 508 | pub x: i32, 509 | pub y: i32, 510 | pub object: i32, 511 | pub id: usize, 512 | pub creation_code: UStr, 513 | pub locked: bool, 514 | } 515 | 516 | #[repr(C)] 517 | #[derive(Clone, Copy)] 518 | pub struct Tile { 519 | pub x: i32, 520 | pub y: i32, 521 | pub source_bg: i32, 522 | pub u: i32, 523 | pub v: i32, 524 | pub width: i32, 525 | pub height: i32, 526 | pub depth: i32, 527 | pub id: usize, 528 | pub locked: bool, 529 | } 530 | 531 | #[repr(C)] 532 | pub struct Room { 533 | vmt: u32, 534 | pub caption: UStr, 535 | pub speed: u32, 536 | pub width: u32, 537 | pub height: u32, 538 | pub snap_y: u32, 539 | pub snap_x: u32, 540 | pub isometric: bool, 541 | pub persistent: bool, 542 | pub bg_colour: i32, 543 | pub clear_screen: bool, 544 | pub backgrounds: [RoomBackground; 8], 545 | pub views_enabled: bool, 546 | pub clear_view: bool, 547 | pub views: [View; 8], 548 | pub creation_code: UStr, 549 | pub instance_count: usize, 550 | instances: DelphiList, 551 | pub tile_count: usize, 552 | tiles: DelphiList, 553 | pub remember_room_editor_info: bool, 554 | pub editor_width: u32, 555 | pub editor_height: u32, 556 | pub show_grid: bool, 557 | pub show_objects: bool, 558 | pub show_tiles: bool, 559 | pub show_backgrounds: bool, 560 | pub show_foregrounds: bool, 561 | pub show_views: bool, 562 | pub delete_underlying_objects: bool, 563 | pub delete_underlying_tiles: bool, 564 | pub tab: u32, 565 | pub x_position_scroll: u32, 566 | pub y_position_scroll: u32, 567 | } 568 | 569 | impl Room { 570 | pub fn new() -> DelphiBox { 571 | unsafe { delphi_box!(0x6577b8, 0x6564cc) } 572 | } 573 | 574 | pub unsafe fn calc_extents(&mut self) { 575 | let _: u32 = delphi_call!(0x657b48, self); 576 | } 577 | 578 | pub unsafe fn alloc_instances(&mut self, count: usize) -> &mut [Instance] { 579 | self.instance_count = count; 580 | self.instances.alloc(count); 581 | &mut self.instances[..count] 582 | } 583 | 584 | pub fn get_instances(&self) -> &[Instance] { 585 | unsafe { self.instances.get_unchecked(..self.instance_count) } 586 | } 587 | 588 | pub fn get_instances_mut(&mut self) -> &mut [Instance] { 589 | unsafe { self.instances.get_unchecked_mut(..self.instance_count) } 590 | } 591 | 592 | pub unsafe fn put_tiles(&mut self, tiles: Vec) { 593 | let count = tiles.len(); 594 | self.tile_count = count; 595 | self.tiles.alloc(count); 596 | self.tiles[..count].copy_from_slice(&tiles); 597 | } 598 | 599 | pub fn get_tiles(&self) -> &[Tile] { 600 | unsafe { self.tiles.get_unchecked(..self.tile_count) } 601 | } 602 | 603 | pub fn get_tiles_mut(&mut self) -> &mut [Tile] { 604 | unsafe { self.tiles.get_unchecked_mut(..self.tile_count) } 605 | } 606 | } 607 | 608 | #[repr(C)] 609 | pub struct IncludedFile { 610 | vmt: u32, 611 | pub file_name: UStr, 612 | pub source_path: UStr, 613 | pub data_exists: bool, 614 | pub source_length: u32, 615 | pub stored_in_gmk: bool, 616 | pub data: DelphiBox, 617 | pub export_setting: u32, 618 | pub export_custom_folder: UStr, 619 | pub overwrite_file: bool, 620 | pub free_memory: bool, 621 | pub remove_at_end: bool, 622 | } 623 | 624 | impl IncludedFile { 625 | pub fn new() -> DelphiBox { 626 | unsafe { delphi_box!(0x6ca800, 0x6ca360) } 627 | } 628 | } 629 | 630 | #[repr(C)] 631 | pub struct Extension { 632 | vmt: u32, 633 | pub name: UStr, 634 | // other stuff that doesn't get written to the gmk 635 | } 636 | 637 | #[repr(C)] 638 | pub struct ActionDefinition { 639 | vmt: u32, 640 | name: UStr, 641 | pub id: u32, 642 | image: u32, // pointer 643 | image_list: u32, // also pointer 644 | image_index: u32, 645 | hidden: bool, 646 | advanced: bool, 647 | pro_only: bool, 648 | short_desc: UStr, 649 | list_text: UStr, 650 | hint_text: UStr, 651 | kind: u32, 652 | interface: u32, 653 | question: bool, 654 | apply_to: bool, 655 | relative: bool, 656 | arg_count: u32, 657 | arg_captions: [UStr; 8], 658 | arg_types: [u32; 8], 659 | arg_defaults: [UStr; 8], 660 | arg_menu_lens: [UStr; 8], 661 | execution_type: u32, 662 | function_name: UStr, 663 | code_string: UStr, 664 | } 665 | 666 | #[repr(C)] 667 | pub struct ActionLibrary { 668 | vmt: u32, 669 | caption: UStr, 670 | pub id: u32, 671 | author: UStr, 672 | version: u32, 673 | padding: u32, 674 | last_changed: f64, 675 | information: UStr, 676 | pub init_code: UStr, 677 | advanced: bool, 678 | pub action_count: usize, 679 | pub actions: *const &'static ActionDefinition, 680 | max_id: u32, 681 | // also an image list but who cares 682 | } 683 | 684 | pub type Form = u32; // pointer but eh 685 | -------------------------------------------------------------------------------- /src/code_form.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | asset::{Action, Event, Room, Timeline, Trigger}, 3 | delphi::UStr, 4 | ide, 5 | ide::AssetListTrait, 6 | patch, patch_call, 7 | }; 8 | use itertools::Itertools; 9 | use std::{ 10 | arch::{asm, naked_asm}, 11 | collections::{HashMap, hash_map::Entry}, 12 | }; 13 | 14 | type AssetId = usize; 15 | type InstanceId = usize; 16 | type CodeFormPointer = usize; 17 | 18 | #[derive(PartialEq, Eq, Hash)] 19 | enum CodeHolder { 20 | Action(*const Action), 21 | Room(*const Room), 22 | Trigger(*const Trigger), 23 | } 24 | 25 | // asset id is bitwise not'd for timelines 26 | static mut CODE_FORMS: Option> = None; 27 | 28 | static mut INSTANCE_FORMS: Option)>> = None; 29 | 30 | #[unsafe(naked)] 31 | unsafe extern "fastcall" fn trigger_disable_condition_memo() { 32 | unsafe extern "fastcall" fn inj(memo: *const *const usize, trigger: *const Trigger) { 33 | asm!( 34 | "call [{}]", 35 | in(reg) memo.read().add(0x74 / 4), 36 | in("eax") memo, 37 | in("dl") u8::from( 38 | CODE_FORMS.as_ref().map(|forms| !forms.contains_key(&CodeHolder::Trigger(trigger))).unwrap_or(true), 39 | ), 40 | ); 41 | } 42 | naked_asm!( 43 | "push eax", 44 | "mov ecx, 0x4ee6d8", 45 | "call ecx", 46 | "pop ecx", 47 | "mov edx, esi", 48 | "jmp {}", 49 | sym inj, 50 | ); 51 | } 52 | 53 | unsafe fn update_trigger_form(trigger: *const Trigger) { 54 | let trigger_form = (0x77f3fc as *const *const *const *const usize).read(); 55 | if !trigger_form.is_null() { 56 | let list_box = trigger_form.add(0x388 / 4).read(); 57 | let mut list_id: usize; 58 | asm!( 59 | "call {}", 60 | in(reg) list_box.read().add(0xec / 4).read(), 61 | inlateout("eax") list_box => list_id, 62 | clobber_abi("C"), 63 | ); 64 | let trigger_id = list_box.add(0x3dc / 4 + list_id).cast::().read(); 65 | if ide::get_triggers()[trigger_id].as_ref().map(|t| t.as_ptr()) == Some(trigger) { 66 | let _: u32 = delphi_call!(0x6bc118, trigger_form, list_id); 67 | } 68 | } 69 | } 70 | 71 | unsafe extern "fastcall" fn update_code(object: *mut usize) { 72 | let (holder, code) = match *object { 73 | 0x6564cc => (CodeHolder::Room(object.cast()), &mut (*object.cast::()).creation_code), 74 | 0x62cf48 => (CodeHolder::Trigger(object.cast()), &mut (*object.cast::()).condition), 75 | 0x70fd70 => (CodeHolder::Action(object.cast()), &mut (*object.cast::()).param_strings[0]), 76 | _ => return, 77 | }; 78 | if let Some((_, form, asset_id)) = CODE_FORMS.as_ref().and_then(|forms| forms.get(&holder)) { 79 | // mark as changed 80 | (*form as *mut bool).add(0x440).write(true); 81 | // save text 82 | let _: u32 = delphi_call!(0x6b8444, (*form as *const usize).add(0x43c / 4).read(), code); 83 | if *object == 0x62cf48 { 84 | // it's a trigger 85 | let _: u32 = delphi_call!(0x6bcb60); 86 | } else if *object == 0x6564cc { 87 | // it's a room 88 | let _: u32 = delphi_call!(0x6930cc, *asset_id); 89 | } else if *object == 0x70fd70 { 90 | // it's an action 91 | // mark the object/timeline as updated 92 | if (*asset_id as isize) >= 0 { 93 | // it's an object 94 | let _: u32 = delphi_call!(0x62cd2c, *asset_id); 95 | } else { 96 | // it's a timeline 97 | let _: u32 = delphi_call!(0x6fa7b0, !*asset_id); 98 | } 99 | // save the "applies to" 100 | let action = object.cast::(); 101 | let self_check = (*form as *const *const *const usize).add(0x3c0 / 4).read(); 102 | let other_check = (*form as *const *const *const usize).add(0x3c4 / 4).read(); 103 | let self_checked: u32; 104 | let other_checked: u32; 105 | asm!( 106 | "call {}", 107 | in(reg) self_check.read().add(0xec / 4).read(), 108 | inlateout("eax") self_check => self_checked, 109 | in("edx") 1, 110 | clobber_abi("C"), 111 | ); 112 | asm!( 113 | "call {}", 114 | in(reg) other_check.read().add(0xec / 4).read(), 115 | inlateout("eax") other_check => other_checked, 116 | in("edx") 1, 117 | clobber_abi("C"), 118 | ); 119 | (*action).applies_to = if self_checked != 0 { 120 | -1 121 | } else if other_checked != 0 { 122 | -2 123 | } else { 124 | (*form as *const *const i32).add(0x3cc / 4).read().add(0xc / 4).read() 125 | }; 126 | } 127 | } 128 | } 129 | 130 | #[unsafe(naked)] 131 | unsafe extern "C" fn update_code_inj() { 132 | naked_asm!( 133 | "mov ecx, eax", 134 | "jmp {}", 135 | sym update_code, 136 | ); 137 | } 138 | 139 | #[unsafe(naked)] 140 | unsafe extern "C" fn update_applies_to() { 141 | naked_asm!( 142 | "mov eax, dword ptr [ebx + 0x43c]", 143 | "mov ecx, dword ptr [eax + 0x2d4]", 144 | "test ecx, ecx", 145 | "jne {}", 146 | "ret", 147 | sym update_code, 148 | ) 149 | } 150 | 151 | #[unsafe(naked)] 152 | unsafe extern "C" fn check_all_closing() { 153 | naked_asm!( 154 | "mov eax, 0x682a4a", 155 | // original check: form.savechanges 156 | "cmp byte ptr [ebx + 0x441], 0", 157 | "jnz 2f", 158 | // new check: closing all? 159 | "mov ecx, 0x77f448", 160 | "cmp byte ptr [ecx], 0", 161 | // return 162 | "2: jmp eax", 163 | ); 164 | } 165 | 166 | #[unsafe(naked)] 167 | unsafe extern "C" fn close_code() { 168 | unsafe extern "fastcall" fn inj(form: usize, response: i32) { 169 | let revert = response != 6; 170 | let mut found_regular_form = false; 171 | if let Some(forms) = CODE_FORMS.as_mut() { 172 | let mut my_trigger = None; 173 | forms.retain(|holder, (original_code, f, asset_id)| { 174 | if *f == form { 175 | found_regular_form = true; 176 | let code = match holder { 177 | CodeHolder::Trigger(trigger) => { 178 | my_trigger = Some(*trigger); 179 | &mut (*trigger.cast_mut()).condition 180 | }, 181 | CodeHolder::Room(room) => &mut (*room.cast_mut()).creation_code, 182 | CodeHolder::Action(action) => &mut (*action.cast_mut()).param_strings[0], 183 | }; 184 | if revert { 185 | *code = original_code.clone(); 186 | match holder { 187 | CodeHolder::Action(_) => { 188 | if (*asset_id as isize) >= 0 { 189 | // it's an object 190 | let _: u32 = delphi_call!(0x62cd2c, *asset_id); 191 | } else { 192 | // it's a timeline 193 | let _: u32 = delphi_call!(0x6fa7b0, !*asset_id); 194 | } 195 | }, 196 | CodeHolder::Room(_) => { 197 | let _: u32 = delphi_call!(0x6930cc, *asset_id); 198 | }, 199 | CodeHolder::Trigger(_) => { 200 | let _: u32 = delphi_call!(0x6bcb60); 201 | }, 202 | } 203 | } 204 | false 205 | } else { 206 | true 207 | } 208 | }); 209 | if let Some(trigger) = my_trigger { 210 | update_trigger_form(trigger); 211 | } 212 | } 213 | if !found_regular_form { 214 | if let Some(forms) = INSTANCE_FORMS.as_mut() { 215 | for (room, (_, map)) in forms.iter_mut() { 216 | map.retain(|&id, f| { 217 | if f.1 == form { 218 | if revert { 219 | if let Some(inst) = 220 | (*room.cast_mut()).get_instances_mut().iter_mut().filter(|i| i.id == id).next() 221 | { 222 | inst.creation_code = f.0.clone(); 223 | } 224 | } 225 | false 226 | } else { 227 | true 228 | } 229 | }) 230 | } 231 | forms.retain(|_, (_, m)| !m.is_empty()); 232 | } 233 | } 234 | } 235 | naked_asm!( 236 | "mov edx, eax", 237 | "mov ecx, ebx", 238 | "jmp {}", 239 | sym inj, 240 | ); 241 | } 242 | 243 | #[unsafe(naked)] 244 | unsafe extern "C" fn update_instance_code() { 245 | unsafe extern "fastcall" fn inj(room: *mut Room) { 246 | if let Some((room_id, forms)) = INSTANCE_FORMS.as_ref().and_then(|forms| forms.get(&room.cast_const())) { 247 | for inst in (*room).get_instances_mut() { 248 | if let Some(form) = forms.get(&inst.id) { 249 | // honestly fuckin sure just mark all of them as changed idc 250 | (form.1 as *mut bool).add(0x440).write(true); 251 | let _: u32 = 252 | delphi_call!(0x6b8444, (form.1 as *const usize).add(0x43c / 4).read(), &mut inst.creation_code); 253 | } 254 | } 255 | // update room 256 | let _: u32 = delphi_call!(0x6930cc, *room_id); 257 | } 258 | } 259 | naked_asm!( 260 | "mov ecx, eax", 261 | "jmp {}", 262 | sym inj, 263 | ); 264 | } 265 | 266 | unsafe fn create_code_form( 267 | code: *const u16, 268 | applies_to: Option, 269 | title: *const u16, 270 | is_code: bool, 271 | holder: usize, 272 | is_instance: bool, 273 | ) -> (UStr, usize) { 274 | // TCodeForm.Create 275 | let form: *mut u32 = delphi_call!(0x514e78, 0x68050c, 1, 0); 276 | // SetText 277 | let _: u32 = delphi_call!(0x4ee6d8, form, title); 278 | // SetVisible applies_to 279 | let _: u32 = delphi_call!(0x4ee5c0, form.add(0x3b8 / 4).read(), u32::from(applies_to.is_some())); 280 | if let Some(applies_to) = applies_to { 281 | // SetVisible WhoName 282 | let _: u32 = delphi_call!(0x4ee5c0, form.add(0x3cc / 4).read(), u32::from(applies_to >= 0)); 283 | // SetVisible WhoMenuBtn 284 | let _: u32 = delphi_call!(0x4ee5c0, form.add(0x3bc / 4).read(), u32::from(applies_to >= 0)); 285 | // set tag 286 | (form as *mut *mut i32).add(0x3cc / 4).read().add(0xc / 4).write(applies_to); 287 | // get object name 288 | let applies_to_name = UStr::default(); 289 | let _: u32 = delphi_call!(0x62cabc, applies_to, &applies_to_name); 290 | // WhoName.SetText 291 | let _: u32 = delphi_call!(0x4ee6d8, form.add(0x3cc / 4).read(), applies_to_name.0); 292 | let check_offset = match applies_to { 293 | -1 => 0x3c0, 294 | -2 => 0x3c4, 295 | _ => 0x3c8, 296 | }; 297 | let check = form.add(check_offset / 4).read() as *const *const u32; 298 | // SetChecked on whatever radio button 299 | asm!( 300 | "call {}", 301 | in(reg) check.read().add(0xf0 / 4).read(), 302 | in("eax") check, 303 | in("edx") 1, 304 | clobber_abi("C"), 305 | ); 306 | } 307 | // hide check button if not code (lmao) 308 | let _: u32 = delphi_call!(0x4ee5c0, form.add(0x394 / 4).read(), u32::from(is_code)); 309 | // ischanged, savechanges 310 | form.add(0x440 / 4).write(0); 311 | // set up editor 312 | let editor = form.add(0x43c / 4).read() as *mut u32; 313 | // TEditor.SetTheText 314 | let _: u32 = delphi_call!(0x6b83b8, editor, code); 315 | // editor OnChange event 316 | editor.add(0x2d4 / 4).write(holder as _); 317 | editor.add(0x2d0 / 4).write(if is_instance { update_instance_code as _ } else { update_code_inj as _ }); 318 | // set up find box 319 | let _: u32 = delphi_call!(0x681d00, form); 320 | (UStr::from_ptr(&code).clone(), form as usize) 321 | } 322 | 323 | unsafe fn open_or_insert(holder: CodeHolder, create: impl FnOnce() -> (UStr, CodeFormPointer, AssetId)) { 324 | match CODE_FORMS.get_or_insert_default().entry(holder) { 325 | Entry::Occupied(entry) => { 326 | let _: u32 = delphi_call!(0x4ee948, entry.get().1); 327 | }, 328 | Entry::Vacant(entry) => { 329 | entry.insert(create()); 330 | }, 331 | } 332 | } 333 | 334 | #[unsafe(naked)] 335 | unsafe extern "C" fn open_code_action() { 336 | unsafe extern "fastcall" fn inj( 337 | title: *const u16, 338 | action: &Action, 339 | form_ebx: *const usize, 340 | form_esi: *const usize, 341 | ) { 342 | enum ThingForm { 343 | Object(*const usize), 344 | Timeline(*const usize), 345 | } 346 | let form = match *form_esi { 347 | 0x6c3530 => ThingForm::Object(form_esi), 348 | 0x6f6a40 => ThingForm::Timeline(form_esi), 349 | _ => match *form_ebx { 350 | 0x6c3530 => ThingForm::Object(form_ebx), 351 | 0x6f6a40 => ThingForm::Timeline(form_ebx), 352 | _ => unreachable!(), 353 | }, 354 | }; 355 | let (title, asset_id) = match form { 356 | ThingForm::Object(form) => { 357 | let object_index = form.add(0x46c / 4).read(); 358 | let object_name = ide::OBJECTS.names()[object_index].clone(); 359 | let event_type = form.add(0x8180 / 4).read(); 360 | let event_number = form.add(0x8184 / 4).read(); 361 | let event = form.add(0x817c / 4).cast::<&Event>().read(); 362 | let action_id = 363 | event.get_actions().iter().find_position(|act| act.as_ptr() == action as *const _).unwrap().0; 364 | let mut event_name = UStr::default(); 365 | let _: u32 = delphi_call!(0x6d0df0, event_type, event_number, &mut event_name); 366 | ( 367 | object_name 368 | + UStr::new(" - ") 369 | + event_name 370 | + UStr::new(format!(" - Action {} - ", action_id + 1)) 371 | + UStr::from_ptr(&title), 372 | object_index, 373 | ) 374 | }, 375 | ThingForm::Timeline(form) => { 376 | let timeline_index = form.add(0x430 / 4).read(); 377 | let timeline_name = ide::TIMELINES.names()[timeline_index].clone(); 378 | let timeline = form.add(0x420 / 4).cast::<&Timeline>().read(); 379 | let event = form.add(0x434 / 4).cast::<&Event>().read(); 380 | let moment_id = 381 | timeline.moment_events.iter().find_position(|e| e.as_ptr() == event as *const _).unwrap().0; 382 | let action_id = 383 | event.get_actions().iter().find_position(|a| a.as_ptr() == action as *const _).unwrap().0; 384 | ( 385 | timeline_name 386 | + UStr::new(format!( 387 | " - Step {} - Action {} - ", 388 | timeline.moment_times[moment_id], 389 | action_id + 1 390 | )) 391 | + UStr::from_ptr(&title), 392 | !timeline_index, 393 | ) 394 | }, 395 | }; 396 | open_or_insert(CodeHolder::Action(action), || { 397 | let (code, form) = create_code_form( 398 | action.param_strings[0].0, 399 | Some(action.applies_to), 400 | title.0, 401 | action.action_kind != 6, 402 | action as *const _ as _, 403 | false, 404 | ); 405 | (code, form, asset_id) 406 | }); 407 | } 408 | naked_asm!( 409 | "mov edx, dword ptr [esi]", 410 | "push dword ptr [ebp - 0x1c]", 411 | "push dword ptr [ebp + 0xc]", 412 | "call {}", 413 | "mov al, 1", 414 | "ret 8", 415 | sym inj, 416 | ); 417 | } 418 | 419 | #[unsafe(naked)] 420 | unsafe extern "C" fn open_room_code() { 421 | unsafe extern "fastcall" fn inj(room_id: usize, room: &Room) { 422 | let title = ide::ROOMS.names()[room_id].clone() + UStr::new(" - Room Creation Code"); 423 | open_or_insert(CodeHolder::Room(room), || { 424 | let (code, form) = 425 | create_code_form(room.creation_code.0, None, title.0, true, room as *const _ as _, false); 426 | (code, form, room_id) 427 | }); 428 | } 429 | naked_asm!( 430 | "mov ecx, [ebx + 0x630]", 431 | "mov edx, [ebx + 0x61c]", 432 | "call {}", 433 | "ret 8", 434 | sym inj, 435 | ); 436 | } 437 | 438 | #[unsafe(naked)] 439 | unsafe extern "C" fn open_trigger_code() { 440 | unsafe extern "fastcall" fn inj(title: *const u16, trigger: &Trigger) { 441 | open_or_insert(CodeHolder::Trigger(trigger), || { 442 | let (code, form) = 443 | create_code_form(trigger.condition.0, None, title, true, trigger as *const _ as _, false); 444 | (code, form, 0) 445 | }); 446 | update_trigger_form(trigger); 447 | } 448 | naked_asm!( 449 | "mov edx, edi", 450 | "call {}", 451 | "ret 8", 452 | sym inj, 453 | ); 454 | } 455 | 456 | #[unsafe(naked)] 457 | unsafe extern "C" fn open_instance_code() { 458 | unsafe extern "fastcall" fn inj(title: *const u16, room: &Room, code: *const u16, inst_id: usize, room_id: usize) { 459 | match INSTANCE_FORMS 460 | .get_or_insert_default() 461 | .entry(room) 462 | .or_insert_with(|| (room_id, HashMap::new())) 463 | .1 464 | .entry(inst_id) 465 | { 466 | Entry::Occupied(entry) => { 467 | let _: u32 = delphi_call!(0x4ee948, entry.get().1); 468 | }, 469 | Entry::Vacant(entry) => { 470 | entry.insert(create_code_form(code, None, title, true, room as *const _ as _, true)); 471 | }, 472 | } 473 | } 474 | naked_asm!( 475 | "mov edx, [ebx + 0x61c]", 476 | "push dword ptr [ebx + 0x630]", // room id 477 | "push dword ptr [ebp - 0x10]", // instance id 478 | "push dword ptr [ebp - 4]", // code 479 | "call {}", 480 | "ret 8", 481 | sym inj, 482 | ); 483 | } 484 | 485 | unsafe extern "C" fn destroy_action(_: u32, action: &Action) { 486 | if let Some(forms) = CODE_FORMS.as_mut() { 487 | if let Some(form) = forms.remove(&CodeHolder::Action(action)) { 488 | let _: u32 = delphi_call!(0x405a7c, form.1); 489 | } 490 | } 491 | } 492 | 493 | unsafe extern "fastcall" fn clear_all_instances_in_room(room: *const Room) { 494 | if let Some(forms) = INSTANCE_FORMS.as_mut().and_then(|forms| forms.remove(&room)) { 495 | for (_, form) in forms.1 { 496 | let _: u32 = delphi_call!(0x405a7c, form.1); 497 | } 498 | } 499 | } 500 | 501 | #[unsafe(naked)] 502 | unsafe extern "C" fn room_delete_instance() { 503 | unsafe extern "fastcall" fn inj(room: *const Room, inst_number: usize) { 504 | let inst_id = (*room).get_instances()[inst_number].id; 505 | if let Some(form) = 506 | INSTANCE_FORMS.as_mut().and_then(|forms| forms.get_mut(&room)).and_then(|(_, forms)| forms.remove(&inst_id)) 507 | { 508 | let _: u32 = delphi_call!(0x405a7c, form.1); 509 | } 510 | } 511 | naked_asm!( 512 | "push esi", 513 | "push edi", 514 | "push ebp", 515 | "mov ebx, eax", 516 | "push edx", 517 | "mov ecx, eax", 518 | "call {}", 519 | "pop edx", 520 | "mov eax, 0x658ab6", 521 | "jmp eax", 522 | sym inj, 523 | ) 524 | } 525 | 526 | #[unsafe(naked)] 527 | unsafe extern "C" fn room_safe_undo() { 528 | unsafe extern "fastcall" fn inj(room: *mut Room) { 529 | update_code(room.cast()); 530 | if let Some(all_forms) = INSTANCE_FORMS.as_mut() { 531 | if let Some((_, forms)) = all_forms.get_mut(&room.cast_const()) { 532 | forms.retain(|&id, _| (*room).get_instances().iter().any(|i| i.id == id)); 533 | } 534 | } 535 | } 536 | naked_asm!( 537 | "mov edx, 0x405a7c", 538 | "call edx", 539 | "mov ecx, [ebx + 0x61c]", 540 | "jmp {}", 541 | sym inj, 542 | ); 543 | } 544 | 545 | #[unsafe(naked)] 546 | unsafe extern "C" fn room_delete_all() { 547 | naked_asm!( 548 | "push eax", 549 | "mov ecx, eax", 550 | "call {}", 551 | "pop eax", 552 | "mov edx, 0x657b48", 553 | "jmp edx", 554 | sym clear_all_instances_in_room, 555 | ) 556 | } 557 | 558 | unsafe extern "C" fn destroy_room(_: u32, room: &Room) { 559 | if let Some(forms) = CODE_FORMS.as_mut() { 560 | if let Some(form) = forms.remove(&CodeHolder::Room(room)) { 561 | let _: u32 = delphi_call!(0x405a7c, form.1); 562 | } 563 | } 564 | clear_all_instances_in_room(room); 565 | } 566 | 567 | unsafe extern "C" fn destroy_trigger(_: u32, trigger: &Trigger) { 568 | if let Some(forms) = CODE_FORMS.as_mut() { 569 | if let Some(form) = forms.remove(&CodeHolder::Trigger(trigger)) { 570 | let _: u32 = delphi_call!(0x405a7c, form.1); 571 | } 572 | } 573 | } 574 | 575 | pub unsafe fn inject() { 576 | // nop out modal creation 577 | patch(0x7724af, &[0x90; 5]); 578 | 579 | // open in appropriate places 580 | patch_call(0x6bc877, open_trigger_code as _); 581 | patch_call(0x689f36, open_room_code as _); 582 | patch_call(0x6fefdf, open_code_action as _); 583 | patch_call(0x68aea0, open_instance_code as _); 584 | 585 | // don't mark as updated when simply opening a form 586 | patch(0x6c781d, &[0xeb]); 587 | patch(0x6f98ad, &[0xeb]); 588 | patch(0x689f3d, &[0xeb]); 589 | patch(0x68aeca, &[0xeb]); 590 | 591 | // update when "applies to" buttons clicked 592 | patch(0x681b21, &[0xe8, 0, 0, 0, 0, 0x90, 0x90]); 593 | patch_call(0x681b21, update_applies_to as _); 594 | patch(0x681b73, &[0xe8, 0, 0, 0, 0, 0x90, 0x90]); 595 | patch_call(0x681b73, update_applies_to as _); 596 | 597 | // close form when deleting instances 598 | patch(0x658ab1, &[0xe9]); 599 | patch_call(0x658ab1, room_delete_instance as _); 600 | patch_call(0x658b14, room_delete_all as _); 601 | 602 | // double check when undoing room 603 | patch_call(0x6889b0, room_safe_undo as _); 604 | 605 | // close form when deleting assets 606 | patch(0x70fd58, &(destroy_action as usize).to_le_bytes()); 607 | patch(0x62cf30, &(destroy_trigger as usize).to_le_bytes()); 608 | patch(0x6564b4, &(destroy_room as usize).to_le_bytes()); 609 | 610 | // disable condition memo when code form is open 611 | patch_call(0x6bc183, trigger_disable_condition_memo as _); 612 | 613 | // close form 614 | patch(0x682a42, &[0x2f]); 615 | patch(0x682a43, &[0xe9, 0, 0, 0, 0, 0x90, 0x90]); 616 | patch_call(0x682a43, check_all_closing as _); 617 | patch(0x682a4b, &[0x26]); 618 | patch(0x682a6b, &[0x0a]); 619 | patch(0x682a72, &[ 620 | 0x33, 0xc0, // xor eax, eax 621 | 0xb0, 0x06, // mov al, 6 622 | 0xe8, 0, 0, 0, 0, // call <...> 623 | 0xc6, 0x06, 0x02, // mov dword ptr [esi], 2 624 | ]); 625 | patch_call(0x682a76, close_code as _); 626 | } 627 | -------------------------------------------------------------------------------- /src/compiler.rs: -------------------------------------------------------------------------------- 1 | use super::{EXTRA_DATA, InstanceExtra, TileExtra, patch, patch_call}; 2 | use crate::{UStr, ide, ide::AssetListTrait}; 3 | use lazy_static::lazy_static; 4 | use rayon::prelude::*; 5 | use regex::Regex; 6 | use std::{ 7 | arch::{asm, naked_asm}, 8 | collections::HashSet, 9 | }; 10 | 11 | #[unsafe(naked)] 12 | unsafe extern "C" fn compile_constants_inj() { 13 | naked_asm!( 14 | "mov ecx, eax", 15 | "jmp {}", 16 | sym compile_constants, 17 | ); 18 | } 19 | 20 | lazy_static! { 21 | static ref INSTANCE_ID_REGEX: Regex = Regex::new(r"[A-Za-z0-9_]+_([0-9A-F]{8})").unwrap(); 22 | } 23 | 24 | unsafe extern "fastcall" fn compile_constants(stream: usize) -> bool { 25 | if EXTRA_DATA.is_none() { 26 | let res: usize = delphi_call!(0x696744, stream, 1); 27 | return res as u8 != 0 28 | } 29 | 30 | let constant_names = ide::get_constant_names(); 31 | let constant_values = ide::get_constants(); 32 | // this is just so my ide will give me autocomplete 33 | let re = &INSTANCE_ID_REGEX; 34 | 35 | // we want to collect instance names that are actually used 36 | // iterate over all code 37 | let cnv = |s: &UStr| s.to_os_string().into_string().unwrap_or_else(|s| s.to_string_lossy().into_owned()); 38 | let room_iter = ide::ROOMS.assets().into_par_iter().flatten().flat_map(|room| { 39 | room.get_instances() 40 | .into_par_iter() 41 | .map(|i| cnv(&i.creation_code)) 42 | .chain(rayon::iter::once(cnv(&room.creation_code))) 43 | }); 44 | let object_iter = ide::OBJECTS.assets().into_par_iter().flatten().flat_map_iter(|o| { 45 | o.events.iter().flatten().flat_map(|e| e.get_actions()).flat_map(|a| &a.param_strings).map(cnv) 46 | }); 47 | let timeline_iter = 48 | ide::TIMELINES.assets().into_par_iter().flatten().flat_map_iter(|t| { 49 | t.get_events().iter().flat_map(|e| e.get_actions()).flat_map(|a| &a.param_strings).map(cnv) 50 | }); 51 | let script_iter = ide::SCRIPTS.assets().into_par_iter().flatten().map(|s| cnv(&s.source)); 52 | let trigger_iter = ide::get_triggers().into_par_iter().flatten().map(|t| cnv(&t.condition)); 53 | let constant_iter = constant_values.par_iter().map(cnv); 54 | 55 | // find instance names in code 56 | let instance_names: HashSet = room_iter 57 | .chain(object_iter) 58 | .chain(timeline_iter) 59 | .chain(script_iter) 60 | .chain(trigger_iter) 61 | .chain(constant_iter) 62 | .flat_map(|s| { 63 | // gotta collect into vec because otherwise string reference is lost 64 | re.captures_iter(&s) 65 | .filter_map(|c| c.get(1)) 66 | .flat_map(|m| u32::from_str_radix(m.as_str(), 16)) 67 | .collect::>() 68 | }) 69 | .collect(); 70 | 71 | // collect data for referenced instances 72 | let instances = ide::ROOMS 73 | .assets() 74 | .into_par_iter() 75 | .zip(ide::ROOMS.names()) 76 | .filter_map(|(room, name)| Some((room.as_ref()?, name))) 77 | .flat_map(|(room, name)| { 78 | let instance_names = &instance_names; 79 | room.get_instances().par_iter().filter_map(move |inst| { 80 | EXTRA_DATA 81 | .as_ref() 82 | .and_then(|(extra, _)| extra.get(&inst.id)) 83 | .filter(|extra| instance_names.contains(&extra.name)) 84 | .map(|extra| (name, inst.id, extra.name)) 85 | }) 86 | }) 87 | .filter(|(_, id, _)| *id != 0) 88 | .collect::>(); 89 | 90 | // write version 91 | let _: u32 = delphi_call!(0x52f12c, stream, 800); 92 | // write count 93 | let _: u32 = delphi_call!(0x52f12c, stream, constant_names.len() + instances.len()); 94 | // write instance ids 95 | for (room_name, id, name) in instances { 96 | // write constant name 97 | let _: u32 = delphi_call!( 98 | 0x52f168, 99 | stream, 100 | UStr::new(format!("{}_{:08X}", room_name.to_os_string().to_str().unwrap(), name)).0 101 | ); 102 | // write constant value 103 | let _: u32 = delphi_call!(0x52f168, stream, UStr::new(id.to_string()).0); 104 | } 105 | // write original constants 106 | for (name, value) in constant_names.iter().zip(constant_values) { 107 | let _: u32 = delphi_call!(0x52f168, stream, name.0); 108 | let _: u32 = delphi_call!(0x52f168, stream, value.0); 109 | } 110 | true 111 | } 112 | 113 | #[unsafe(naked)] 114 | unsafe extern "C" fn save_82_if_exe() { 115 | // only saves settings version 825 when saving an exe with the creation code flag set 116 | naked_asm!( 117 | "mov edx, 825", 118 | "mov ecx, 800", 119 | "test bl, bl", // if exe 120 | "cmovz edx, ecx", 121 | "bt word ptr [0x77f54e], 15", // if force cpu 122 | "cmovnc edx, ecx", 123 | "ret", 124 | ); 125 | } 126 | 127 | #[unsafe(naked)] 128 | unsafe extern "C" fn save_bool_if_exe() { 129 | naked_asm!( 130 | "push esi", 131 | "mov esi, 0x52f240", // WriteBoolean 132 | "mov ecx, 0x52f12c", // WriteInteger 133 | "test bl, bl", // if exe 134 | "cmovnz ecx, esi", 135 | "call ecx", 136 | "pop esi", 137 | "ret", 138 | ); 139 | } 140 | 141 | #[unsafe(naked)] 142 | unsafe extern "C" fn save_creation_code_flag() { 143 | naked_asm!( 144 | "mov ecx, 0x52f12c", // WriteInteger (for uninitialized args) 145 | "call ecx", 146 | "test bl, bl", // if exe 147 | "jz 2f", 148 | "bt word ptr [0x77f54e], 15", // if force cpu 149 | "jnc 2f", 150 | // write webgl 151 | "mov eax, esi", // gmk stream 152 | "xor edx, edx", // 0 (webgl) 153 | "mov ecx, 0x52f12c", // WriteInteger 154 | "call ecx", 155 | // write creation code flag 156 | "mov eax, esi", // gmk stream 157 | "mov dl, 1", // true (creation code) 158 | "mov ecx, 0x52f240", // WriteBoolean 159 | "call ecx", 160 | // exit 161 | "2: ret", 162 | ); 163 | } 164 | 165 | #[unsafe(naked)] 166 | unsafe extern "C" fn save_room_version_inj() { 167 | naked_asm!( 168 | "mov cl, byte ptr [esp]", 169 | "call {}", 170 | "mov edx, eax", 171 | "mov eax, 0x658372", 172 | "jmp eax", 173 | sym save_room_version, 174 | ); 175 | } 176 | 177 | unsafe extern "fastcall" fn save_room_version(exe: bool) -> u32 { 178 | if exe && EXTRA_DATA.is_some() { 811 } else { 541 } 179 | } 180 | 181 | #[unsafe(naked)] 182 | unsafe extern "C" fn save_instance_extra_inj() { 183 | naked_asm!( 184 | "mov ecx, ebx", // file 185 | "mov eax, dword ptr [edi + 0x2f4]", // instance list 186 | "mov edx, dword ptr [eax + ebp*0x8 + 0xc]", // instance id 187 | "xor eax, eax", 188 | "mov al, byte ptr [esp]", // are we exe? 189 | "push eax", 190 | "call {}", 191 | "inc esi", 192 | "mov eax, 0x658600", // jnz of loop 193 | "dec dword ptr [esp + 0x4]", 194 | "jmp eax", 195 | sym save_instance_extra, 196 | ); 197 | } 198 | 199 | #[unsafe(naked)] 200 | unsafe extern "C" fn save_tile_extra_inj() { 201 | naked_asm!( 202 | "mov ecx, ebx", // file 203 | "mov eax, dword ptr [edi + 0x2fc]", // tile list 204 | "mov edx, dword ptr [eax + ebp*0x8 + 0x20]", // tile id 205 | "xor eax, eax", 206 | "mov al, byte ptr [esp]", // are we exe? 207 | "push eax", 208 | "call {}", 209 | "inc esi", 210 | "mov eax, 0x6586dd", // jnz of loop 211 | "dec dword ptr [esp + 0x4]", 212 | "jmp eax", 213 | sym save_tile_extra, 214 | ); 215 | } 216 | 217 | unsafe fn save_real(file: usize, real: &f64) { 218 | asm!( 219 | "push dword ptr [{real} + 0x4]", 220 | "push dword ptr [{real}]", 221 | "call {call}", 222 | call = in(reg) 0x52f140, 223 | real = in(reg) real, 224 | in("eax") file, 225 | clobber_abi("C"), 226 | ); 227 | } 228 | 229 | unsafe extern "fastcall" fn save_instance_extra(file: usize, id: usize, exe: bool) { 230 | if exe { 231 | if let Some(data) = EXTRA_DATA.as_ref().map(|(insts, _)| insts.get(&id).unwrap_or(&InstanceExtra::DEFAULT)) { 232 | save_real(file, &data.xscale); 233 | save_real(file, &data.yscale); 234 | let _: u32 = delphi_call!(0x52f12c, file, data.blend); 235 | save_real(file, &data.angle); 236 | } 237 | } 238 | } 239 | 240 | unsafe extern "fastcall" fn save_tile_extra(file: usize, id: usize, exe: bool) { 241 | if exe { 242 | if let Some(data) = EXTRA_DATA.as_ref().map(|(_, tiles)| tiles.get(&id).unwrap_or(&TileExtra::DEFAULT)) { 243 | save_real(file, &data.xscale); 244 | save_real(file, &data.yscale); 245 | let _: u32 = delphi_call!(0x52f12c, file, data.blend); 246 | } 247 | } 248 | } 249 | 250 | pub unsafe fn inject() { 251 | // add instance ids to constants 252 | patch_call(0x6cd90d as _, compile_constants_inj as _); 253 | 254 | // save creation code flag (reusing the software vertex processing flag) 255 | // write 825 instead of 800 for settings version if saving exe 256 | patch(0x70997c as _, &[0xe8]); 257 | patch_call(0x70997c as _, save_82_if_exe as _); 258 | // call WriteBoolean instead of WriteInteger if saving exe 259 | patch_call(0x709a4f as _, save_bool_if_exe as _); 260 | // save extra info if saving exe 261 | patch_call(0x709c99 as _, save_creation_code_flag as _); 262 | 263 | // save extra data on instances and tiles 264 | // write 811 instead of 541 for room version if saving exe 265 | patch(0x65836d as _, &[0xe9]); 266 | patch_call(0x65836d as _, save_room_version_inj as _); 267 | // instance stuff 268 | patch(0x6585fb as _, &[0xe9]); 269 | patch_call(0x6585fb as _, save_instance_extra_inj as _); 270 | // tile stuff 271 | patch(0x6586d8 as _, &[0xe9]); 272 | patch_call(0x6586d8 as _, save_tile_extra_inj as _); 273 | } 274 | -------------------------------------------------------------------------------- /src/delphi.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, dead_code)] 2 | 3 | use std::{ 4 | arch::{asm, naked_asm}, 5 | ffi::{OsStr, OsString}, 6 | os::windows::ffi::{OsStrExt, OsStringExt}, 7 | ptr, slice, 8 | }; 9 | 10 | // this gets optimized out in release mode so it's fine 11 | #[macro_export] 12 | macro_rules! check_call { 13 | ($call: literal) => {{ 14 | if $call & 3 != 0 { 15 | crate::show_message(format!("can you let floogle know {:#x} isn't a valid function thanks", $call)); 16 | } 17 | }}; 18 | } 19 | 20 | #[macro_export] 21 | macro_rules! delphi_call { 22 | ($call: literal) => {{ 23 | crate::check_call!($call); 24 | let out; 25 | std::arch::asm!( 26 | "call {call}", 27 | call = in(reg) $call, 28 | lateout("eax") out, 29 | clobber_abi("C"), 30 | ); 31 | out 32 | }}; 33 | ($call: literal, $a: expr) => {{ 34 | crate::check_call!($call); 35 | let out; 36 | std::arch::asm!( 37 | "call {call}", 38 | call = in(reg) $call, 39 | inlateout("eax") $a => out, 40 | clobber_abi("C"), 41 | ); 42 | out 43 | }}; 44 | ($call: literal, $a: expr, $b: expr) => {{ 45 | crate::check_call!($call); 46 | let out; 47 | std::arch::asm!( 48 | "call {call}", 49 | call = in(reg) $call, 50 | inlateout("eax") $a => out, 51 | in("edx") $b, 52 | clobber_abi("C"), 53 | ); 54 | out 55 | }}; 56 | ($call: literal, $a: expr, $b: expr, $c: expr) => {{ 57 | crate::check_call!($call); 58 | let out; 59 | std::arch::asm!( 60 | "call {call}", 61 | call = in(reg) $call, 62 | inlateout("eax") $a => out, 63 | in("edx") $b, 64 | in("ecx") $c, 65 | clobber_abi("C"), 66 | ); 67 | out 68 | }}; 69 | ($call: literal, $a: expr, $b: expr, $c: expr, $d: expr) => {{ 70 | crate::check_call!($call); 71 | let out; 72 | std::arch::asm!( 73 | "push {arg4}", 74 | "call {call}", 75 | call = in(reg) $call, 76 | arg4 = in(reg) $d, 77 | inlateout("eax") $a => out, 78 | in("edx") $b, 79 | in("ecx") $c, 80 | clobber_abi("C"), 81 | ); 82 | out 83 | }}; 84 | ($call: literal, $a: expr, $b: expr, $c: expr, $d: expr, $e: expr) => {{ 85 | crate::check_call!($call); 86 | let out; 87 | std::arch::asm!( 88 | "push {arg5}", 89 | "push {arg4}", 90 | "call {call}", 91 | call = in(reg) $call, 92 | arg4 = in(reg) $d, 93 | arg5 = in(reg) $e, 94 | inlateout("eax") $a => out, 95 | inlateout("edx") $b, 96 | inlateout("ecx") $c, 97 | clobber_abi("C"), 98 | ); 99 | out 100 | }}; 101 | } 102 | 103 | #[macro_export] 104 | macro_rules! delphi_box { 105 | ($call: literal, $vmt: literal) => {{ 106 | DelphiBox::from_ptr(delphi_call!($call, $vmt, 1)) 107 | }}; 108 | ($call: literal, $vmt: literal, $($x:expr),*) => {{ 109 | DelphiBox::from_ptr(delphi_call!($call, $vmt, 1, $($x),*)) 110 | }}; 111 | } 112 | 113 | #[repr(transparent)] 114 | pub struct DelphiBox(ptr::NonNull); 115 | 116 | unsafe impl Send for DelphiBox {} 117 | unsafe impl Sync for DelphiBox {} 118 | 119 | impl DelphiBox { 120 | pub fn from_ptr(ptr: *mut T) -> Self { 121 | unsafe { Self(ptr::NonNull::new_unchecked(ptr)) } 122 | } 123 | 124 | pub fn as_ptr(&self) -> *const T { 125 | self.0.as_ptr() 126 | } 127 | } 128 | 129 | impl Drop for DelphiBox { 130 | fn drop(&mut self) { 131 | unsafe { 132 | let _: u32 = delphi_call!(0x405a7c, self.0.as_ref()); 133 | } 134 | } 135 | } 136 | 137 | impl std::ops::Deref for DelphiBox { 138 | type Target = T; 139 | 140 | fn deref(&self) -> &Self::Target { 141 | unsafe { self.0.as_ref() } 142 | } 143 | } 144 | 145 | impl std::ops::DerefMut for DelphiBox { 146 | fn deref_mut(&mut self) -> &mut Self::Target { 147 | unsafe { self.0.as_mut() } 148 | } 149 | } 150 | 151 | impl std::io::Write for DelphiBox { 152 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 153 | (**self).write(buf) 154 | } 155 | 156 | fn flush(&mut self) -> std::io::Result<()> { 157 | (**self).flush() 158 | } 159 | } 160 | 161 | // this is really game maker specific but i left it here for simplicity 162 | #[repr(C)] 163 | pub struct TreeNodeData { 164 | unknown: u32, 165 | pub rtype: u32, // 0 for toplevel, 1 for group, 2 for folder i think? 166 | pub kind: u32, // what resource type i.e. sprite, sound, etc 167 | pub index: usize, // resource index 168 | } 169 | 170 | impl TreeNodeData { 171 | pub fn new(rtype: u32, kind: u32, index: usize) -> *const TreeNodeData { 172 | let data = unsafe { 173 | let data: *mut TreeNodeData = delphi_call!(0x405a4c, 0x71c368, 1); 174 | &mut *data 175 | }; 176 | data.rtype = rtype; 177 | data.kind = kind; 178 | data.index = index; 179 | data 180 | } 181 | } 182 | 183 | #[repr(C)] 184 | pub struct TTreeNode { 185 | unknown: u64, 186 | pub name: UStr, 187 | pub data: *const TreeNodeData, 188 | } 189 | 190 | impl TTreeNode { 191 | pub unsafe fn GetCount(&self) -> u32 { 192 | delphi_call!(0x4ad490, self) 193 | } 194 | 195 | pub unsafe fn GetItem(&self, index: u32) -> *const Self { 196 | delphi_call!(0x4ad3b4, self, index) 197 | } 198 | 199 | pub unsafe fn SetData(&self, data: *const TreeNodeData) { 200 | let _: u32 = delphi_call!(0x4ac9b4, self, data); 201 | } 202 | 203 | pub unsafe fn SetImageIndex(&self, index: i32) { 204 | let _: u32 = delphi_call!(0x4acb64, self, index); 205 | } 206 | 207 | pub unsafe fn new(name: &UStr, rtype: u32, kind: u32, index: usize) -> *const TTreeNode { 208 | delphi_call!(0x71cb48, name.0, rtype, kind, index) 209 | } 210 | 211 | pub unsafe fn DeleteChildren(&self) { 212 | let _: u32 = delphi_call!(0x4ad9ec, self); 213 | } 214 | } 215 | 216 | #[repr(C)] 217 | pub struct TTreeNodes {} 218 | 219 | impl TTreeNodes { 220 | pub unsafe fn AddChild(&self, parent: *const TTreeNode, s: &UStr) -> *const TTreeNode { 221 | delphi_call!(0x4ae1e8, self, parent, s.0) 222 | } 223 | 224 | pub unsafe fn Clear(&self) { 225 | let _: u32 = delphi_call!(0x4ae12c, self); 226 | } 227 | } 228 | 229 | #[repr(C)] 230 | pub struct TTreeView { 231 | padding: [u8; 0x6c], 232 | pub color: [u8; 3], 233 | padding2: [u8; 0x269], 234 | pub nodes: *const TTreeNodes, 235 | } 236 | 237 | #[repr(C)] 238 | pub struct TMenuItem { 239 | padding: [u8; 12], 240 | pub Tag: i32, 241 | padding2: [u8; 136 - 12], 242 | pub OnClick: usize, 243 | } 244 | 245 | impl TMenuItem { 246 | pub fn new(parent: usize) -> DelphiBox { 247 | unsafe { delphi_box!(0x4d89cc, 0x4d5e80, parent) } 248 | } 249 | 250 | pub fn set_caption(&mut self, caption: &UStr) { 251 | unsafe { 252 | let _: u32 = delphi_call!(0x4dbc6c, self, caption.0); 253 | } 254 | } 255 | 256 | pub fn set_image_index(&mut self, image_index: i32) { 257 | unsafe { 258 | let _: u32 = delphi_call!(0x4dbe60, self, image_index); 259 | } 260 | } 261 | 262 | pub fn add(&mut self, added: DelphiBox) { 263 | unsafe { 264 | let added: &Self = &added; 265 | let _: u32 = delphi_call!(0x4dc190, self, added); 266 | } 267 | std::mem::forget(added); 268 | } 269 | 270 | pub fn add_from_tree_node(&self, tree_node: &TTreeNode, custom_events: Option<&[u32; 6]>) { 271 | const BLANK_EVENTS: [u32; 6] = [0; 6]; 272 | unsafe { 273 | asm!( 274 | "push dword ptr [{base} + 0x1c]", 275 | "push dword ptr [{base} + 0x18]", 276 | "push dword ptr [{base} + 0x14]", 277 | "push dword ptr [{base} + 0x10]", 278 | "push dword ptr [{base} + 0xc]", 279 | "push dword ptr [{base} + 0x8]", 280 | "call {func}", 281 | base = in(reg) custom_events.unwrap_or(&BLANK_EVENTS), 282 | func = in(reg) 0x71c3fc, 283 | in("eax") self, 284 | in("edx") tree_node, 285 | in("ecx") u32::from(custom_events.is_some()), 286 | clobber_abi("C"), 287 | ); 288 | } 289 | } 290 | 291 | pub fn add_with_fake_tree_node( 292 | &self, 293 | name: &UStr, 294 | rtype: u32, 295 | kind: u32, 296 | index: usize, 297 | custom_events: Option<&[u32; 6]>, 298 | ) { 299 | // very bad and evil 300 | let data = TreeNodeData { unknown: 0, rtype, kind, index }; 301 | let node = TTreeNode { unknown: 0x49614c, name: name.clone(), data: &data }; 302 | self.add_from_tree_node(&node, custom_events); 303 | } 304 | } 305 | 306 | #[repr(C)] 307 | pub struct TPopupMenu { 308 | padding: [u8; 0x38], 309 | pub Items: &'static mut TMenuItem, 310 | } 311 | 312 | impl TPopupMenu { 313 | pub fn new(parent: usize) -> DelphiBox { 314 | unsafe { delphi_box!(0x4dee2c, 0x4d7824, parent) } 315 | } 316 | 317 | pub fn SetAutoHotkeys(&mut self, hotkeys: u8) { 318 | unsafe { 319 | let _: u32 = delphi_call!(0x4de514, self, u32::from(hotkeys)); 320 | } 321 | } 322 | 323 | pub fn SetImages(&mut self) { 324 | unsafe { 325 | let _: u32 = delphi_call!(0x4de0a0, self, *(0x789b38 as *const usize)); 326 | } 327 | } 328 | 329 | pub fn popup_at_cursor_pos(&self) { 330 | let mouse_pos: [i32; 2] = [0, 0]; 331 | unsafe { 332 | // TMouse.GetCursorPos 333 | let _: u32 = delphi_call!(0x4fd580, 0, mouse_pos.as_ptr()); 334 | // TPopupMenu.PopUp 335 | let _: u32 = delphi_call!(0x4def94, self, mouse_pos[0], mouse_pos[1]); 336 | // TApplication.ProcessMessages 337 | let _: u32 = delphi_call!(0x51f71c, *(0x7882ec as *const usize)); 338 | } 339 | } 340 | } 341 | 342 | #[repr(C)] 343 | pub struct TBitmap {} 344 | 345 | impl TBitmap { 346 | pub unsafe fn new() -> DelphiBox { 347 | delphi_box!(0x462144, 0x4587d4) 348 | } 349 | 350 | pub unsafe fn SaveToFile(&self, filename: &UStr) { 351 | let _: u32 = delphi_call!(0x45e6d8, self, filename.0); 352 | } 353 | 354 | pub unsafe fn LoadFromFile(&self, filename: &UStr) { 355 | let _: u32 = delphi_call!(0x45e64c, self, filename.0); 356 | } 357 | 358 | pub unsafe fn SetWidth(&mut self, width: u32) { 359 | asm!( 360 | "mov ecx, [eax]", 361 | "call [ecx+0x44]", 362 | in("eax") self, 363 | in("edx") width, 364 | clobber_abi("C"), 365 | ); 366 | } 367 | 368 | pub unsafe fn SetHeight(&mut self, height: u32) { 369 | asm!( 370 | "mov ecx, [eax]", 371 | "call [ecx+0x38]", 372 | in("eax") self, 373 | in("edx") height, 374 | clobber_abi("C"), 375 | ); 376 | } 377 | 378 | pub unsafe fn SetSize(&mut self, width: u32, height: u32) { 379 | asm!( 380 | "call {}", 381 | in(reg) (self as *const Self).cast::<*const u32>().read().add(0x6c / 4).read(), 382 | in("eax") self, 383 | in("edx") width, 384 | in("ecx") height, 385 | ); 386 | } 387 | 388 | pub unsafe fn SetPixelFormat(&mut self, format: u8) { 389 | let _: u32 = delphi_call!(0x463dc0, self, u32::from(format)); 390 | } 391 | 392 | pub unsafe fn GetCanvas(&self) -> &mut TCanvas { 393 | let ptr: *mut TCanvas = delphi_call!(0x462a64, self); 394 | &mut *ptr 395 | } 396 | 397 | pub unsafe fn GetScanline(&self, row: u32) -> *const u8 { 398 | delphi_call!(0x462be8, self, row) 399 | } 400 | 401 | #[unsafe(naked)] 402 | pub unsafe extern "fastcall" fn load_from_clipboard(&mut self) -> bool { 403 | naked_asm!( 404 | "push edi", 405 | "push ebx", 406 | // Clipboard 407 | "mov edi, ecx", 408 | "mov eax, 0x4895b4", 409 | "call eax", 410 | "mov ebx, eax", 411 | // TClipboard.HasFormat 412 | "mov dx, 2", 413 | "mov ecx, 0x48957c", 414 | "call ecx", 415 | // return false if false 416 | "test al, al", 417 | "jz 2f", 418 | // TClipboard.GetAsHandle 419 | "mov eax, ebx", 420 | "mov dx, 2", 421 | "mov ecx, 0x489448", 422 | "call ecx", 423 | // TBitmap.LoadFromClipboardFormat 424 | "push 0", 425 | "mov ecx, eax", 426 | "mov edx, 2", 427 | "mov eax, edi", 428 | "mov ebx, 0x46319c", 429 | "call ebx", 430 | "mov al, 1", 431 | // exception handler 432 | "2:", 433 | "pop ebx", 434 | "pop edi", 435 | "ret", 436 | ) 437 | } 438 | } 439 | 440 | #[repr(C)] 441 | pub struct TCanvas { 442 | _unused: [u8; 0x40], 443 | pub FFont: &'static mut TFont, 444 | _unused2: u32, 445 | pub FBrush: &'static mut TBrush, 446 | } 447 | 448 | impl TCanvas { 449 | pub unsafe fn GetHandle(&self) -> usize { 450 | delphi_call!(0x45ce2c, self) 451 | } 452 | 453 | pub unsafe fn TextWidth(&self, text: &UStr) -> u32 { 454 | delphi_call!(0x45bec4, self, text.0) 455 | } 456 | 457 | pub unsafe fn TextHeight(&self, text: &UStr) -> u32 { 458 | delphi_call!(0x45bea4, self, text.0) 459 | } 460 | 461 | pub unsafe fn GetClipRect(&self, rect: &mut [u32; 4]) { 462 | let _: u32 = delphi_call!(0x45ce04, self, rect.as_mut_ptr()); 463 | } 464 | 465 | pub unsafe fn FillRect(&self, rect: &[u32; 4]) { 466 | let _: u32 = delphi_call!(0x45c664, self, rect.as_ptr()); 467 | } 468 | 469 | pub unsafe fn TextOut(&self, x: i32, y: u32, text: &UStr) { 470 | let _: u32 = delphi_call!(0x45c9e8, self, x, y, text.0); 471 | } 472 | } 473 | 474 | #[repr(C)] 475 | pub struct TFont {} 476 | 477 | impl TFont { 478 | pub fn Assign(&mut self, other: &Self) { 479 | unsafe { 480 | let _: u32 = delphi_call!(0x45af94, self, other); 481 | } 482 | } 483 | 484 | pub unsafe fn SetColor(&self, col: u32) { 485 | let _: u32 = delphi_call!(0x45b0d0, self, col); 486 | } 487 | } 488 | 489 | #[repr(C)] 490 | pub struct TBrush {} 491 | 492 | impl TBrush { 493 | pub unsafe fn SetColor(&self, color: u32) { 494 | let _: u32 = delphi_call!(0x45bc24, self, color); 495 | } 496 | 497 | pub unsafe fn SetStyle(&self, style: u8) { 498 | let _: u32 = delphi_call!(0x45bda4, self, u32::from(style)); 499 | } 500 | } 501 | 502 | #[repr(C)] 503 | pub struct TIcon {} 504 | 505 | impl TIcon { 506 | pub unsafe fn SaveToFile(&self, filename: &UStr) { 507 | let _: u32 = delphi_call!(0x45e6d8, self, filename.0); 508 | } 509 | 510 | pub unsafe fn LoadFromFile(&mut self, filename: &UStr) { 511 | let _: u32 = delphi_call!(0x45e64c, self, filename.0); 512 | } 513 | } 514 | 515 | #[repr(C)] 516 | pub struct TMemoryStream { 517 | // fields are dangerous to use 518 | vmt: u32, 519 | memory: *mut u8, 520 | size: usize, 521 | position: usize, 522 | capacity: usize, 523 | } 524 | 525 | impl TMemoryStream { 526 | pub unsafe fn new() -> DelphiBox { 527 | delphi_box!(0x405a4c, 0x433630) 528 | } 529 | 530 | pub fn get_pos(&self) -> u32 { 531 | unsafe { delphi_call!(0x43f234, self) } 532 | } 533 | 534 | pub fn set_pos(&self, pos: u32) { 535 | unsafe { 536 | asm!( 537 | "push 0", 538 | "push {pos_lo}", 539 | "call {call}", 540 | call = in(reg) 0x43f254, 541 | pos_lo = in(reg) pos, 542 | in("eax") self, 543 | clobber_abi("C"), 544 | ); 545 | } 546 | } 547 | 548 | pub fn get_size(&self) -> u32 { 549 | let out; 550 | unsafe { 551 | asm!( 552 | "mov ecx,[eax]", 553 | "call [ecx]", 554 | inlateout("eax") self => out, 555 | clobber_abi("C"), 556 | ); 557 | } 558 | out 559 | } 560 | 561 | pub fn get_slice(&self) -> &[u8] { 562 | unsafe { slice::from_raw_parts(self.memory, self.size) } 563 | } 564 | 565 | pub fn get_slice_mut(&mut self) -> &mut [u8] { 566 | unsafe { slice::from_raw_parts_mut(self.memory, self.size) } 567 | } 568 | 569 | pub unsafe fn read(&self, buf: *mut u8, count: u32) { 570 | let _: u32 = delphi_call!(0x43f488, self, buf, count); 571 | } 572 | 573 | pub unsafe fn load(&self, fname: &UStr) { 574 | let s: *const u16 = fname.0; 575 | let _: u32 = delphi_call!(0x43ff44, self, s); 576 | } 577 | } 578 | 579 | // only usable for real TMemoryStreams 580 | impl std::io::Write for TMemoryStream { 581 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 582 | unsafe { 583 | let result: usize = delphi_call!(0x44006c, self, buf.as_ptr(), buf.len()); 584 | Ok(result) 585 | } 586 | } 587 | 588 | fn flush(&mut self) -> std::io::Result<()> { 589 | Ok(()) 590 | } 591 | } 592 | 593 | pub const DPI: *mut u32 = 0x78810c as _; 594 | 595 | #[repr(C)] 596 | pub struct THelpForm { 597 | padding: [u8; 0x388], 598 | pub editor: *mut TRichEdit, 599 | } 600 | 601 | #[repr(C)] 602 | pub struct TRichEdit { 603 | padding: [u8; 0x6c], 604 | pub colour: u32, 605 | padding2: [u8; 0x2c0 - 0x70], 606 | pub rich_edit_strings: *mut TStrings, 607 | } 608 | 609 | pub struct TStrings {} 610 | 611 | impl TStrings { 612 | pub unsafe fn SaveToFile(&self, fname: &UStr) { 613 | let _: u32 = delphi_call!(0x43e204, self, fname.0); 614 | } 615 | 616 | pub unsafe fn LoadFromFile(&mut self, fname: &UStr) { 617 | let _: u32 = delphi_call!(0x43DEC0, self, fname.0); 618 | } 619 | } 620 | 621 | // weird name for an allocator function 622 | pub unsafe fn GetMem(size: usize) -> *mut T { 623 | delphi_call!(0x40431c, size) 624 | } 625 | 626 | pub unsafe fn FreeMem(mem: *const T) { 627 | let _: u32 = delphi_call!(0x404338, mem); 628 | } 629 | 630 | pub unsafe fn UStrAsg(dest: &mut UStr, src: &UStr) { 631 | let _: u32 = delphi_call!(0x407eb8, dest, src.0); 632 | } 633 | 634 | pub unsafe fn UStrFromPCharLen(dest: &mut UStr, source: *const u8, length: usize) { 635 | let _: u32 = delphi_call!(0x407fe4, dest, source, length); 636 | } 637 | 638 | pub unsafe fn UStrFromPWCharLen(dest: &mut UStr, source: *const u16, length: usize) { 639 | let _: u32 = delphi_call!(0x407ff4, dest, source, length); 640 | } 641 | 642 | pub unsafe fn UStrSetLength(dest: &mut UStr, length: usize) { 643 | let _: u32 = delphi_call!(0x0408244, dest, length); 644 | } 645 | 646 | pub unsafe fn UStrAddRef(str: &mut UStr) { 647 | let _: u32 = delphi_call!(0x407ea0, str.0); 648 | } 649 | 650 | pub unsafe fn UStrClr(str: &mut UStr) { 651 | let _: u32 = delphi_call!(0x407ea8, str); 652 | } 653 | 654 | // terrible hack for the second operand i'm sorry 655 | pub unsafe fn CompareText(a: &UStr, b: *const u16) -> i32 { 656 | delphi_call!(0x415924, a.0, b) 657 | } 658 | 659 | #[repr(transparent)] 660 | pub struct UStr(pub *mut u16); 661 | 662 | impl UStr { 663 | pub const EMPTY: Self = Self(ptr::null_mut()); 664 | 665 | pub fn new(s: impl AsRef) -> Self { 666 | let mut out = UStr(ptr::null_mut()); 667 | let s = s.as_ref(); 668 | // if it takes more than one WTF-16 u16, it will DEFINITELY take more than one WTF-8 u8 669 | let guess_len = s.len(); 670 | if guess_len != 0 { 671 | unsafe { 672 | UStrSetLength(&mut out, guess_len); 673 | let mut real_len = 0; 674 | for (dst, src) in slice::from_raw_parts_mut(out.0, guess_len).iter_mut().zip(s.encode_wide()) { 675 | *dst = src; 676 | real_len += 1; 677 | } 678 | UStrSetLength(&mut out, real_len); 679 | } 680 | } 681 | out 682 | } 683 | 684 | pub fn from_char(c: u16) -> Self { 685 | let mut out = UStr(ptr::null_mut()); 686 | let _: u32 = unsafe { delphi_call!(0x408034, &mut out, u32::from(c)) }; 687 | out 688 | } 689 | 690 | pub fn len(&self) -> usize { 691 | if self.0.is_null() { 0 } else { unsafe { self.0.cast::().sub(1).read() } } 692 | } 693 | 694 | pub fn as_slice(&self) -> &[u16] { 695 | if self.0.is_null() { &[] } else { unsafe { slice::from_raw_parts(self.0, self.len()) } } 696 | } 697 | 698 | pub fn to_os_string(&self) -> OsString { 699 | OsString::from_wide(self.as_slice()) 700 | } 701 | 702 | pub const unsafe fn from_ptr(s: &*const u16) -> &Self { 703 | std::mem::transmute(s) 704 | } 705 | 706 | pub fn push_ustr(&mut self, other: &Self) { 707 | unsafe { 708 | let _: u32 = delphi_call!(0x4082dc, self, other.0); 709 | } 710 | } 711 | } 712 | 713 | impl Default for UStr { 714 | fn default() -> Self { 715 | UStr(ptr::null_mut()) 716 | } 717 | } 718 | 719 | impl Clone for UStr { 720 | fn clone(&self) -> Self { 721 | let mut new_str = Self(self.0); 722 | unsafe { 723 | UStrAddRef(&mut new_str); 724 | } 725 | new_str 726 | } 727 | } 728 | 729 | impl std::ops::Add for UStr { 730 | type Output = UStr; 731 | 732 | fn add(mut self, rhs: UStr) -> Self::Output { 733 | self.push_ustr(&rhs); 734 | self 735 | } 736 | } 737 | 738 | impl std::ops::Add<&UStr> for UStr { 739 | type Output = UStr; 740 | 741 | fn add(mut self, rhs: &UStr) -> Self::Output { 742 | self.push_ustr(rhs); 743 | self 744 | } 745 | } 746 | 747 | impl std::hash::Hash for UStr { 748 | fn hash(&self, state: &mut H) { 749 | self.as_slice().hash(state); 750 | } 751 | } 752 | 753 | impl PartialEq for UStr { 754 | fn eq(&self, other: &Self) -> bool { 755 | self.as_slice() == other.as_slice() 756 | } 757 | } 758 | 759 | impl Eq for UStr {} 760 | 761 | impl Drop for UStr { 762 | fn drop(&mut self) { 763 | unsafe { UStrClr(self) } 764 | } 765 | } 766 | 767 | unsafe impl Sync for UStr {} 768 | 769 | unsafe impl Send for UStr {} 770 | 771 | pub unsafe fn ShowMessage(msg: &UStr) { 772 | let _: u32 = delphi_call!(0x4d43f8, msg.0); 773 | } 774 | 775 | pub fn advance_progress_form(progress: u32) { 776 | unsafe { 777 | let _: u32 = delphi_call!(0x6ca2ac, progress); 778 | } 779 | } 780 | 781 | pub fn close_progress_form() { 782 | unsafe { 783 | let _: u32 = delphi_call!(0x6ca2cc); 784 | } 785 | } 786 | 787 | pub unsafe fn DynArrayClear(a: *mut T, type_info: *const U) { 788 | let _: u32 = delphi_call!(0x409ce0, a, type_info); 789 | } 790 | 791 | pub fn Random() -> u32 { 792 | unsafe { delphi_call!(0x4047b0) } 793 | } 794 | 795 | pub unsafe fn DynArraySetLength(a: *mut *mut T, type_info: *const u8, dimensions: usize, size: usize) { 796 | // this has caller clean-up for some reason 797 | asm!( 798 | "push {d}", 799 | "call {call}", 800 | "add esp,4", 801 | call = in(reg) 0x409be0, 802 | d = in(reg) size, 803 | in("eax") a, 804 | in("edx") type_info, 805 | in("ecx") dimensions, 806 | clobber_abi("C"), 807 | ); 808 | } 809 | 810 | pub fn Now(out: *mut f64) { 811 | unsafe { 812 | asm!( 813 | "call {call}", 814 | "fstp qword ptr [{output}]", 815 | call = in(reg) 0x4199b0, 816 | output = in(reg) out, 817 | // manually exclude eax, edx, ecx from register selection 818 | out("eax") _, 819 | out("edx") _, 820 | out("ecx") _, 821 | clobber_abi("C"), 822 | ); 823 | } 824 | } 825 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub const EV_CREATE: usize = 0; 4 | pub const EV_DESTROY: usize = 1; 5 | pub const EV_ALARM: usize = 2; 6 | pub const EV_STEP: usize = 3; 7 | pub const EV_COLLISION: usize = 4; 8 | pub const EV_KEYBOARD: usize = 5; 9 | pub const EV_MOUSE: usize = 6; 10 | pub const EV_OTHER: usize = 7; 11 | pub const EV_DRAW: usize = 8; 12 | pub const EV_KEYPRESS: usize = 9; 13 | pub const EV_KEYRELEASE: usize = 10; 14 | pub const EV_TRIGGER: usize = 11; 15 | 16 | pub const EVENT_NAMES: [&str; 12] = [ 17 | "Create", 18 | "Destroy", 19 | "Alarm", 20 | "Step", 21 | "Collision", 22 | "Keyboard", 23 | "Mouse", 24 | "Other", 25 | "Draw", 26 | "KeyPress", 27 | "KeyRelease", 28 | "Trigger", 29 | ]; 30 | -------------------------------------------------------------------------------- /src/font_render.rs: -------------------------------------------------------------------------------- 1 | use crate::{UStr, asset::Font, delphi::TBitmap}; 2 | use std::{ptr, slice}; 3 | 4 | #[repr(C)] 5 | #[derive(Default)] 6 | #[allow(non_snake_case)] 7 | struct TEXTMETRICSW { 8 | tmHeight: u32, 9 | tmAscent: u32, 10 | tmDescent: u32, 11 | tmInternalLeading: u32, 12 | tmExternalLeading: u32, 13 | tmAveCharWidth: u32, 14 | tmMaxCharWidth: u32, 15 | tmWeight: u32, 16 | tmOverhang: u32, 17 | tmDigitizedAspectX: u32, 18 | tmDigitizedAspectY: u32, 19 | tmFirstChar: u16, 20 | tmLastChar: u16, 21 | tmDefaultChar: u16, 22 | tmBreakChar: u16, 23 | tmItalic: u8, 24 | tmUnderlined: u8, 25 | tmStruckOut: u8, 26 | tmPitchAndFamily: u8, 27 | tmCharSet: u8, 28 | } 29 | #[repr(C)] 30 | #[derive(Default)] 31 | #[allow(non_snake_case)] 32 | struct GLYPHMETRICS { 33 | gmBlackBoxX: u32, 34 | gmBlackBoxY: u32, 35 | gmptGlyphOrigin: [i32; 2], 36 | gmCellIncX: u32, 37 | gmCellIncY: u32, 38 | } 39 | #[repr(C)] 40 | #[derive(Default)] 41 | #[allow(non_snake_case)] 42 | struct ABC { 43 | abcA: i32, 44 | abcB: u32, 45 | abcC: i32, 46 | } 47 | 48 | #[link(name = "gdi32")] 49 | unsafe extern "system" { 50 | fn GetTextMetricsW(hdc: usize, text_metrics: &mut TEXTMETRICSW) -> u32; 51 | fn GetGlyphOutlineW( 52 | hdc: usize, 53 | uChar: u32, 54 | fuFormat: u32, 55 | lpgm: &mut GLYPHMETRICS, 56 | cjBuffer: u32, 57 | pvBuffer: *mut u8, 58 | lpmat2: &[u16; 8], 59 | ) -> u32; 60 | fn GetCharABCWidthsW(hdc: usize, wFirst: u32, wLast: u32, lpABC: &mut ABC) -> u32; 61 | } 62 | 63 | #[link(name = "kernel32")] 64 | unsafe extern "system" { 65 | fn GetACP() -> u32; 66 | fn MultiByteToWideChar( 67 | CodePage: u32, 68 | dwFlags: u32, 69 | lpMultiByteStr: *const u8, 70 | cbMultiByte: i32, 71 | lpWideCharStr: *mut u16, 72 | cchWideChar: i32, 73 | ) -> i32; 74 | } 75 | 76 | fn charset_to_codepage(charset: u32) -> u32 { 77 | match charset { 78 | 0xa1 => 0x4e5, 79 | 0x80 => 0x3a4, 80 | 0x00 => 0x4e4, 81 | 0x81 => 0x3b5, 82 | 0x82 => 0x551, 83 | 0x86 => 0x3a8, 84 | 0x88 => 0x3b6, 85 | 0xba => 0x4e9, 86 | 0xa2 => 0x4e6, 87 | 0xa3 => 0x4ea, 88 | 0xb1 => 0x4e7, 89 | 0xb2 => 0x4e8, 90 | 0xcc => 0x4e3, 91 | 0xde => 0x36a, 92 | 0xee => 0x4e2, 93 | _ => unsafe { GetACP() }, 94 | } 95 | } 96 | 97 | impl Font { 98 | fn calculate_bitmap_size(&mut self, chars: &[u32]) { 99 | let hspacing = 2; 100 | let vspacing = 2; 101 | let mut width = 0x800; 102 | let mut height = 0x800; 103 | let mut shrink_h = false; 104 | loop { 105 | let mut x = 0; 106 | let mut y = 0; 107 | let mut row_height = 0; 108 | for &c in chars { 109 | if x + self.s_w[c as usize] >= width { 110 | x = 0; 111 | y += row_height + vspacing; 112 | row_height = 0; 113 | } 114 | row_height = row_height.max(self.s_h[c as usize]); 115 | x += self.s_w[c as usize] + hspacing; 116 | } 117 | if y + row_height > height { 118 | break 119 | } 120 | self.s_bw = width; 121 | self.s_bh = height; 122 | shrink_h = !shrink_h; 123 | if shrink_h { 124 | height >>= 1; 125 | } else { 126 | width >>= 1; 127 | } 128 | } 129 | } 130 | 131 | #[unsafe(naked)] 132 | unsafe extern "fastcall" fn copy_glyph_onto_atlas( 133 | &mut self, 134 | format: u32, 135 | y: u32, 136 | glyph_buf: *mut u8, 137 | text_metrics: *const TEXTMETRICSW, 138 | glyph_metrics: *const GLYPHMETRICS, 139 | c: u32, 140 | x: u32, 141 | ) { 142 | std::arch::naked_asm!( 143 | "mov eax, ecx", 144 | "pop ecx", 145 | "xchg ecx, [esp]", 146 | "push 0x5a7b54", // copy glyph onto font atlas 147 | "ret", 148 | ); 149 | } 150 | 151 | pub fn render(&mut self) { 152 | unsafe { 153 | let mut bitmap = TBitmap::new(); 154 | bitmap.SetPixelFormat(6); 155 | let canvas = bitmap.GetCanvas(); 156 | let font = self.make_tfont(); 157 | canvas.FFont.Assign(&font); 158 | drop(font); 159 | let hdc = canvas.GetHandle(); 160 | let matrix = [0u16, 1, 0, 0, 0, 0, 0, 1]; 161 | let mut text_metrics = std::mem::zeroed(); 162 | if GetTextMetricsW(hdc, &mut text_metrics) == 0 { 163 | return 164 | } 165 | let codepage = charset_to_codepage(self.charset); 166 | self.s_bw = 0; 167 | self.s_bh = 0; 168 | let vector_font = text_metrics.tmPitchAndFamily & 0x2 != 0; 169 | let format = [1, 4, 5, 6].get(self.aa_level as usize).copied().unwrap_or(1); 170 | let mut max_buffer_size = 0; 171 | let mut glyph_metrics = std::mem::zeroed(); 172 | // calculate character sizes and required buffer size 173 | let w_height = canvas.TextHeight(&UStr::new("W")); 174 | for c in self.range_start..=self.range_end { 175 | let c = c as usize; 176 | self.s_chr[c] = if c == 0 { 177 | 0 178 | } else { 179 | let mut wc = 1; 180 | let cc = c as u8; 181 | MultiByteToWideChar(codepage, 0, &cc, 1, &mut wc, 1); 182 | wc.into() 183 | }; 184 | if vector_font { 185 | // vector font, use GetGlyphOutlineW to get character dimensions 186 | let out = 187 | GetGlyphOutlineW(hdc, self.s_chr[c], format, &mut glyph_metrics, 0, ptr::null_mut(), &matrix); 188 | if out != u32::MAX { 189 | max_buffer_size = max_buffer_size.max(out); 190 | self.s_w[c] = glyph_metrics.gmBlackBoxX; 191 | self.s_h[c] = (text_metrics.tmAscent as i32 + glyph_metrics.gmBlackBoxY as i32 192 | - glyph_metrics.gmptGlyphOrigin[1]) as u32; 193 | self.s_shift[c] = 194 | (glyph_metrics.gmCellIncX as i32 - glyph_metrics.gmptGlyphOrigin[0].min(0)) as u32; 195 | self.s_offset[c] = glyph_metrics.gmptGlyphOrigin[0] as u32; 196 | } else { 197 | self.s_w[c] = 0; 198 | self.s_h[c] = 0; 199 | self.s_shift[c] = 0; 200 | self.s_offset[c] = 0; 201 | } 202 | } else { 203 | // bitmap font, use ABC 204 | let mut abc = Default::default(); 205 | if GetCharABCWidthsW(hdc, self.s_chr[c], self.s_chr[c], &mut abc) != 0 { 206 | self.s_w[c] = abc.abcB + 1; 207 | self.s_shift[c] = (abc.abcA + abc.abcB as i32 + abc.abcC) as u32; 208 | self.s_offset[c] = abc.abcA as _; 209 | } else { 210 | let width = canvas.TextWidth(&UStr::from_char(c as u16)); 211 | self.s_shift[c] = width; 212 | self.s_w[c] = width + 1; 213 | if self.italic { 214 | self.s_w[c] += w_height / 2 + 1; 215 | } 216 | } 217 | self.s_h[c] = w_height; 218 | } 219 | self.s_x[c] = 0; 220 | self.s_y[c] = 0; 221 | } 222 | if self.s_h[0x20] == 0 { 223 | self.s_h[0x20] = text_metrics.tmHeight; 224 | } 225 | if self.s_w[0x20] == 0 { 226 | let out = GetGlyphOutlineW(hdc, 0x20, format, &mut glyph_metrics, 0, ptr::null_mut(), &matrix); 227 | self.s_w[0x20] = if out != u32::MAX && glyph_metrics.gmCellIncX != 0 { 228 | glyph_metrics.gmCellIncX 229 | } else { 230 | text_metrics.tmAveCharWidth.min(text_metrics.tmMaxCharWidth) 231 | }; 232 | self.s_shift[0x20] = self.s_w[0x20]; 233 | } 234 | // setup 235 | let mut chars = (self.range_start..=self.range_end).collect::>(); 236 | chars.sort_unstable_by_key(|&c| -((self.s_w[c as usize] * self.s_h[c as usize]) as i32)); 237 | self.calculate_bitmap_size(&chars); 238 | if !vector_font { 239 | bitmap.SetSize(self.s_bw, self.s_bh); 240 | let canvas = bitmap.GetCanvas(); 241 | canvas.FBrush.SetColor(0); 242 | let mut rect = [0; 4]; 243 | canvas.GetClipRect(&mut rect); 244 | canvas.FillRect(&rect); 245 | canvas.FFont.SetColor(0xffffff); 246 | canvas.FBrush.SetStyle(1); 247 | } 248 | let canvas = bitmap.GetCanvas(); 249 | self.s_bytes.alloc((self.s_bw * self.s_bh) as usize); 250 | self.s_bytes.fill(0); // is this really necessary? 251 | let mut glyph_buf = vec![0; max_buffer_size as usize]; 252 | // draw the characters 253 | let mut x = 0; 254 | let mut y = 0; 255 | let mut row_height = 0; 256 | let hspacing = 2; 257 | let vspacing = 2; 258 | for c in chars { 259 | let c = c as usize; 260 | if x + self.s_w[c] >= self.s_bw { 261 | x = 0; 262 | y += row_height + vspacing; 263 | row_height = 0; 264 | } 265 | row_height = row_height.max(self.s_h[c]); 266 | self.s_x[c] = x; 267 | self.s_y[c] = y; 268 | if c != 0x20 { 269 | if vector_font { 270 | // vector font, use GetGlyphOutlineW 271 | glyph_buf.fill(0); 272 | GetGlyphOutlineW( 273 | hdc, 274 | self.s_chr[c], 275 | format, 276 | &mut glyph_metrics, 277 | max_buffer_size, 278 | glyph_buf.as_mut_ptr(), 279 | &matrix, 280 | ); 281 | self.copy_glyph_onto_atlas( 282 | format, 283 | y, 284 | glyph_buf.as_mut_ptr(), 285 | &text_metrics, 286 | &glyph_metrics, 287 | c as u32, 288 | x, 289 | ); 290 | } else { 291 | // bitmap font, draw onto bitmap 292 | let s = UStr::from_char(c as u16); 293 | let mut abc = Default::default(); 294 | if GetCharABCWidthsW(hdc, self.s_chr[c], self.s_chr[c], &mut abc) != 0 { 295 | canvas.TextOut(self.s_x[c] as i32 - abc.abcA, self.s_y[c], &s); 296 | } else { 297 | canvas.TextOut(self.s_x[c] as i32, self.s_y[c], &s); 298 | } 299 | } 300 | } 301 | x += self.s_w[c as usize] + hspacing; 302 | } 303 | if !vector_font { 304 | // download bitmap 305 | for (y, dst_row) in self.s_bytes.chunks_mut(self.s_bw as usize).enumerate() { 306 | let scanline = bitmap.GetScanline(y as u32); 307 | for (dst, src) in 308 | dst_row.iter_mut().zip(slice::from_raw_parts(scanline, self.s_bw as usize * 3).chunks(3)) 309 | { 310 | *dst = (src.iter().copied().map(u16::from).sum::() / 3u16) as u8; 311 | } 312 | } 313 | } 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/ide.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crate::{ 3 | asset::*, 4 | delphi::{DelphiBox, TTreeNode, TTreeView, UStr}, 5 | list::DelphiList, 6 | }; 7 | use std::slice; 8 | 9 | type IntPtr = *mut usize; 10 | type GaplessList = *mut DelphiList, P>; 11 | type TypeInfoPtr = usize; 12 | 13 | macro_rules! get_member { 14 | (pub $n:ident, $t:ty, $p:ident) => { 15 | pub fn $n(&self) -> &[$t] { 16 | unsafe { (&(*self.0).$p).get_unchecked(self.range()) } 17 | } 18 | }; 19 | ($n:ident, $t:ty, $p:ident) => { 20 | fn $n(&self) -> &[$t] { 21 | unsafe { (&(*self.0).$p).get_unchecked(self.range()) } 22 | } 23 | }; 24 | } 25 | 26 | macro_rules! get_member_mut { 27 | (pub $n:ident, $t:ty, $p:ident) => { 28 | pub fn $n(&self) -> &'static mut [$t] { 29 | unsafe { (&mut (*self.0).$p).get_unchecked_mut(self.range()) } 30 | } 31 | }; 32 | ($n:ident, $t:ty, $p:ident) => { 33 | fn $n(&self) -> &'static mut [$t] { 34 | unsafe { (&mut (*self.0).$p).get_unchecked_mut(self.range()) } 35 | } 36 | }; 37 | } 38 | 39 | pub trait AssetListTrait: Sync { 40 | fn assets(&self) -> &[Option>]; 41 | fn assets_mut(&self) -> &'static mut [Option>]; 42 | fn names(&self) -> &[UStr]; 43 | fn names_mut(&self) -> &'static mut [UStr]; 44 | fn timestamps(&self) -> &[f64]; 45 | fn alloc(&self, count: usize); 46 | } 47 | 48 | #[repr(C)] 49 | struct AssetListInner { 50 | pub assets: DelphiList>, P1>, 51 | pub forms: DelphiList, 52 | pub names: DelphiList, 53 | pub timestamps: DelphiList, 54 | pub count: usize, 55 | } 56 | 57 | #[repr(C)] 58 | struct GraphicAssetListInner< 59 | T: 'static, 60 | const P1: usize, 61 | const P2: usize, 62 | const P3: usize, 63 | const P4: usize, 64 | const P5: usize, 65 | > { 66 | pub assets: DelphiList>, P1>, 67 | pub forms: DelphiList, 68 | pub names: DelphiList, 69 | pub timestamps: DelphiList, 70 | pub thumbs: DelphiList, 71 | pub count: usize, 72 | } 73 | 74 | pub struct AssetList( 75 | *mut AssetListInner, 76 | ); 77 | 78 | pub struct GraphicAssetList< 79 | T: 'static, 80 | const P1: usize, 81 | const P2: usize, 82 | const P3: usize, 83 | const P4: usize, 84 | const P5: usize, 85 | >(*mut GraphicAssetListInner); 86 | 87 | unsafe impl Sync 88 | for AssetList 89 | { 90 | } 91 | 92 | unsafe impl Sync 93 | for GraphicAssetList 94 | { 95 | } 96 | 97 | impl AssetList { 98 | get_member!(pub forms, Form, forms); 99 | 100 | get_member_mut!(pub forms_mut, Form, forms); 101 | 102 | get_member_mut!(pub timestamps_mut, f64, timestamps); 103 | 104 | unsafe fn range(&self) -> std::ops::RangeTo { 105 | ..(*self.0).count 106 | } 107 | } 108 | 109 | impl 110 | GraphicAssetList 111 | { 112 | get_member!(pub forms, Form, forms); 113 | 114 | get_member!(pub forms_mut, Form, forms); 115 | 116 | get_member_mut!(pub timestamps_mut, f64, timestamps); 117 | 118 | get_member_mut!(pub thumbs_mut, i32, thumbs); 119 | 120 | unsafe fn range(&self) -> std::ops::RangeTo { 121 | ..(*self.0).count 122 | } 123 | } 124 | 125 | impl AssetListTrait 126 | for AssetList 127 | { 128 | get_member!(assets, Option>, assets); 129 | 130 | get_member_mut!(assets_mut, Option>, assets); 131 | 132 | get_member!(names, UStr, names); 133 | 134 | get_member_mut!(names_mut, UStr, names); 135 | 136 | get_member!(timestamps, f64, timestamps); 137 | 138 | fn alloc(&self, count: usize) { 139 | unsafe { 140 | (*self.0).count = count; 141 | (*self.0).assets.alloc(count); 142 | (*self.0).forms.alloc(count); 143 | (*self.0).names.alloc(count); 144 | (*self.0).timestamps.alloc(count); 145 | } 146 | } 147 | } 148 | 149 | impl AssetListTrait 150 | for GraphicAssetList 151 | { 152 | get_member!(assets, Option>, assets); 153 | 154 | get_member_mut!(assets_mut, Option>, assets); 155 | 156 | get_member!(names, UStr, names); 157 | 158 | get_member_mut!(names_mut, UStr, names); 159 | 160 | get_member!(timestamps, f64, timestamps); 161 | 162 | fn alloc(&self, count: usize) { 163 | unsafe { 164 | (*self.0).count = count; 165 | (*self.0).assets.alloc(count); 166 | (*self.0).forms.alloc(count); 167 | (*self.0).names.alloc(count); 168 | (*self.0).timestamps.alloc(count); 169 | (*self.0).thumbs.alloc(count); 170 | } 171 | } 172 | } 173 | 174 | const TRIGGERS: *mut DelphiList>, 0x6bc93c> = 0x77f3f4 as _; 175 | const TRIGGER_COUNT: IntPtr = 0x77f3f8 as _; 176 | pub const TRIGGERS_UPDATED: *const bool = 0x790058 as _; 177 | 178 | const CONSTANT_COUNT: IntPtr = 0x77f3c4 as _; 179 | const CONSTANT_NAMES: *mut DelphiList = 0x78c14c as _; 180 | const CONSTANT_VALUES: *mut DelphiList = 0x78c150 as _; 181 | const CONSTANT_NAME_TYPE: TypeInfoPtr = 0x696594 as _; 182 | const CONSTANT_VALUE_TYPE: TypeInfoPtr = 0x6965c0 as _; 183 | pub const CONSTANTS_UPDATED: *mut bool = 0x78c154 as _; 184 | 185 | pub const SOUNDS: AssetList< 186 | Sound, 187 | SOUND_TYPEINFO, 188 | { SOUND_TYPEINFO + SOUND_TYPESIZE }, 189 | { SOUND_TYPEINFO + SOUND_TYPESIZE * 2 }, 190 | { SOUND_TYPEINFO + SOUND_TYPESIZE * 3 }, 191 | > = AssetList(0x77f2b8 as _); 192 | const SOUND_TYPEINFO: TypeInfoPtr = 0x651ce0 as _; 193 | const SOUND_TYPESIZE: usize = 0x2c; 194 | pub const SOUNDS_UPDATED: *mut bool = 0x78a1b0 as _; 195 | 196 | pub const SPRITES: GraphicAssetList< 197 | Sprite, 198 | SPRITE_TYPEINFO, 199 | { SPRITE_TYPEINFO + SPRITE_TYPESIZE }, 200 | { SPRITE_TYPEINFO + SPRITE_TYPESIZE * 2 }, 201 | { SPRITE_TYPEINFO + SPRITE_TYPESIZE * 3 }, 202 | { SPRITE_TYPEINFO + SPRITE_TYPESIZE * 4 }, 203 | > = GraphicAssetList(0x77f4c4 as _); 204 | const SPRITE_TYPEINFO: TypeInfoPtr = 0x6f522c as _; 205 | const SPRITE_TYPESIZE: usize = 0x2c; 206 | pub const SPRITES_UPDATED: *mut bool = 0x790170 as _; 207 | 208 | pub const BACKGROUNDS: GraphicAssetList< 209 | Background, 210 | BACKGROUND_TYPEINFO, 211 | { BACKGROUND_TYPEINFO + BACKGROUND_TYPESIZE }, 212 | { BACKGROUND_TYPEINFO + BACKGROUND_TYPESIZE * 2 }, 213 | { BACKGROUND_TYPEINFO + BACKGROUND_TYPESIZE * 3 }, 214 | { BACKGROUND_TYPEINFO + BACKGROUND_TYPESIZE * 4 }, 215 | > = GraphicAssetList(0x77f1ac as _); 216 | const BACKGROUND_TYPEINFO: TypeInfoPtr = 0x64d734 as _; 217 | const BACKGROUND_TYPESIZE: usize = 0x30; 218 | pub const BACKGROUNDS_UPDATED: *mut bool = 0x78a168 as _; 219 | 220 | pub const PATHS: AssetList< 221 | Path, 222 | PATH_TYPEINFO, 223 | { PATH_TYPEINFO + PATH_TYPESIZE }, 224 | { PATH_TYPEINFO + PATH_TYPESIZE * 2 }, 225 | { PATH_TYPEINFO + PATH_TYPESIZE * 3 }, 226 | > = AssetList(0x77f608 as _); 227 | const PATH_TYPEINFO: TypeInfoPtr = 0x72207c as _; 228 | const PATH_TYPESIZE: usize = 0x28; 229 | pub const PATHS_UPDATED: *mut bool = 0x7a4658 as _; 230 | 231 | pub const SCRIPTS: AssetList< 232 | Script, 233 | SCRIPT_TYPEINFO, 234 | { SCRIPT_TYPEINFO + SCRIPT_TYPESIZE }, 235 | { SCRIPT_TYPEINFO + SCRIPT_TYPESIZE * 2 }, 236 | { SCRIPT_TYPEINFO + SCRIPT_TYPESIZE * 3 }, 237 | > = AssetList(0x77f2cc as _); 238 | const SCRIPT_TYPEINFO: TypeInfoPtr = 0x6550a8 as _; 239 | const SCRIPT_TYPESIZE: usize = 0x2c; 240 | pub const SCRIPTS_UPDATED: *mut bool = 0x78a1b8 as _; 241 | 242 | pub const FONTS: AssetList< 243 | Font, 244 | FONT_TYPEINFO, 245 | { FONT_TYPEINFO + FONT_TYPESIZE }, 246 | { FONT_TYPEINFO + FONT_TYPESIZE * 2 }, 247 | { FONT_TYPEINFO + FONT_TYPESIZE * 3 }, 248 | > = AssetList(0x77f4fc as _); 249 | const FONT_TYPEINFO: TypeInfoPtr = 0x6fc680 as _; 250 | const FONT_TYPESIZE: usize = 0x28; 251 | pub const FONTS_UPDATED: *mut bool = 0x790190 as _; 252 | 253 | pub const TIMELINES: AssetList< 254 | Timeline, 255 | TIMELINE_TYPEINFO, 256 | { TIMELINE_TYPEINFO + TIMELINE_TYPESIZE }, 257 | { TIMELINE_TYPEINFO + TIMELINE_TYPESIZE * 2 }, 258 | { TIMELINE_TYPEINFO + TIMELINE_TYPESIZE * 3 }, 259 | > = AssetList(0x77f4e4 as _); 260 | const TIMELINE_TYPEINFO: TypeInfoPtr = 0x6fa020 as _; 261 | const TIMELINE_TYPESIZE: usize = 0x2c; 262 | pub const TIMELINES_UPDATED: *mut bool = 0x790188 as _; 263 | 264 | pub const OBJECTS: AssetList< 265 | Object, 266 | OBJECT_TYPEINFO, 267 | { OBJECT_TYPEINFO + OBJECT_TYPESIZE }, 268 | { OBJECT_TYPEINFO + OBJECT_TYPESIZE * 2 }, 269 | { OBJECT_TYPEINFO + OBJECT_TYPESIZE * 3 }, 270 | > = AssetList(0x77f0d0 as _); 271 | const OBJECT_TYPEINFO: TypeInfoPtr = 0x62c4a8 as _; 272 | const OBJECT_TYPESIZE: usize = 0x2c; 273 | pub const OBJECTS_UPDATED: *mut bool = 0x78a0d4 as _; 274 | 275 | pub const ROOMS: AssetList< 276 | Room, 277 | ROOM_TYPEINFO, 278 | { ROOM_TYPEINFO + ROOM_TYPESIZE }, 279 | { ROOM_TYPEINFO + ROOM_TYPESIZE * 2 }, 280 | { ROOM_TYPEINFO + ROOM_TYPESIZE * 3 }, 281 | > = AssetList(0x77f3a8 as _); 282 | const ROOM_TYPEINFO: TypeInfoPtr = 0x6928f8 as _; 283 | const ROOM_TYPESIZE: usize = 0x28; 284 | pub const ROOMS_UPDATED: *mut bool = 0x78a1f8 as _; 285 | 286 | const INCLUDED_FILES: GaplessList = 0x77f420 as _; 287 | const INCLUDED_FILE_TIMESTAMPS: *mut DelphiList = 0x77f424 as _; 288 | const INCLUDED_FILE_COUNT: IntPtr = 0x77f428 as _; 289 | pub const INCLUDED_FILES_UPDATED: *mut bool = 0x7900a0 as _; 290 | 291 | const EXTENSIONS: GaplessList = 0x77f5d4 as _; 292 | const EXTENSION_COUNT: IntPtr = 0x77f5d8 as _; 293 | const EXTENSIONS_LOADED: *mut DelphiList = 0x790a14 as _; 294 | pub const EXTENSIONS_UPDATED: *mut bool = 0x790a0c as _; 295 | 296 | pub const GAME_ID: IntPtr = 0x7907f4 as IntPtr; 297 | 298 | pub const RESOURCE_TREE: *const *mut TTreeView = 0x79a9e8 as _; 299 | pub const RESOURCE_TREE_HIDDEN: *const *mut TTreeView = 0x79a9ec as _; 300 | pub const RT_OBJECTS: *const *const TTreeNode = 0x79a9b8 as _; 301 | pub const RT_SPRITES: *const *const TTreeNode = 0x79a9bc as _; 302 | pub const RT_SOUNDS: *const *const TTreeNode = 0x79a9c0 as _; 303 | pub const RT_ROOMS: *const *const TTreeNode = 0x79a9c4 as _; 304 | pub const RT_BACKGROUNDS: *const *const TTreeNode = 0x79a9c8 as _; 305 | pub const RT_PATHS: *const *const TTreeNode = 0x79a9cc as _; 306 | pub const RT_SCRIPTS: *const *const TTreeNode = 0x79a9d0 as _; 307 | pub const RT_FONTS: *const *const TTreeNode = 0x79a9d4 as _; 308 | pub const RT_TIMELINES: *const *const TTreeNode = 0x79a9d8 as _; 309 | pub const _RT_GAME_INFO: *const *const TTreeNode = 0x79a9dc as _; 310 | pub const _RT_GLOBAL_GAME_SETTINGS: *const *const TTreeNode = 0x79a9e0 as _; 311 | pub const _RT_EXTENSION_PACKAGES: *const *const TTreeNode = 0x79a9e4 as _; 312 | pub const RESOURCE_TREE_UPDATED: *const bool = 0x77f5f4 as _; 313 | 314 | pub const LAST_INSTANCE_ID: IntPtr = 0x77f2e0 as IntPtr; 315 | pub const LAST_TILE_ID: IntPtr = 0x77f2e4 as IntPtr; 316 | 317 | pub const SETTINGS_UPDATED: *mut bool = 0x790824 as _; 318 | pub mod settings { 319 | #![allow(dead_code)] 320 | use crate::delphi::{DelphiBox, TBitmap, TIcon, UStr}; 321 | 322 | pub const FULLSCREEN: *mut bool = 0x77f514 as _; 323 | pub const INTERPOLATE_PIXELS: *mut bool = 0x77f518 as _; 324 | pub const DONT_DRAW_BORDER: *mut bool = 0x77f51c as _; 325 | pub const DISPLAY_CURSOR: *mut bool = 0x77f520 as _; 326 | pub const SCALING: *mut i32 = 0x77f524 as _; 327 | pub const ALLOW_RESIZE: *mut bool = 0x77f528 as _; 328 | pub const WINDOW_ON_TOP: *mut bool = 0x77f52c as _; 329 | pub const CLEAR_COLOUR: *mut u32 = 0x77f530 as _; 330 | pub const SET_RESOLUTION: *mut bool = 0x77f534 as _; 331 | pub const COLOUR_DEPTH: *mut u32 = 0x77f538 as _; 332 | pub const RESOLUTION: *mut u32 = 0x77f53c as _; 333 | pub const FREQUENCY: *mut u32 = 0x77f540 as _; 334 | pub const DONT_SHOW_BUTTONS: *mut bool = 0x77f544 as _; 335 | pub const VSYNC_AND_FORCE_CPU: *mut u32 = 0x77f54c as _; 336 | pub const DISABLE_SCREENSAVER: *mut bool = 0x77f550 as _; 337 | pub const F4_FULLSCREEN: *mut bool = 0x77f554 as _; 338 | pub const F1_HELP: *mut bool = 0x77f558 as _; 339 | pub const ESC_CLOSE: *mut bool = 0x77f55c as _; 340 | pub const F5_SAVE_F6_LOAD: *mut bool = 0x77f560 as _; 341 | pub const F9_SCREENSHOT: *mut bool = 0x77f564 as _; 342 | pub const TREAT_CLOSE_AS_ESC: *mut bool = 0x77f568 as _; 343 | pub const PRIORITY: *mut u32 = 0x77f56c as _; 344 | pub const FREEZE_ON_LOSE_FOCUS: *mut bool = 0x77f548 as _; 345 | pub const LOADING_BAR: *mut u32 = 0x77f570 as _; 346 | pub const LOADING_BACKGROUND: *mut Option> = 0x77f580 as _; 347 | pub const LOADING_FOREGROUND: *mut Option> = 0x77f57c as _; 348 | pub const HAS_CUSTOM_LOAD_IMAGE: *mut bool = 0x77f574 as _; 349 | pub const CUSTOM_LOAD_IMAGE: *mut Option> = 0x77f578 as _; 350 | pub const LOADING_TRANSPARENT: *mut bool = 0x77f584 as _; 351 | pub const LOADING_TRANSLUCENCY: *mut u32 = 0x77f588 as _; 352 | pub const LOADING_PROGRESS_BAR_SCALE: *mut bool = 0x77f58c as _; 353 | pub const ICON: *mut DelphiBox = 0x77f590 as _; 354 | pub const SHOW_ERROR_MESSAGES: *mut bool = 0x77f594 as _; 355 | pub const LOG_ERRORS: *mut bool = 0x77f598 as _; 356 | pub const ALWAYS_ABORT: *mut bool = 0x77f59c as _; 357 | pub const ZERO_UNINITIALIZED_VARS: *mut bool = 0x77f5a0 as _; 358 | pub const ERROR_ON_UNINITIALIZED_ARGS: *mut bool = 0x77f5a4 as _; 359 | pub const INFO_AUTHOR: *mut UStr = 0x77f5a8 as _; 360 | pub const INFO_VERSION: *mut UStr = 0x77f5ac as _; 361 | pub const INFO_TIMESTAMP: *mut f64 = 0x79081c as _; 362 | pub const INFO_INFORMATION: *mut UStr = 0x77f5b0 as _; 363 | pub const VERSION_MAJOR: *mut u32 = 0x77f5b4 as _; 364 | pub const VERSION_MINOR: *mut u32 = 0x77f5b8 as _; 365 | pub const VERSION_RELEASE: *mut u32 = 0x77f5bc as _; 366 | pub const VERSION_BUILD: *mut u32 = 0x77f5c0 as _; 367 | pub const EXE_COMPANY: *mut UStr = 0x77f5c4 as _; 368 | pub const EXE_PRODUCT: *mut UStr = 0x77f5c8 as _; 369 | pub const EXE_COPYRIGHT: *mut UStr = 0x77f5cc as _; 370 | pub const EXE_DESCRIPTION: *mut UStr = 0x77f5d0 as _; 371 | } 372 | 373 | pub const GAME_INFO_UPDATED: *mut bool = 0x78895c as _; 374 | pub mod game_info { 375 | use crate::delphi::{THelpForm, UStr}; 376 | pub const NEW_WINDOW: *mut bool = 0x77b578 as _; 377 | pub const CAPTION: *mut UStr = 0x77b57c as _; 378 | pub const LEFT: *mut i32 = 0x77b580 as _; 379 | pub const TOP: *mut i32 = 0x77b584 as _; 380 | pub const WIDTH: *mut i32 = 0x77b588 as _; 381 | pub const HEIGHT: *mut i32 = 0x77b58c as _; 382 | pub const BORDER: *mut bool = 0x77b590 as _; 383 | pub const RESIZABLE: *mut bool = 0x77b594 as _; 384 | pub const WINDOW_ON_TOP: *mut bool = 0x77b598 as _; 385 | pub const FREEZE_GAME: *mut bool = 0x77b59c as _; 386 | pub const FORM: *mut *const THelpForm = 0x788958 as _; 387 | } 388 | 389 | const ACTION_LIBRARIES: *const &'static ActionLibrary = 0x79a660 as _; 390 | const ACTION_LIBRARY_COUNT: IntPtr = 0x77f5dc as _; 391 | 392 | pub const PROJECT_PATH: *mut UStr = 0x77f44c as _; 393 | 394 | pub fn initialize_project() { 395 | unsafe { 396 | let _: u32 = delphi_call!(0x705964); 397 | } 398 | } 399 | 400 | macro_rules! read_array { 401 | ($n:ident, $nm:ident, $t:ty, $p:expr, $c:expr) => { 402 | pub fn $n<'a>() -> &'a [$t] { 403 | unsafe { (&$p).get_unchecked(..$c) } 404 | } 405 | 406 | pub fn $nm<'a>() -> &'a mut [$t] { 407 | unsafe { (&mut $p).get_unchecked_mut(..$c) } 408 | } 409 | }; 410 | } 411 | 412 | read_array!(get_constant_names, get_constant_names_mut, UStr, *CONSTANT_NAMES, *CONSTANT_COUNT); 413 | read_array!(get_constants, get_constants_mut, UStr, *CONSTANT_VALUES, *CONSTANT_COUNT); 414 | read_array!(get_triggers, get_triggers_mut, Option>, *TRIGGERS, *TRIGGER_COUNT); 415 | 416 | read_array!(get_included_files, get_included_files_mut, DelphiBox, *INCLUDED_FILES, *INCLUDED_FILE_COUNT); 417 | read_array!( 418 | get_included_file_timestamps, 419 | get_included_file_timestamps_mut, 420 | f64, 421 | *INCLUDED_FILE_TIMESTAMPS, 422 | *INCLUDED_FILE_COUNT 423 | ); 424 | read_array!(get_extensions, get_extensions_mut, DelphiBox, *EXTENSIONS, *EXTENSION_COUNT); 425 | read_array!(get_extensions_loaded, get_extensions_loaded_mut, bool, *EXTENSIONS_LOADED, *EXTENSION_COUNT); 426 | 427 | pub fn get_action_libraries<'a>() -> &'a [&'a ActionLibrary] { 428 | unsafe { slice::from_raw_parts(ACTION_LIBRARIES, ACTION_LIBRARY_COUNT.read()) } 429 | } 430 | 431 | pub fn alloc_constants(count: usize) { 432 | unsafe { 433 | CONSTANT_COUNT.write(count); 434 | (*CONSTANT_NAMES).alloc(count); 435 | (*CONSTANT_VALUES).alloc(count); 436 | } 437 | } 438 | 439 | pub fn alloc_triggers(count: usize) { 440 | unsafe { 441 | TRIGGER_COUNT.write(count); 442 | (*TRIGGERS).alloc(count); 443 | } 444 | } 445 | 446 | pub fn alloc_included_files(count: usize) { 447 | unsafe { 448 | INCLUDED_FILE_COUNT.write(count); 449 | (*INCLUDED_FILES).alloc_fill(count, IncludedFile::new); 450 | (*INCLUDED_FILE_TIMESTAMPS).alloc(count); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/list.rs: -------------------------------------------------------------------------------- 1 | use crate::delphi::DynArraySetLength; 2 | use std::{ 3 | arch::asm, 4 | ops::{Deref, DerefMut}, 5 | ptr, slice, 6 | }; 7 | 8 | #[repr(transparent)] 9 | pub struct DelphiList(pub *mut T); 10 | 11 | impl Default for DelphiList { 12 | fn default() -> Self { 13 | Self(ptr::null_mut()) 14 | } 15 | } 16 | 17 | impl Deref for DelphiList { 18 | type Target = [T]; 19 | 20 | fn deref(&self) -> &Self::Target { 21 | if self.0.is_null() { 22 | &[] 23 | } else { 24 | unsafe { slice::from_raw_parts(self.0, self.0.cast::().sub(1).read()) } 25 | } 26 | } 27 | } 28 | 29 | impl DerefMut for DelphiList { 30 | fn deref_mut(&mut self) -> &mut Self::Target { 31 | if self.0.is_null() { 32 | &mut [] 33 | } else { 34 | unsafe { slice::from_raw_parts_mut(self.0, self.0.cast::().sub(1).read()) } 35 | } 36 | } 37 | } 38 | 39 | impl<'a, T, const P: usize> IntoIterator for &'a DelphiList { 40 | type IntoIter = slice::Iter<'a, T>; 41 | type Item = &'a T; 42 | 43 | fn into_iter(self) -> Self::IntoIter { 44 | self.deref().into_iter() 45 | } 46 | } 47 | 48 | impl DelphiList { 49 | pub unsafe fn alloc_evil(&self, len: usize) { 50 | asm!( 51 | "push {d}", 52 | "call {call}", 53 | "add esp,4", 54 | call = in(reg) 0x409be0, 55 | d = in(reg) len, 56 | in("eax") &self.0, 57 | in("edx") P, 58 | in("ecx") 1, 59 | clobber_abi("C"), 60 | ); 61 | } 62 | 63 | pub fn alloc(&mut self, len: usize) { 64 | unsafe { 65 | DynArraySetLength(&mut self.0, P as _, 1, len); 66 | } 67 | } 68 | 69 | pub fn alloc_fill(&mut self, len: usize, f: impl Fn() -> T) { 70 | self.alloc(len); 71 | unsafe { 72 | for dst in self.get_unchecked_mut(..len) { 73 | let dst = dst as *mut T; 74 | dst.write(f()); 75 | } 76 | } 77 | } 78 | } 79 | 80 | unsafe impl Sync for DelphiList {} 81 | -------------------------------------------------------------------------------- /src/regular.rs: -------------------------------------------------------------------------------- 1 | pub mod extension_watcher; 2 | pub mod project_watcher; 3 | 4 | extern "fastcall" fn on_notify() { 5 | if project_watcher::watching() { 6 | project_watcher::on_notify(); 7 | } 8 | extension_watcher::on_notify(); 9 | } 10 | 11 | pub fn init() { 12 | enable_timer(); 13 | let _ = extension_watcher::setup_watcher(); 14 | } 15 | 16 | fn enable_timer() { 17 | unsafe { 18 | let main_form = (0x790100 as *const *mut *mut usize).read(); 19 | let timer_ptr = main_form.add(0x65c / 4); 20 | if timer_ptr.read().is_null() { 21 | // create timer if needed 22 | *timer_ptr = delphi_call!(0x48e048, 0x48ab50, 1, main_form); 23 | } 24 | let timer = timer_ptr.read(); 25 | timer.add(0x34 / 4).write(1000); // interval (ms) 26 | timer.add(0x40 / 4).write(on_notify as _); // event 27 | timer.add(0x48 / 4).write(1); // enabled 28 | let _: u32 = delphi_call!(0x48e12c, timer); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/regular/extension_watcher.rs: -------------------------------------------------------------------------------- 1 | use crate::delphi::UStr; 2 | use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, recommended_watcher}; 3 | use once_cell::unsync::Lazy; 4 | use parking_lot::Mutex; 5 | use std::{ 6 | env::args, 7 | path::PathBuf, 8 | sync::mpsc::{Receiver, Sender, channel}, 9 | time::{Duration, SystemTime}, 10 | }; 11 | 12 | static WATCHER_CHANNEL: Mutex>, Receiver>)>> = 13 | Mutex::new(Lazy::new(|| channel())); 14 | static mut WATCHER: Option = None; 15 | 16 | pub fn setup_watcher() -> notify::Result<()> { 17 | unsafe { 18 | LAST_UPDATE = SystemTime::now(); 19 | } 20 | let lock = WATCHER_CHANNEL.lock(); 21 | let tx = lock.0.clone(); 22 | drop(lock); 23 | let watcher = unsafe { WATCHER.insert(recommended_watcher(tx)?) }; 24 | let mut exe_path = PathBuf::from(args().next().unwrap()); 25 | exe_path.pop(); 26 | exe_path.push("extensions"); 27 | watcher.watch(&exe_path, RecursiveMode::NonRecursive)?; 28 | let appdata_path = unsafe { UStr::from_ptr(&*(0x78898c as *const *const u16)) }; 29 | let appdata_path = PathBuf::from(appdata_path.to_os_string()); 30 | watcher.watch(&appdata_path, RecursiveMode::NonRecursive)?; 31 | Ok(()) 32 | } 33 | 34 | static mut LAST_UPDATE: SystemTime = SystemTime::UNIX_EPOCH; 35 | static mut NEEDS_UPDATE: bool = false; 36 | 37 | pub fn on_notify() { 38 | let lock = WATCHER_CHANNEL.lock(); 39 | while let Some(_event) = lock.1.try_recv().ok().map(|e| e.unwrap()).filter(|event| match event.kind { 40 | EventKind::Create(_) => false, 41 | EventKind::Modify(_) => { 42 | event.paths.iter().filter(|p| p.extension() == Some("ged".as_ref())).any(|p| { 43 | if let Ok(modified) = p.metadata().and_then(|m| m.modified()) { 44 | unsafe { 45 | // it's foreign if modified time is after save end 46 | // if someone edited a file in notepad, we'll get one of these 47 | // if someone dragged in an older copy, we'll get a NoticeRemove instead 48 | LAST_UPDATE < modified 49 | } 50 | } else { 51 | true 52 | } 53 | }) 54 | }, 55 | _ => true, 56 | }) { 57 | unsafe { 58 | LAST_UPDATE = SystemTime::now(); 59 | NEEDS_UPDATE = true; 60 | } 61 | } 62 | } 63 | 64 | pub extern "fastcall" fn update_extensions() { 65 | unsafe { 66 | if NEEDS_UPDATE { 67 | if LAST_UPDATE.elapsed().map(|t| t >= Duration::from_secs(1)).unwrap_or_default() { 68 | NEEDS_UPDATE = false; 69 | let _: u32 = delphi_call!(0x713994); 70 | let _: u32 = delphi_call!(0x712a44); 71 | let _: u32 = delphi_call!(0x713a14); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/regular/project_watcher.rs: -------------------------------------------------------------------------------- 1 | use crate::{SAVE_END, UStr, ide, show_message}; 2 | use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, recommended_watcher}; 3 | use once_cell::unsync::Lazy; 4 | use parking_lot::Mutex; 5 | use std::{ 6 | arch::naked_asm, 7 | path::PathBuf, 8 | sync::{ 9 | Once, 10 | mpsc::{Receiver, Sender, channel}, 11 | }, 12 | }; 13 | 14 | static WATCHER_CHANNEL: Mutex>, Receiver>)>> = 15 | Mutex::new(Lazy::new(|| channel())); 16 | static mut WATCHER: Option = None; 17 | 18 | static WATCHER_ERROR: Once = Once::new(); 19 | 20 | unsafe extern "fastcall" fn show_message_and_reload() { 21 | const SCREEN: *const *const *mut *const usize = 0x7882f0 as _; 22 | // allow user to finish setting preferences 23 | { 24 | let screen = *SCREEN; 25 | let current_modal = screen.add(0x74 / 4).read(); 26 | if !current_modal.is_null() { 27 | let vmt = current_modal.read() as usize; 28 | // is this TPreferencesForm? 29 | if vmt == 0x7153c4 { 30 | // call real TApplication.Idle and return 31 | let _: u32 = delphi_call!(0x520418, *(0x7882ec as *const usize)); 32 | return 33 | } 34 | } 35 | } 36 | 37 | // reset TApplication.Idle 38 | crate::patch_call(0x51f74b, 0x520418); 39 | let message = UStr::new(format!( 40 | "Project files have been modified outside Game Maker. Reload project? \ 41 | Unsaved changes will be lost.\r\n\ 42 | If you click \"No\", saving will overwrite any foreign changes.", 43 | )); 44 | let answer = crate::show_question(&message); 45 | if answer == 6 { 46 | // yes -> reload project 47 | // but first, close all modals 48 | // this is done by patching the TApplication.Idle call in TApplication.HandleMessage 49 | // so that instead of idling after each form is closed, it closes the next form 50 | static mut OLD_ONCLOSE: usize = 0; 51 | static mut OLD_ONCLOSE_SENDER: usize = 0; 52 | #[unsafe(naked)] 53 | unsafe extern "C" fn put_old_onclose_back() { 54 | naked_asm!( 55 | "mov edx, {}", 56 | "mov [eax + 0x2f0], edx", 57 | "mov edx, {}", 58 | "mov [eax + 0x2f4], edx", 59 | "ret", 60 | sym OLD_ONCLOSE, 61 | sym OLD_ONCLOSE_SENDER, 62 | ); 63 | } 64 | unsafe extern "fastcall" fn instead_of_idle() { 65 | let screen = *SCREEN; 66 | // get TScreen.FSaveFocusedList 67 | let list = screen.add(0x78 / 4).read(); 68 | // check if that list's Count is 0 (i.e. no modals open) 69 | if !list.add(2).read().is_null() { 70 | // there are more modals 71 | // get 72 | let current_modal = screen.add(0x74 / 4).read(); 73 | // back up current modal's OnClose and replace with our own 74 | let onclose_ptr = current_modal.add(0x2f0 / 4); 75 | OLD_ONCLOSE = *onclose_ptr as _; 76 | OLD_ONCLOSE_SENDER = *onclose_ptr.add(1) as _; 77 | *onclose_ptr = put_old_onclose_back as _; 78 | *onclose_ptr.add(1) = current_modal as _; 79 | // let the modal know it should close 80 | current_modal.add(0x2b8 / 4).write(2 as _); 81 | } else { 82 | // put original TApplication.Idle back 83 | crate::patch_call(0x51f74b, 0x520418); 84 | // reload 85 | let _: u32 = delphi_call!(0x7059d8, (*ide::PROJECT_PATH).0); 86 | } 87 | } 88 | // patch TApplication.Idle call in TApplication.HandleMessage to close modals instead 89 | crate::patch_call(0x51f74b, instead_of_idle as _); 90 | } else { 91 | // no -> mark project as modified 92 | ide::SETTINGS_UPDATED.write(true); 93 | } 94 | } 95 | 96 | pub fn on_notify() { 97 | let lock = WATCHER_CHANNEL.lock(); 98 | // note: no need to check needs_rescan because no windows watcher uses it 99 | while let Some(_event) = lock.1.try_recv().ok().map(|e| e.unwrap()).filter(|event| match event.kind { 100 | EventKind::Create(_) => false, 101 | EventKind::Modify(_) => { 102 | event.paths.iter().any(|p| { 103 | if let Ok(modified) = p.metadata().and_then(|m| m.modified()) { 104 | unsafe { 105 | // it's foreign if modified time is after save end 106 | // if someone edited a file in notepad, we'll get one of these 107 | // if someone dragged in an older copy, we'll get a NoticeRemove instead 108 | SAVE_END < modified 109 | } 110 | } else { 111 | true 112 | } 113 | }) 114 | }, 115 | _ => true, 116 | }) { 117 | drop(lock); 118 | unwatch(); 119 | unsafe { 120 | // patch TApplication.Idle so it only pops the question after any dialogs are done 121 | // (note: not modals, i actually have some level of control over those) 122 | crate::patch_call(0x51f74b, show_message_and_reload as _); 123 | } 124 | break 125 | } 126 | } 127 | 128 | pub fn watching() -> bool { 129 | unsafe { WATCHER.is_some() } 130 | } 131 | 132 | pub fn setup_watcher(path: &mut PathBuf) { 133 | unwatch(); 134 | let lock = WATCHER_CHANNEL.lock(); 135 | let tx = lock.0.clone(); 136 | drop(lock); 137 | match recommended_watcher(tx) { 138 | Ok(mut watcher) => { 139 | let mut watch_all = || -> notify::Result<()> { 140 | path.push("backgrounds"); 141 | if path.exists() { 142 | watcher.watch(&path, RecursiveMode::Recursive)?; 143 | } 144 | path.pop(); 145 | path.push("datafiles"); 146 | if path.exists() { 147 | watcher.watch(&path, RecursiveMode::Recursive)?; 148 | } 149 | path.pop(); 150 | path.push("fonts"); 151 | if path.exists() { 152 | watcher.watch(&path, RecursiveMode::Recursive)?; 153 | } 154 | path.pop(); 155 | path.push("objects"); 156 | if path.exists() { 157 | watcher.watch(&path, RecursiveMode::Recursive)?; 158 | } 159 | path.pop(); 160 | path.push("paths"); 161 | if path.exists() { 162 | watcher.watch(&path, RecursiveMode::Recursive)?; 163 | } 164 | path.pop(); 165 | path.push("rooms"); 166 | watcher.watch(&path, RecursiveMode::Recursive)?; 167 | path.pop(); 168 | path.push("scripts"); 169 | if path.exists() { 170 | watcher.watch(&path, RecursiveMode::Recursive)?; 171 | } 172 | path.pop(); 173 | path.push("settings"); 174 | watcher.watch(&path, RecursiveMode::Recursive)?; 175 | path.pop(); 176 | path.push("sounds"); 177 | if path.exists() { 178 | watcher.watch(&path, RecursiveMode::Recursive)?; 179 | } 180 | path.pop(); 181 | path.push("sprites"); 182 | if path.exists() { 183 | watcher.watch(&path, RecursiveMode::Recursive)?; 184 | } 185 | path.pop(); 186 | path.push("timelines"); 187 | if path.exists() { 188 | watcher.watch(&path, RecursiveMode::Recursive)?; 189 | } 190 | path.pop(); 191 | path.push("triggers"); 192 | if path.exists() { 193 | watcher.watch(&path, RecursiveMode::Recursive)?; 194 | } 195 | path.pop(); 196 | watcher.watch(unsafe { &*ide::PROJECT_PATH }.to_os_string().as_ref(), RecursiveMode::Recursive)?; 197 | Ok(()) 198 | }; 199 | if let Err(e) = watch_all() { 200 | show_message(format!("Couldn't initialize directory watch: {e}")); 201 | } else { 202 | // Great Success! 203 | unsafe { 204 | WATCHER = Some(watcher); 205 | } 206 | } 207 | }, 208 | Err(e) => WATCHER_ERROR.call_once(|| show_message(format!("Couldn't create directory watcher: {e}"))), 209 | } 210 | } 211 | 212 | pub extern "C" fn unwatch() { 213 | WATCHER_CHANNEL.lock().1.try_iter().for_each(|_| ()); // clear channel 214 | unsafe { 215 | WATCHER = None; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/save_exe.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | AssetListTrait, DEFLATE_LEVEL, EXTRA_DATA, InstanceExtra, TileExtra, UStr, asset, 3 | delphi::{self, TMemoryStream}, 4 | ide, 5 | regular::extension_watcher::update_extensions, 6 | save::GetAsset, 7 | }; 8 | use byteorder::{LE, WriteBytesExt}; 9 | use flate2::{Compression, write::ZlibEncoder}; 10 | use rayon::prelude::*; 11 | use std::{ 12 | arch::{asm, naked_asm}, 13 | io, 14 | io::Write, 15 | ptr, slice, 16 | }; 17 | 18 | pub trait GetAssetList: Sync + 'static { 19 | fn get_asset_list() -> &'static dyn AssetListTrait; 20 | fn save(&mut self, exe: bool, out: impl Write) -> io::Result<()>; 21 | fn write_additional(_stream: &mut TMemoryStream) -> io::Result<()> { 22 | Ok(()) 23 | } 24 | } 25 | 26 | impl GetAssetList for asset::Sprite { 27 | fn get_asset_list() -> &'static dyn AssetListTrait { 28 | &ide::SPRITES 29 | } 30 | 31 | fn save(&mut self, exe: bool, mut out: impl Write) -> io::Result<()> { 32 | out.write_u32::(800)?; 33 | out.write_i32::(self.origin_x)?; 34 | out.write_i32::(self.origin_y)?; 35 | out.write_u32::(self.frame_count as u32)?; 36 | for frame in self.get_frames() { 37 | if !exe { 38 | save_frame(frame, &mut out)?; 39 | } else { 40 | let mut f = frame.duplicate(); 41 | f.prepare_for_export(); 42 | save_frame(&f, &mut out)?; 43 | } 44 | } 45 | if !exe { 46 | out.write_u32::(self.collision_shape)?; 47 | out.write_u32::(self.alpha_tolerance)?; 48 | } 49 | out.write_u32::(self.per_frame_colliders.into())?; 50 | if !exe { 51 | out.write_u32::(self.bbox_type)?; 52 | out.write_i32::(self.bbox_left)?; 53 | out.write_i32::(self.bbox_right)?; 54 | out.write_i32::(self.bbox_bottom)?; 55 | out.write_i32::(self.bbox_top)?; 56 | } else if self.frame_count > 0 { 57 | unsafe { 58 | #[repr(C)] 59 | struct Mask { 60 | _padding: u32, 61 | i_w: u32, 62 | i_h: u32, 63 | i_mask: *const *const u8, 64 | i_bbox: [u32; 4], 65 | } 66 | unsafe fn write_mask(mask: &Mask, mut out: impl Write) -> io::Result<()> { 67 | out.write_u32::(800)?; 68 | out.write_u32::(mask.i_w)?; 69 | out.write_u32::(mask.i_h)?; 70 | out.write_u32::(mask.i_bbox[0])?; 71 | out.write_u32::(mask.i_bbox[2])?; 72 | out.write_u32::(mask.i_bbox[3])?; 73 | out.write_u32::(mask.i_bbox[1])?; 74 | for y in 0..mask.i_h as usize { 75 | for x in 0..mask.i_w as usize { 76 | out.write_u32::(mask.i_mask.add(x).read().add(y).read().into())?; 77 | } 78 | } 79 | Ok(()) 80 | } 81 | // TODO speed up 82 | if !self.per_frame_colliders { 83 | // create mask 84 | let f: &asset::Frame = &self.get_frames()[0]; 85 | let mut mask: *const Mask; 86 | asm!( 87 | "push dword ptr [{sprite}+0x1c]", 88 | "push {bbox}", 89 | "push dword ptr [{sprite}+0x10]", 90 | "push dword ptr [{sprite}+0x14]", 91 | "call {call}", 92 | sprite = in(reg) &*self, 93 | call = in(reg) 0x5aecac, 94 | bbox = in(reg) &self.bbox_left, 95 | inlateout("eax") 0x5ae848 => mask, 96 | in("edx") 1, 97 | in("ecx") f, 98 | clobber_abi("C"), 99 | ); 100 | // merge mask 101 | for f in &self.get_frames()[1..] { 102 | let f: &asset::Frame = f; 103 | asm!( 104 | "push {frame}", 105 | "push dword ptr [{sprite}+0x10]", 106 | "push dword ptr [{sprite}+0x14]", 107 | "call {call}", 108 | call = in(reg) 0x5af188, 109 | sprite = in(reg) &*self, 110 | frame = in(reg) &self.bbox_left, 111 | in("eax") mask, 112 | in("edx") f, 113 | in("ecx") self.bbox_type, 114 | clobber_abi("C"), 115 | ); 116 | } 117 | write_mask(&*mask, &mut out)?; 118 | // free mask 119 | let _: u32 = delphi_call!(0x405a7c, mask); 120 | } else { 121 | // TODO 122 | for f in self.get_frames() { 123 | let f: &asset::Frame = f; 124 | // create mask 125 | let mut mask: *const Mask; 126 | asm!( 127 | "push dword ptr [{sprite}+0x1c]", 128 | "lea eax, [{sprite}+0x20]", 129 | "push eax", 130 | "push dword ptr [{sprite}+0x10]", 131 | "push dword ptr [{sprite}+0x14]", 132 | "mov edx, 1", 133 | "mov eax, 0x5ae848", 134 | "call {call}", 135 | sprite = in(reg) &*self, 136 | call = in(reg) 0x5aecac, 137 | inlateout("eax") 0x5ae848 => mask, 138 | in("edx") 1, 139 | in("ecx") f, 140 | clobber_abi("C"), 141 | ); 142 | write_mask(&*mask, &mut out)?; 143 | // free mask 144 | let _: u32 = delphi_call!(0x405a7c, mask); 145 | } 146 | } 147 | } 148 | } 149 | Ok(()) 150 | } 151 | } 152 | 153 | impl GetAssetList for asset::Background { 154 | fn get_asset_list() -> &'static dyn AssetListTrait { 155 | &ide::BACKGROUNDS 156 | } 157 | 158 | fn save(&mut self, exe: bool, mut out: impl Write) -> io::Result<()> { 159 | out.write_u32::(710)?; 160 | if !exe { 161 | out.write_u32::(self.is_tileset.into())?; 162 | out.write_u32::(self.tile_width)?; 163 | out.write_u32::(self.tile_height)?; 164 | out.write_u32::(self.h_offset)?; 165 | out.write_u32::(self.v_offset)?; 166 | out.write_u32::(self.h_sep)?; 167 | out.write_u32::(self.v_sep)?; 168 | save_frame(&self.frame, out)?; 169 | } else { 170 | let mut f = self.frame.duplicate(); 171 | f.prepare_for_export(); 172 | save_frame(&f, out)?; 173 | } 174 | Ok(()) 175 | } 176 | } 177 | 178 | impl GetAssetList for asset::Path { 179 | fn get_asset_list() -> &'static dyn AssetListTrait { 180 | &ide::PATHS 181 | } 182 | 183 | fn save(&mut self, exe: bool, mut out: impl Write) -> io::Result<()> { 184 | out.write_u32::(530)?; 185 | out.write_u32::(self.connection)?; 186 | out.write_u32::(self.closed.into())?; 187 | out.write_u32::(self.precision)?; 188 | if !exe { 189 | out.write_u32::(self.path_editor_room_background as _)?; 190 | out.write_u32::(self.snap_x)?; 191 | out.write_u32::(self.snap_y)?; 192 | } 193 | out.write_u32::(self.point_count as _)?; 194 | for p in self.get_points() { 195 | out.write_f64::(p.x)?; 196 | out.write_f64::(p.y)?; 197 | out.write_f64::(p.speed)?; 198 | } 199 | Ok(()) 200 | } 201 | } 202 | 203 | impl GetAssetList for asset::Script { 204 | fn get_asset_list() -> &'static dyn AssetListTrait { 205 | &ide::SCRIPTS 206 | } 207 | 208 | fn save(&mut self, _exe: bool, mut out: impl Write) -> io::Result<()> { 209 | out.write_u32::(800)?; 210 | write_string(&self.source, out)?; 211 | Ok(()) 212 | } 213 | } 214 | 215 | fn write_event(event: &mut asset::Event, mut out: impl Write) -> io::Result<()> { 216 | out.write_u32::(400)?; 217 | out.write_u32::(event.action_count)?; 218 | for action in event.get_actions_mut() { 219 | // define action from library 220 | unsafe { 221 | let lib_id = action.lib_id; 222 | let act_id = action.id; 223 | action.fill_in(lib_id, act_id); 224 | } 225 | out.write_u32::(440)?; 226 | out.write_u32::(action.lib_id)?; 227 | out.write_u32::(action.id)?; 228 | out.write_u32::(action.action_kind)?; 229 | out.write_u32::(action.can_be_relative.into())?; 230 | out.write_u32::(action.is_condition.into())?; 231 | out.write_u32::(action.applies_to_something.into())?; 232 | out.write_u32::(action.execution_type)?; 233 | write_string(&action.fn_name, &mut out)?; 234 | write_string(&action.fn_code, &mut out)?; 235 | out.write_u32::(action.param_count)?; 236 | out.write_u32::(8)?; 237 | for ty in action.param_types { 238 | out.write_u32::(ty)?; 239 | } 240 | out.write_i32::(action.applies_to)?; 241 | out.write_u32::(action.is_relative.into())?; 242 | out.write_u32::(8)?; 243 | for param in &action.param_strings { 244 | write_string(param, &mut out)?; 245 | } 246 | out.write_u32::(action.invert_condition.into())?; 247 | } 248 | Ok(()) 249 | } 250 | 251 | impl GetAssetList for asset::Object { 252 | fn get_asset_list() -> &'static dyn AssetListTrait { 253 | &ide::OBJECTS 254 | } 255 | 256 | fn save(&mut self, _exe: bool, mut out: impl Write) -> io::Result<()> { 257 | // make sure everything exists 258 | let _: u32 = unsafe { delphi_call!(0x704b30, self) }; 259 | out.write_u32::(430)?; 260 | out.write_i32::(self.sprite_index)?; 261 | out.write_u32::(self.solid.into())?; 262 | out.write_u32::(self.visible.into())?; 263 | out.write_i32::(self.depth)?; 264 | out.write_u32::(self.persistent.into())?; 265 | out.write_i32::(self.parent_index)?; 266 | out.write_i32::(self.mask_index.into())?; 267 | out.write_u32::(11)?; 268 | for events in &mut self.events { 269 | // note: gm saves them backwards, might as well replicate lol 270 | for (i, event) in events.iter_mut().enumerate().rev() { 271 | if event.action_count > 0 { 272 | out.write_u32::(i as _)?; 273 | write_event(event, &mut out)?; 274 | } 275 | } 276 | out.write_i32::(-1)?; 277 | } 278 | Ok(()) 279 | } 280 | } 281 | 282 | impl GetAssetList for asset::Timeline { 283 | fn get_asset_list() -> &'static dyn AssetListTrait { 284 | &ide::TIMELINES 285 | } 286 | 287 | fn save(&mut self, _exe: bool, mut out: impl Write) -> io::Result<()> { 288 | out.write_u32::(500)?; 289 | out.write_u32::(self.moment_count as _)?; 290 | for (&time, event) in self.moment_times.iter().zip(self.moment_events.iter_mut()) { 291 | out.write_u32::(time)?; 292 | write_event(event, &mut out)?; 293 | } 294 | Ok(()) 295 | } 296 | } 297 | 298 | impl GetAssetList for asset::Sound { 299 | fn get_asset_list() -> &'static dyn AssetListTrait { 300 | &ide::SOUNDS 301 | } 302 | 303 | fn save(&mut self, _exe: bool, mut out: impl Write) -> io::Result<()> { 304 | out.write_u32::(800)?; 305 | out.write_u32::(self.kind)?; 306 | write_string(&self.extension, &mut out)?; 307 | write_string(&self.source, &mut out)?; 308 | out.write_u32::(self.data.is_some().into())?; 309 | if let Some(data) = self.data.as_ref() { 310 | write_buffer(data.get_slice(), &mut out)?; 311 | } 312 | out.write_u32::(self.effects)?; 313 | out.write_f64::(self.volume)?; 314 | out.write_f64::(self.pan)?; 315 | out.write_u32::(self.preload.into())?; 316 | Ok(()) 317 | } 318 | } 319 | 320 | static mut OLD_DPI: u32 = 96; 321 | 322 | impl GetAssetList for asset::Font { 323 | fn get_asset_list() -> &'static dyn AssetListTrait { 324 | unsafe { 325 | OLD_DPI = *delphi::DPI; 326 | *delphi::DPI = 96; 327 | } 328 | &ide::FONTS 329 | } 330 | 331 | fn save(&mut self, exe: bool, mut out: impl Write) -> io::Result<()> { 332 | out.write_u32::(800)?; 333 | write_string(&self.sys_name, &mut out)?; 334 | out.write_u32::(self.size)?; 335 | out.write_u32::(self.bold.into())?; 336 | out.write_u32::(self.italic.into())?; 337 | let charset = if self.charset == 1 { 0 } else { self.charset }; 338 | out.write_u32::((self.range_start & 0xffff) | (charset << 16) | ((self.aa_level + 1) << 24))?; 339 | out.write_u32::(self.range_end)?; 340 | if exe { 341 | self.render(); // the only reason all these functions are &mut self 342 | for i in 0..256 { 343 | out.write_u32::(self.s_x[i])?; 344 | out.write_u32::(self.s_y[i])?; 345 | out.write_u32::(self.s_w[i])?; 346 | out.write_u32::(self.s_h[i])?; 347 | out.write_u32::(self.s_shift[i])?; 348 | out.write_u32::(self.s_offset[i])?; 349 | } 350 | out.write_u32::(self.s_bw)?; 351 | out.write_u32::(self.s_bh)?; 352 | unsafe { 353 | write_buffer(&self.s_bytes.get_unchecked(..(self.s_bw * self.s_bh) as usize), out)?; 354 | self.s_bytes.alloc_evil(0); 355 | } 356 | } 357 | Ok(()) 358 | } 359 | 360 | fn write_additional(_stream: &mut TMemoryStream) -> io::Result<()> { 361 | unsafe { 362 | *delphi::DPI = OLD_DPI; 363 | } 364 | Ok(()) 365 | } 366 | } 367 | 368 | impl GetAssetList for asset::Room { 369 | fn get_asset_list() -> &'static dyn AssetListTrait { 370 | &ide::ROOMS 371 | } 372 | 373 | fn save(&mut self, exe: bool, mut out: impl Write) -> io::Result<()> { 374 | unsafe { 375 | let _: u32 = delphi_call!(0x6576fc, self); // clean unused assets 376 | } 377 | let extra_data = unsafe { EXTRA_DATA.as_ref() }; 378 | let version: u32 = if exe && extra_data.is_some() { 811 } else { 541 }; 379 | out.write_u32::(version)?; 380 | write_string(&self.caption, &mut out)?; 381 | out.write_u32::(self.width)?; 382 | out.write_u32::(self.height)?; 383 | if !exe { 384 | out.write_u32::(self.snap_x)?; 385 | out.write_u32::(self.snap_y)?; 386 | out.write_u32::(self.isometric.into())?; 387 | } 388 | out.write_u32::(self.speed)?; 389 | out.write_u32::(self.persistent.into())?; 390 | out.write_u32::(self.bg_colour as _)?; 391 | out.write_u32::(u32::from(self.clear_screen) | (u32::from(self.clear_view) << 1))?; 392 | write_string(&self.creation_code, &mut out)?; 393 | out.write_u32::(8)?; 394 | for b in &self.backgrounds { 395 | out.write_u32::(b.visible_on_start.into())?; 396 | out.write_u32::(b.is_foreground.into())?; 397 | out.write_u32::(b.source_bg as _)?; 398 | out.write_u32::(b.xoffset as _)?; 399 | out.write_u32::(b.yoffset as _)?; 400 | out.write_u32::(b.tile_horz as _)?; 401 | out.write_u32::(b.tile_vert as _)?; 402 | out.write_u32::(b.hspeed as _)?; 403 | out.write_u32::(b.vspeed as _)?; 404 | out.write_u32::(b.stretch as _)?; 405 | } 406 | out.write_u32::(self.views_enabled.into())?; 407 | out.write_u32::(8)?; 408 | for v in &self.views { 409 | out.write_u32::(v.visible as _)?; 410 | out.write_u32::(v.source_x as _)?; 411 | out.write_u32::(v.source_y as _)?; 412 | out.write_u32::(v.source_w as _)?; 413 | out.write_u32::(v.source_h as _)?; 414 | out.write_u32::(v.port_x as _)?; 415 | out.write_u32::(v.port_y as _)?; 416 | out.write_u32::(v.port_w as _)?; 417 | out.write_u32::(v.port_h as _)?; 418 | out.write_u32::(v.following_hborder as _)?; 419 | out.write_u32::(v.following_vborder as _)?; 420 | out.write_u32::(v.following_hspeed as _)?; 421 | out.write_u32::(v.following_vspeed as _)?; 422 | out.write_u32::(v.following_target as _)?; 423 | } 424 | out.write_u32::(self.instance_count as _)?; 425 | for i in self.get_instances() { 426 | out.write_u32::(i.x as _)?; 427 | out.write_u32::(i.y as _)?; 428 | out.write_u32::(i.object as _)?; 429 | out.write_u32::(i.id as _)?; 430 | write_string(&i.creation_code, &mut out)?; 431 | if !exe { 432 | out.write_u32::(i.locked as _)?; 433 | } else if let Some(data) = extra_data.map(|(insts, _)| insts.get(&i.id).unwrap_or(&InstanceExtra::DEFAULT)) 434 | { 435 | out.write_f64::(data.xscale)?; 436 | out.write_f64::(data.yscale)?; 437 | out.write_u32::(data.blend as _)?; 438 | out.write_f64::(data.angle)?; 439 | } 440 | } 441 | out.write_u32::(self.tile_count as _)?; 442 | for t in self.get_tiles() { 443 | out.write_u32::(t.x as _)?; 444 | out.write_u32::(t.y as _)?; 445 | out.write_u32::(t.source_bg as _)?; 446 | out.write_u32::(t.u as _)?; 447 | out.write_u32::(t.v as _)?; 448 | out.write_u32::(t.width as _)?; 449 | out.write_u32::(t.height as _)?; 450 | out.write_u32::(t.depth as _)?; 451 | out.write_u32::(t.id as _)?; 452 | if !exe { 453 | out.write_u32::(t.locked as _)?; 454 | } else if let Some(data) = extra_data.map(|(_, tiles)| tiles.get(&t.id).unwrap_or(&TileExtra::DEFAULT)) { 455 | out.write_f64::(data.xscale)?; 456 | out.write_f64::(data.yscale)?; 457 | out.write_u32::(data.blend as _)?; 458 | } 459 | } 460 | if !exe { 461 | out.write_u32::(self.remember_room_editor_info as _)?; 462 | out.write_u32::(self.editor_width as _)?; 463 | out.write_u32::(self.editor_height as _)?; 464 | out.write_u32::(self.show_grid as _)?; 465 | out.write_u32::(self.show_objects as _)?; 466 | out.write_u32::(self.show_tiles as _)?; 467 | out.write_u32::(self.show_backgrounds as _)?; 468 | out.write_u32::(self.show_foregrounds as _)?; 469 | out.write_u32::(self.show_views as _)?; 470 | out.write_u32::(self.delete_underlying_objects as _)?; 471 | out.write_u32::(self.delete_underlying_tiles as _)?; 472 | out.write_u32::(self.tab as _)?; 473 | out.write_u32::(self.x_position_scroll as _)?; 474 | out.write_u32::(self.y_position_scroll as _)?; 475 | } 476 | Ok(()) 477 | } 478 | 479 | fn write_additional(stream: &mut TMemoryStream) -> io::Result<()> { 480 | unsafe { 481 | stream.write_u32::(*ide::LAST_INSTANCE_ID as _)?; 482 | stream.write_u32::(*ide::LAST_TILE_ID as _)?; 483 | } 484 | Ok(()) 485 | } 486 | } 487 | 488 | fn write_string(s_wide: &UStr, mut out: impl Write) -> io::Result<()> { 489 | unsafe { 490 | let mut s_utf8: *const usize = ptr::null(); 491 | let _: u32 = delphi_call!(0x40810c, &mut s_utf8, s_wide.0, 0xfde9); 492 | if s_utf8.is_null() { 493 | out.write_u32::(0)?; 494 | } else { 495 | let len = s_utf8.sub(1).read(); 496 | write_buffer(slice::from_raw_parts(s_utf8.cast::(), len), out)?; 497 | let _: u32 = delphi_call!(0x406e5c, &mut s_utf8); 498 | } 499 | } 500 | Ok(()) 501 | } 502 | 503 | fn write_buffer(buf: &[u8], mut out: impl Write) -> io::Result<()> { 504 | out.write_u32::(buf.len() as u32)?; 505 | out.write_all(buf) 506 | } 507 | 508 | fn save_frame(frame: &asset::Frame, mut out: impl Write) -> io::Result<()> { 509 | out.write_u32::(800)?; 510 | out.write_u32::(frame.width)?; 511 | out.write_u32::(frame.height)?; 512 | let data = frame.get_data(); 513 | if !data.is_empty() { 514 | write_buffer(data, &mut out)?; 515 | } 516 | Ok(()) 517 | } 518 | 519 | extern "fastcall" fn save_assets(mut stream: &mut TMemoryStream, exe: bool) -> bool { 520 | let asset_list = T::get_asset_list(); 521 | stream.write_u32::(800).unwrap(); 522 | let assets = asset_list.assets_mut(); 523 | stream.write_u32::(assets.len() as _).unwrap(); 524 | (assets, asset_list.names(), asset_list.timestamps()) 525 | .into_par_iter() 526 | .map(|(asset, name, timestamp)| { 527 | let mut out = ZlibEncoder::new(Vec::new(), Compression::new(unsafe { DEFLATE_LEVEL })); 528 | out.write_u32::(asset.is_some().into()).unwrap(); 529 | if let Some(asset) = asset.as_mut() { 530 | write_string(name, &mut out).unwrap(); 531 | if !exe { 532 | out.write_f64::(*timestamp).unwrap(); 533 | } 534 | asset.save(exe, &mut out).unwrap(); 535 | } 536 | out.finish().unwrap() 537 | }) 538 | .collect::>() 539 | .into_iter() 540 | .for_each(|buf| { 541 | write_buffer(&buf, &mut stream).unwrap(); 542 | }); 543 | T::write_additional(stream).unwrap(); 544 | true 545 | } 546 | 547 | #[unsafe(naked)] 548 | pub unsafe extern "C" fn save_assets_inj() { 549 | naked_asm!( 550 | "mov ecx, eax", 551 | "jmp {save_assets}", 552 | save_assets = sym save_assets::, 553 | ); 554 | } 555 | 556 | #[unsafe(naked)] 557 | pub unsafe extern "C" fn write_event_tables() { 558 | extern "fastcall" fn inj(stream: &mut TMemoryStream) -> bool { 559 | let resource_tree_res: u32 = unsafe { delphi_call!(0x71de7c, stream, 1) }; 560 | if resource_tree_res == 0 { 561 | return false 562 | } 563 | 564 | fn obj_has_event(obj: &asset::Object, event_type: usize, event_number: usize) -> bool { 565 | let mut obj_opt = Some(obj); 566 | while let Some(obj) = obj_opt { 567 | if obj.has_direct_event(event_type, event_number) { 568 | return true 569 | } 570 | obj_opt = ide::OBJECTS.assets().get_asset(obj.parent_index); 571 | } 572 | false 573 | } 574 | 575 | fn obj_has_collision_event(obj: &asset::Object, mut other_id: usize) -> bool { 576 | while let Some(other) = ide::OBJECTS.assets().get_asset(other_id as i32) { 577 | if obj_has_event(obj, 4, other_id) { 578 | return true 579 | } 580 | other_id = other.parent_index as usize; 581 | } 582 | false 583 | } 584 | 585 | #[repr(C)] 586 | struct CollisionPair { 587 | me: usize, 588 | other: usize, 589 | } 590 | // same table sizes as used by game maker 591 | let mut event_holders = [ 592 | vec![Vec::new(); 1], 593 | vec![Vec::new(); 1], 594 | vec![Vec::new(); 13], 595 | vec![Vec::new(); 17], 596 | Vec::new(), 597 | vec![Vec::new(); 129], 598 | vec![Vec::new(); 129], 599 | vec![Vec::new(); 129], 600 | vec![Vec::new(); 1], 601 | vec![Vec::new(); 129], 602 | vec![Vec::new(); 129], 603 | vec![Vec::new(); ide::get_triggers().len()], 604 | ]; 605 | let mut collision_holders = Vec::new(); 606 | for (obj_id, obj) in ide::OBJECTS.assets().iter().enumerate().filter_map(|(i, o)| o.as_deref().map(|o| (i, o))) 607 | { 608 | for (event_type, type_holder) in event_holders.iter_mut().enumerate() { 609 | if event_type != 4 { 610 | // not collision 611 | for (event_number, event_holder) in type_holder.iter_mut().enumerate() { 612 | if obj_has_event(obj, event_type, event_number) { 613 | event_holder.push(obj_id); 614 | } 615 | } 616 | } 617 | } 618 | // collision 619 | for (other_id, other_obj) in 620 | ide::OBJECTS.assets().iter().enumerate().skip(obj_id).filter_map(|(i, o)| o.as_deref().map(|o| (i, o))) 621 | { 622 | if obj_has_collision_event(obj, other_id) || obj_has_collision_event(other_obj, obj_id) { 623 | collision_holders.push(CollisionPair { me: obj_id, other: other_id }); 624 | } 625 | } 626 | } 627 | 628 | let mut encoder = 629 | flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(unsafe { DEFLATE_LEVEL })); 630 | if event_holders 631 | .iter() 632 | .try_for_each(|holder| { 633 | encoder.write_u32::(holder.len() as u32)?; 634 | holder.iter().try_for_each(|event| { 635 | encoder.write_u32::(event.len() as u32)?; 636 | event.iter().try_for_each(|&obj| encoder.write_u32::(obj as u32)) 637 | }) 638 | }) 639 | .is_err() 640 | { 641 | return false 642 | } 643 | 644 | if encoder.write_u32::(collision_holders.len() as u32).is_err() { 645 | return false 646 | } 647 | 648 | if collision_holders 649 | .iter() 650 | .try_for_each(|pair| { 651 | encoder.write_u32::(pair.me as u32)?; 652 | encoder.write_u32::(pair.other as u32) 653 | }) 654 | .is_err() 655 | { 656 | return false 657 | } 658 | 659 | let data = if let Ok(x) = encoder.finish() { x } else { return false }; 660 | 661 | if stream.write_u32::(data.len() as u32).is_err() { 662 | return false 663 | } 664 | stream.write_all(&data).is_ok() 665 | } 666 | naked_asm!( 667 | "mov ecx, eax", 668 | "jmp {}", 669 | sym inj, 670 | ); 671 | } 672 | 673 | #[unsafe(naked)] 674 | pub unsafe extern "C" fn write_encrypted_gamedata_inj() { 675 | naked_asm!( 676 | "mov ecx, eax", 677 | "jmp {}", 678 | sym write_encrypted_gamedata, 679 | ) 680 | } 681 | 682 | pub unsafe extern "fastcall" fn write_encrypted_gamedata(stream: &mut TMemoryStream) -> bool { 683 | // update extensions if needed 684 | update_extensions(); 685 | // write encryption headers 686 | // no garbage data 687 | stream.write_u32::(0).ok(); 688 | stream.write_u32::(0).ok(); 689 | // no swap table: it's just 0,1,2,etc 690 | for i in 0..=255 { 691 | stream.write_u8(i).ok(); 692 | } 693 | let length_pos = stream.get_pos() as usize; 694 | stream.write_u32::(0).ok(); 695 | // encrypted data start 696 | let data_pos = stream.get_pos() as usize; 697 | // no garbage data 698 | stream.write_u32::(0).ok(); 699 | stream.write_u32::(1).ok(); 700 | // generate gamedata 701 | let res: u32 = delphi_call!(0x6cd8ac, stream); 702 | if res == 0 { 703 | return false 704 | } 705 | // write a few null bytes so i don't have to figure out how to fix the decompiler 706 | for _ in 0..4 { 707 | stream.write_u32::(0).ok(); 708 | } 709 | let data = stream.get_slice_mut(); 710 | let data_len = data.len() - data_pos; 711 | data[length_pos..data_pos].copy_from_slice(&data_len.to_le_bytes()); 712 | let data = &mut data[data_pos..]; 713 | // first pass: swap bytes around 714 | for i in 0..data_len { 715 | data.swap(i, i & !0xff); 716 | } 717 | // second pass 718 | for i in 1..data_len { 719 | data[i] = data[i].wrapping_add(data[i - 1]).wrapping_add(i as u8); 720 | } 721 | true 722 | } 723 | -------------------------------------------------------------------------------- /src/stub.rs: -------------------------------------------------------------------------------- 1 | // This DLL is emulating winspool.drv, and these are stubs for the functions that GM8 imports. 2 | 3 | #[unsafe(no_mangle)] 4 | pub extern "system" fn ClosePrinter(_a: u32) {} 5 | #[unsafe(no_mangle)] 6 | pub extern "system" fn DocumentPropertiesW(_a: u32, _b: u32, _c: u32, _d: u32, _e: u32, _f: u32) {} 7 | #[unsafe(no_mangle)] 8 | pub extern "system" fn EnumPrintersW(_a: u32, _b: u32, _c: u32, _d: u32, _e: u32, _f: u32, _g: u32) {} 9 | #[unsafe(no_mangle)] 10 | pub extern "system" fn GetDefaultPrinterW(_a: u32, _b: u32) {} 11 | #[unsafe(no_mangle)] 12 | pub extern "system" fn OpenPrinterW(_a: u32, _b: u32, _c: u32) {} 13 | --------------------------------------------------------------------------------