diff --git a/.editorconfig b/.editorconfig index df845099..19b4051e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,6 @@ indent_size = 2 [*.nix] indent_style = space indent_size = 2 + +[*.py] +indent_style = space diff --git a/.gitignore b/.gitignore index 61b8fcd1..57fdf0dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ /target /result /.vscode +/changelogs *.o *.dmb *.int *.lk +/byond diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ad91a15a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,273 @@ +# Agent Notes + +This repository is primarily useful to agents as a source of BYOND runtime signatures, raw type layouts, and function prototypes for populating Ghidra projects. Most agent work here is expected to happen through the Ghidra MCP tools rather than through code edits. + +## Ghidra Programs + +The Ghidra project contains these programs: + +- `byondcore.dll` (this is 516.1681) +- `byondcore_515.1602.dll` +- `libbyond_516.1681.so` +- `libbyond_516.1667.so` +- `libbyond_515.1647.so` +- `libbyond_515.1602.so` + +`byondcore.dll` is the main reference database for naming conventions, data type names, comments, and general layout style. `byondcore_515.1602.dll` is also populated enough to use as a 515-era reference. `libbyond_515.1602.so` has been populated with the auxtools Linux runtime signatures that matched uniquely, but should still be treated as partial. The auxtools codebase is currently a better reference for data structures and function definitions themselves, as the ghidra programs are not fully verified. + +Always pass `program_name` explicitly to Ghidra MCP calls. Do not rely on the active program, because the project contains many similar BYOND binaries. It is expected that the current program reported by the mcp server can change arbitrarily, don't worry about it. + +If a requested program is not open in Ghidra, some MCP calls can silently operate on the active program even when `program_name` was supplied. Always start with `list_binaries` and confirm the target is listed before trusting search, stats, or rename results. + +## Version Conventions + +Use the BYOND build minor number when discussing signatures and ranges: + +- `1602` means BYOND `515.1602`. +- `1647` means BYOND `515.1647`. +- `1648` means BYOND `516.1648`. +- `1667` means BYOND `516.1667`. +- `1681` means BYOND `516.1681`. + +Important boundaries: + +- Last 515 build: `515.1647`. +- First 516 build: `516.1648`. +- In auxtools signature ranges, the major version is usually implicit and the minor build number drives the branch selection. + +Public BYOND changelogs are in: + +- `changelogs/515.md` +- `changelogs/516.md` + +Use those changelogs when a structure, signature, or behavior changes between versions. They can explain why a signature moved, why an execution context field changed, or why a function stopped matching. They do not include every change. + +## Platform Status + +Windows builds should be reasonably well supported by the signatures in this repo. Prefer starting with the Windows DLLs when porting names and types across versions. + +Linux support is much weaker: + +- `libbyond_515.1602.so` is the last Linux build expected to have useful support. +- Linux support started breaking after `515.1602`. +- Auxtools is not currently functional on Linux for the version immediately before the compiler break, but that version may still contain useful migration clues. +- The Linux compiler changed at some point, probably around `516.1667`. +- Assume every signature is invalid on the latest Linux builds until verified. + +For Linux work, do not bulk-apply Windows-derived names just because a function looks similar. Verify with byte patterns, call graph context, decompiler behavior, and version changelogs. + +Current `libbyond_515.1602.so` status: + +- Verified and named from unique auxtools Linux signatures: `BYOND_get_proc_array_entry`, `BYOND_get_string_id`, `BYOND_call_proc_by_id`, `BYOND_get_variable`, `BYOND_set_variable`, `BYOND_get_string_table_entry`, `BYOND_inc_ref_count`, `BYOND_dec_ref_count`, `BYOND_get_assoc_element`, `BYOND_set_assoc_element`, `BYOND_create_list`, `BYOND_append_to_list`, `BYOND_remove_from_list`, `BYOND_get_length`, `BYOND_get_misc_by_id`, and `BYOND_runtime`. +- Verified and named globals: `BYOND_suspended_procs`, `BYOND_suspended_procs_buffer`, and `BYOND_variable_names`. +- The auxtools signatures for `BYOND_call_datum_proc_by_name`, `BYOND_to_string`, and `BYOND_current_execution_context` did not match exactly during population. Leave them unresolved unless a future agent verifies them semantically. +- A shortened `to_string`-like pattern matched `FUN_00419980`, but decompilation showed key-file/fopen behavior, not BYOND value stringification. Do not use that candidate. + +## Source Files To Use + +NOTE: This is just a suggestion. + +Core signature and initialization data: + +- `auxtools/src/lib.rs` +- `auxtools/src/sigscan.rs` +- `instruction_hooking/src/lib.rs` +- `debug_server/src/ckey_override.rs` + +Raw BYOND type layouts: + +- `auxtools/src/raw_types/values.rs` +- `auxtools/src/raw_types/procs.rs` +- `auxtools/src/raw_types/misc.rs` +- `auxtools/src/raw_types/lists.rs` +- `auxtools/src/raw_types/strings.rs` +- `auxtools/src/raw_types/variables.rs` +- `auxtools/src/raw_types/funcs.rs` +- `auxtools/src/raw_types/funcs.cpp` + +Signature macro behavior: + +- `signature_struct!(call, "...")` means search for the pattern, then resolve the relative `CALL` target from the first byte of the match. +- `signature_struct!(N, "...")` means search for the pattern, then read a pointer-sized value at `match + N`. +- A plain signature resolves to the match address. +- `version_dependent_signature!` ranges are selected using the BYOND minor build number. + +## Ghidra Population Workflow + +NOTE: This is just a suggestion. + +For each unpopulated program: + +1. Start by checking `list_binaries`, `get_binary_info`, and `get_function_statistics`. +2. Confirm the image base, architecture, and program name. +3. Select the correct signatures from `auxtools/src/lib.rs` for that platform and minor version. +4. Use `search_bytes` and require unique matches before renaming functions. +5. Resolve `call` and integer-offset signatures exactly as implemented in `auxtools/src/sigscan.rs`. +6. Use `byondcore_516.1681.dll` as the naming and typing reference, but adjust layouts for the target version. +7. Apply structures before prototypes so decompiler output can recover field references. +8. Apply function names with the `BYOND_` prefix for runtime functions resolved from auxtools signatures. +9. Add comments for hook sites and non-obvious signature-derived globals. +10. Re-decompile representative callers to verify the result. + +Ghidra MCP quirks observed while populating `libbyond_515.1602.so`: + +- `batch_rename` did not find functions by bare address or `0x` address, but did find them by current symbol name such as `FUN_00364d30`. +- `search_bytes` reports match addresses, not resolved signature targets. For `signature_struct!(call, ...)`, read the call bytes and compute `target = call_address + 5 + rel32` before renaming. +- The struct C parser often treats forward declarations or referenced structs as extra structure definitions. If it reports multiple structures, create the referenced type first or inline fields with primitive types for the live target layout. +- Types created under `/auxtools` may not be found by `create_data_var` or `types set` using the bare type name. Creating simple root-category aliases can be faster when applying data variables through MCP. +- Do not include a trailing semicolon when using `variables set_prototype`; the parser rejects it as trailing text. + +Prefer exact names already present in the populated reference program: + +- `BYOND_call_proc_by_id` +- `BYOND_call_datum_proc_by_name` +- `BYOND_get_proc_array_entry` +- `BYOND_get_string_id` +- `BYOND_get_variable` +- `BYOND_set_variable` +- `BYOND_get_string_table_entry` +- `BYOND_inc_ref_count` +- `BYOND_dec_ref_count` +- `BYOND_get_assoc_element` +- `BYOND_set_assoc_element` +- `BYOND_create_list` +- `BYOND_append_to_list` +- `BYOND_remove_from_list` +- `BYOND_get_length` +- `BYOND_get_misc_by_id` +- `BYOND_to_string` +- `BYOND_runtime` +- `BYOND_execute_instruction` + +Known global labels from the populated `1681` Windows database: + +- `BYOND_current_execution_context` +- `BYOND_suspended_procs` +- `BYOND_suspended_procs_buffer` +- `BYOND_variable_names` +- `BYOND_guest_ckey_format_string` + +For auxtools-populated BYOND functions, prototype the native BYOND function, not the Rust/C++ wrapper. Use `auxtools/src/raw_types/funcs.cpp` for the native function pointer definitions and `funcs.rs` for wrapper-facing types. + +If a byte pattern is non-unique but one candidate is strongly implied, verify with decompiler semantics before naming it. For example `get_length` should switch on `ByondValue` tag and return string/list/content lengths. + +## Struct And Type Conventions + +Use `Byond`-prefixed type names in Ghidra. Keep the reference database's style: + +- `ByondValue` +- `ByondValueTag` +- `ByondProcEntry` +- `ByondProcInstance` +- `ByondExecutionContext` +- `ByondStringEntry` +- `ByondList` +- `ByondAssociativeListEntry` +- `ByondSuspendedProcs` +- `ByondSuspendedProcsBuffer` +- `ByondVariableNameIdTable` + +For versioned layouts, encode the boundary in the name when both variants are useful, for example: + +- `ByondExecutionContextPre1668` +- `ByondBytecodePre1630` +- `ByondBytecodePost1630` +- `ByondProcInstancePre516Tail` +- `ByondProcInstancePost516Tail` + +Apply only the target version's real layout to live variables and function prototypes. Keep older layouts available as reference types. + +Known layout boundaries from the Rust sources: + +- `ProcEntry.metadata` uses the post-1630 bytecode layout for `1630+`. +- `ProcInstance` uses the post-516 argument tail for BYOND 516 builds. +- `ExecutionContext` uses the post-1668 layout for `516.1668+`. +- `ExecutionContext` uses the pre-1668 layout for 515 builds and `516.1667`. + +## Signature Repair Guidelines + +When a signature breaks between versions: + +- Check whether the version range in `auxtools/src/lib.rs` is too broad. +- Compare the populated `1681` Windows target with the closest older populated Windows target. +- Check BYOND changelogs for relevant runtime, list, proc, compiler, exception, or debugger changes. +- For Windows, inspect nearby calls and string references before changing a pattern. +- For Linux, assume compiler output changed enough that Windows pattern logic may not transfer. +- Use shorter patterns only when they still match exactly once. +- Prefer stable semantic anchors: calls to already identified helpers, accesses to known globals, distinctive runtime strings, jump-table setup, and structure field offsets. +- Record repaired signatures in code comments or Ghidra comments when the reason is not obvious. + +Do not rename or prototype a function from a non-unique byte match. If a match is ambiguous, add a comment explaining the candidates and leave the symbol unresolved until there is stronger evidence. + +## Repository Work + +Use `rg` for searching. Keep code edits scoped to the requested task. The repo contains Rust crates plus C++/assembly hook shims; avoid broad formatting churn. + +Reasonable verification commands when editing code: + +- `cargo check` +- `cargo test` +- Target-specific checks for 32-bit builds only when the required toolchain and system libraries are installed. + +Network access and dependency downloads may not be available in agent environments, so report when verification could not be run. + +## BYOND builds +Builds of BYOND can be found under the `./byond` directory. For example: ./byond/linux/515.1602_byond_linux/... or ./byond/windows/515.1602_byond/... + +## Auxtest Runner + +Use `tools/run-auxtest.sh` to run the auxtest integration world against local BYOND builds. The script is WSL-oriented and supports these targets: + +- `tools/run-auxtest.sh all` +- `tools/run-auxtest.sh linux-515` +- `tools/run-auxtest.sh linux-516` +- `tools/run-auxtest.sh windows-515` +- `tools/run-auxtest.sh windows-516` +- `tools/run-auxtest.sh linux-515.1647` +- `tools/run-auxtest.sh windows-515.1647` +- `tools/run-auxtest.sh linux-1647` +- `tools/run-auxtest.sh windows-1647` +- `tools/run-auxtest.sh 1647` + +The current target mappings are: + +- `linux-515`: `./byond/linux/515.1602_byond_linux/byond` +- `linux-516`: `./byond/linux/516.1681_byond_linux/byond` +- `windows-515`: `./byond/windows/515.1602_byond/byond` +- `windows-516`: `./byond/windows/516.1681_byond/byond` +- `linux-` and `windows-` map to `./byond//_byond.../byond` +- Bare minor builds such as `1602`, `1647`, and `1681` are mapped to their BYOND major version and run on both Windows and Linux. + +The runner builds and runs `tests/test_runner`, which builds `tests/auxtest`, compiles `tests/auxtest_host/auxtest_host.dme` with the selected DreamMaker, then launches DreamDaemon with `AUXTEST_DLL` pointing at the built auxtest library. It deletes the old `.dmb` before compiling so the selected BYOND version always produces the world being run. + +Linux requirements: + +- Rust target: `i686-unknown-linux-gnu` +- Usual 32-bit build/runtime packages from the README, including multilib C/C++ support. +- Newer Linux BYOND builds may also need runtime libraries such as `libcurl4:i386`. + +Windows-from-WSL requirements: + +- Windows Rust target: `i686-pc-windows-msvc` +- Visual Studio C++ x86 tools discoverable via `vswhere.exe`. +- The script uses PowerShell, initializes the VS developer shell, copies the source tree to `%TEMP%\auxtools-wsl-`, and runs Windows Cargo from there. This avoids Rust/Cargo and BYOND issues with WSL UNC paths. +- The script intentionally excludes `.git`, `target`, and the `byond` symlink from the temp copy. + +Auxtest output and diagnostics: + +- `auxtest_out()` writes markers like `SUCCESS: Finished` and `FAILED: ...` to stderr and, when `AUXTEST_OUT` is set, to `auxtest_output.log` beside the `.dmb`. +- DreamDaemon is launched with `-log auxtest_host.txt`; the runner includes that log in failure output. This is important for failures before auxtest can emit markers, such as `auxtools_init` returning `FAILED (Couldn't find )`. +- A missing `SUCCESS:` marker should be treated as a real test failure. Inspect the included `world log:` first; it often contains the root cause. + +When asked to run auxtest against a BYOND version and fix issues: + +- Start with the closest runner target above, using exact-version syntax when the requested minor build matters. For example, use `linux-515.1647` or `linux-1647` to test Linux BYOND `515.1647` independently from `515.1602`. +- If DreamMaker or DreamDaemon fails before loading the world, fix environment/runtime prerequisites first. Examples include missing 32-bit Linux shared libraries or Windows path handling. +- If the world log says `init_result = FAILED (Couldn't find )`, this is a signature issue. Repair only the missing signature first, keep the version range as narrow as justified, and rerun the same target before moving on. +- For signature repair, require unique byte matches. If using a semantic cross-reference, document the reasoning in the conversation and prefer version-bounded signatures. Do not broaden a signature just because it works on one build. +- After fixing a Windows 515-era signature, rerun `windows-515` and `windows-516` to check both sides of the 515/516 boundary. After fixing a Linux 515-era signature, rerun `linux-515`; only run `linux-516` if the task explicitly includes latest Linux or if the signature range could affect it. +- Latest Linux support is known to be weak. If `linux-516` fails on missing signatures, do not bulk-apply Windows-derived names or patterns; verify with Ghidra semantics and version-bound every fix. + +# Ghidra Tips + +When searching for new symbols in a BYOND program, prioritising using x-refs within the program of a better-supported BYOND version as part of the search. For example, if version 1602 and 1647 both have a known "A" function, and we know that "A" calls "B", but "B" is only known in 1602, we can use the fact that "A" x-refs "B" to find "B" in 1647. diff --git a/Cargo.lock b/Cargo.lock index d12f0c88..dd78512a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,7 +138,7 @@ dependencies = [ "attribute-derive-macro", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -154,7 +154,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -199,12 +199,14 @@ dependencies = [ "ahash", "auxtools-impl", "cc", + "colog", "ctor", "dashmap", "fxhash", "inventory", "lazy_static", "libc", + "log", "once_cell", "retour", "winapi", @@ -217,7 +219,7 @@ dependencies = [ "hex", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -485,7 +487,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -509,6 +511,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +[[package]] +name = "colog" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df62599ba6adc9c6c04a54278c8209125343dc4775f57b9d76c9a4287e58f2bd" +dependencies = [ + "colored", + "env_logger", + "log", +] + [[package]] name = "color_space" version = "0.5.4" @@ -521,6 +534,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -636,7 +658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -664,6 +686,7 @@ dependencies = [ "instruction_hooking", "lazy_static", "libc", + "log", "region", "retour", "serde", @@ -713,7 +736,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -724,7 +747,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -752,7 +775,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -803,6 +826,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -955,7 +1001,7 @@ checksum = "13a1bcfb855c1f340d5913ab542e36f25a1c56f57de79022928297632435dec2" dependencies = [ "attribute-derive", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -1383,7 +1429,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -1513,6 +1559,30 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.32" @@ -1801,7 +1871,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -1917,7 +1987,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -1971,7 +2041,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -2001,6 +2071,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2053,9 +2138,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2071,9 +2156,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2086,7 +2171,7 @@ checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" dependencies = [ "quote", "quote-use-macros", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -2098,7 +2183,7 @@ dependencies = [ "derive-where", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -2456,7 +2541,7 @@ checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -2642,9 +2727,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.91" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2668,7 +2753,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -2811,7 +2896,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -3121,7 +3206,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3156,7 +3241,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3361,7 +3446,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", "synstructure", ] @@ -3383,7 +3468,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -3403,7 +3488,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", "synstructure", ] @@ -3424,7 +3509,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] @@ -3446,7 +3531,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.117", ] [[package]] diff --git a/auxtools/Cargo.toml b/auxtools/Cargo.toml index b9593533..7d9fd93d 100644 --- a/auxtools/Cargo.toml +++ b/auxtools/Cargo.toml @@ -24,6 +24,8 @@ ahash = "0.8" fxhash = "0.2" ctor = "0.2" retour = { workspace = true } +log = { version = "0.4", features = [ "release_max_level_off" ] } +colog = "1.4" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "psapi", "processthreadsapi"] } diff --git a/auxtools/build.rs b/auxtools/build.rs index 80947482..50e87e83 100644 --- a/auxtools/build.rs +++ b/auxtools/build.rs @@ -1,4 +1,8 @@ fn main() { + println!("cargo:rerun-if-changed=src/hooks.cpp"); + println!("cargo:rerun-if-changed=src/hooks.h"); + println!("cargo:rerun-if-changed=src/raw_types/funcs.cpp"); + cc::Build::new() .include("src/") .file("src/hooks.cpp") diff --git a/auxtools/src/hooks.cpp b/auxtools/src/hooks.cpp index 652e86b1..b3102cb5 100644 --- a/auxtools/src/hooks.cpp +++ b/auxtools/src/hooks.cpp @@ -4,6 +4,23 @@ // The type of the func defined in Byond using Runtime_Ptr = void(*)(char *pError); using CallProcById_Ptr = Value(LINUX_REGPARM3 *)(Value, uint32_t, uint32_t, uint32_t, Value, Value*, uint32_t, uint32_t, uint32_t); +using CallProcById_1647_Ptr = Value(*)(Value, uint32_t, uint32_t, uint32_t, Value, Value*, uint32_t, void*, uint32_t); + +struct ProcInstance_1647 { + uint32_t proc_id; + uint32_t flags; + Value usr; + Value src; + void* context; + uint32_t argslist_idx; + uint32_t unk_1; + uint32_t unk_2; + uint32_t unk_3; + uint32_t args_count; + Value* args; +}; + +using ExecuteProcInstance_1647_Ptr = void(LINUX_REGPARM2 *)(Value*, ProcInstance_1647*); // The type of the hook defined in hooks.rs using CallProcById_Hook_Ptr = Value(*)(Value, uint32_t, uint32_t, uint32_t, Value, Value*, uint32_t, uint32_t, uint32_t); @@ -14,7 +31,7 @@ extern "C" { // The original function - set by rust after hooking Runtime_Ptr runtime_original = nullptr; - CallProcById_Ptr call_proc_by_id_original = nullptr; + void *call_proc_by_id_original = nullptr; } // If the top of this stack is true, we replace byond's runtime exceptions with our own @@ -68,7 +85,25 @@ extern "C" Value LINUX_REGPARM3 call_proc_by_id_hook_trampoline( clean(ret); return ret; } else { - return call_proc_by_id_original(usr, proc_type, proc_id, unk_0, src, args, args_count, unk_1, unk_2); + return reinterpret_cast(call_proc_by_id_original)(usr, proc_type, proc_id, unk_0, src, args, args_count, unk_1, unk_2); } //return call_proc_by_id_hook(usr, proc_type, proc_id, unk_0, src, args, args_count, unk_1, unk_2); } + +extern "C" void LINUX_REGPARM2 call_proc_by_id_hook_trampoline_1647( + Value* out, + ProcInstance_1647* proc_instance +) { + uint32_t flags = proc_instance->flags; + uint32_t proc_type = flags & 0xFF; + uint32_t unk_0 = (flags >> 8) & 0xFF; + + Value ret; + + if (call_proc_by_id_hook(&ret, proc_instance->usr, proc_type, proc_instance->proc_id, unk_0, proc_instance->src, proc_instance->args, proc_instance->args_count, 0, 0)) { + clean(ret); + *out = ret; + } else { + reinterpret_cast(call_proc_by_id_original)(out, proc_instance); + } +} diff --git a/auxtools/src/hooks.rs b/auxtools/src/hooks.rs index b27e8279..38fac1ae 100644 --- a/auxtools/src/hooks.rs +++ b/auxtools/src/hooks.rs @@ -1,3 +1,5 @@ +use log::{info, warn}; + use std::{ cell::RefCell, ffi::{c_void, CStr}, @@ -39,6 +41,11 @@ extern "C" { unk_1: u32, unk_2: u32 ) -> raw_types::values::Value; + + fn call_proc_by_id_hook_trampoline_1647( + ret: *mut raw_types::values::Value, + proc_instance: *mut raw_types::procs::ProcInstance + ); } struct Detours { @@ -82,11 +89,13 @@ pub fn init() -> Result<(), String> { runtime_hook.enable().unwrap(); runtime_original = runtime_hook.trampoline() as *const () as *const c_void; - let call_hook = RawDetour::new( - raw_types::funcs::call_proc_by_id_byond as *const (), + let call_proc_trampoline = if cfg!(unix) && crate::version::get().build >= 1647 { + call_proc_by_id_hook_trampoline_1647 as *const () + } else { call_proc_by_id_hook_trampoline as *const () - ) - .unwrap(); + }; + + let call_hook = RawDetour::new(raw_types::funcs::CALL_PROC_BY_ID_HOOK_TARGET as *const (), call_proc_trampoline).unwrap(); call_hook.enable().unwrap(); call_proc_by_id_original = call_hook.trampoline() as *const () as *const c_void; @@ -150,6 +159,7 @@ impl Proc { #[no_mangle] extern "C" fn on_runtime(error: *const c_char) { let str = unsafe { CStr::from_ptr(error) }.to_string_lossy(); + warn!("Runtime hooked! Reason: {str:?}"); for func in inventory::iter:: { func.0(&str); @@ -169,8 +179,16 @@ extern "C" fn call_proc_by_id_hook( _unknown2: u32, _unknown3: u32 ) -> u8 { - match PROC_HOOKS.with(|h| match h.borrow().get(&proc_id) { + match PROC_HOOKS.with(|h| { + let hooks = h.borrow(); + let hook_entry = hooks.get(&proc_id).or_else(|| { + let proc_path = Proc::from_id(proc_id)?.path; + hooks.values().find(|(_, path)| *path == proc_path) + }); + + match hook_entry { Some((hook, path)) => { + info!("Proc ({path}) hooked!\nproc_id: {}\nnum_args: {}\nargs_ptr: 0x{:08X}", proc_id.0, num_args, args_ptr as usize); let (src, usr, args) = unsafe { ( Value::from_raw(src_raw), @@ -202,6 +220,7 @@ extern "C" fn call_proc_by_id_hook( } } None => None + } }) { Some(result) => { unsafe { diff --git a/auxtools/src/lib.rs b/auxtools/src/lib.rs index e9472acf..72d9545f 100644 --- a/auxtools/src/lib.rs +++ b/auxtools/src/lib.rs @@ -25,6 +25,8 @@ use std::{ sync::atomic::{AtomicBool, Ordering} }; +pub use log::{error, warn, info}; + pub use auxtools_impl::{full_shutdown, hook, init, pin_dll, runtime_handler, shutdown}; /// Used by the [pin_dll] macro to set dll pinning pub use ctor; @@ -61,6 +63,11 @@ signatures! { 1602..1648 => "55 8B EC 81 EC 98 00 00 00 A1 ?? ?? ?? ?? 33 C5 89 45 FC 8B 55", ..1602 => (call, "E8 ?? ?? ?? ?? 83 C4 2C 89 45 F4 89 55 F8 8B 45 F4 8B 55 F8 5F 5E 5B 8B E5 5D C3 CC 55 8B EC 83 EC 0C 53 8B 5D 10 8D 45 FF") ), + hook_call_proc_by_id => version_dependent_signature!( + 1648.. => "55 8B EC 81 EC 9C 00 00 00 A1 ?? ?? ?? ?? 33 C5 89 45 ?? 8B 55 ?? 8B 45 ??", + 1602..1648 => "55 8B EC 81 EC 98 00 00 00 A1 ?? ?? ?? ?? 33 C5 89 45 FC 8B 55", + ..1602 => (call, "E8 ?? ?? ?? ?? 83 C4 2C 89 45 F4 89 55 F8 8B 45 F4 8B 55 F8 5F 5E 5B 8B E5 5D C3 CC 55 8B EC 83 EC 0C 53 8B 5D 10 8D 45 FF") + ), get_variable => version_dependent_signature!( 1648.. => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 83 EC 0C 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 ?? ?? ?? ?? 8B 5D ?? 8B 75 ??", 1615..1648 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 00 00 00 00 50 83 EC 28 A1 ?? ?? ?? ?? 33 C5 89 45 F0 53 56 57 50 8D 45 F4 64 A3 00 00 00 00 8B 5D", @@ -71,18 +78,15 @@ signatures! { 1648.. => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 83 EC 24 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 ?? ?? ?? ?? 8B 4D ??", 1615..1648 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 00 00 00 00 50 83 EC 0C 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 F4 64 A3 00 00 00 00 8B 4D", 1602..1614 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 00 00 00 00 50 83 EC 08 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 F4 64 A3 00 00 00 00 8B 4D", - ..1602 => "55 8B EC 8B 4D 08 0F B6 C1 48 57 8B 7D 10 83 F8 53 0F ?? ?? ?? ?? ?? 0F B6 80 ?? ?? ?? ?? FF 24 85 ?? ?? ?? ?? FF 75 18 FF 75 14 57 FF 75 0C E8 ?? ?? ?? ?? 83 C4 10 5F 5D C3"), + ..1602 => "55 8B EC 8B 4D 08 0F B6 C1 48 57 8B 7D 10 83 F8 53 0F ?? ?? ?? ?? ?? 0F B6 80 ?? ?? ?? ?? FF 24 85 ?? ?? ?? ?? FF 75 18 FF 75 14 57 FF 75 0C E8 ?? ?? ?? ?? 83 C4 10 5F 5D C3" + ), get_string_table_entry => universal_signature!("55 8B EC 8B 4D 08 3B 0D ?? ?? ?? ?? 73 10 A1"), call_datum_proc_by_name => version_dependent_signature!( 1615.. => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 00 00 00 00 50 83 EC 14 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 F4 64 A3 00 00 00 00 8B 75 1C 8D 45 F3 8B 7D 18 8B 5D 10 6A 00", 1602..1614 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 00 00 00 00 50 83 EC 18 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 F4 64 A3 00 00 00 00 8B 75 14 8D 45 F3 8B 5D 10 6A 01", ..1602 => "55 8B EC 83 EC 0C 53 8B 5D 10 8D 45 FF 56 8B 75 14 57 6A 01 50 FF 75 1C C6 45 FF 00 FF 75 18 6A 00 56" ), - /* ..1614 "E8 ?? ?? ?? ?? 83 C4 0C 81 FF FF FF 00 00 74 ?? 85 FF 74 ?? 57 FF 75 ??" - 1615.. "E8 ?? ?? ?? ?? 83 C4 0C 81 FF FF FF 00 00 74 13 85 FF 74 0F 8B 4D 14 57 53 56" */ dec_ref_count => universal_signature!(call, "E8 ?? ?? ?? ?? 83 C4 0C 81 FF FF FF 00 00 74 ?? 85 FF 74"), - /* ..1614 "E8 ?? ?? ?? ?? FF 77 ?? FF 77 ?? E8 ?? ?? ?? ?? 8D 77 ?? 56 E8 ?? ?? ?? ??" - 1615.. "E8 ?? ?? ?? ?? FF 73 14 FF 73 10 E8 ?? ?? ?? ?? 8D 73 30 56 E8" */ inc_ref_count => universal_signature!(call, "E8 ?? ?? ?? ?? FF ?? ?? FF ?? ?? E8 ?? ?? ?? ?? 8D ?? ?? 56 E8"), get_assoc_element => version_dependent_signature!( 1614.. => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 00 00 00 00 50 51 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 F4 64 A3 00 00 00 00 8B 5D 08 80", @@ -109,13 +113,19 @@ signatures! { 1615..1648 => "55 8B EC 83 EC 08 53 8B 5D 08 0F B6 C3 48 56 57 83 F8 53 0F 87 23 01 00 00 0F B6 80 ?? ?? ?? ?? 8B 4D 10 FF 24 85 ?? ?? ?? ?? 8B 7D 0C 6A 0F 57 53 E8 ?? ?? ?? ?? 50 E8 ?? ?? ?? ?? 83 C4 10 85 C0 0F 84 02 01 00 00 8B 08 8B 40 0C 85 C0 0F 84 F5 00 00 00 8B 75 14 8B 5D 10", ..1614 => "55 8B EC 8B 4D 08 83 EC 0C 0F B6 C1 48 53 83 F8 53 0F 87 ?? ?? ?? ?? 0F B6 ?? ?? ?? ?? ?? 8B 55 10 FF 24 ?? ?? ?? ?? ?? 6A 0F FF 75 0C 51 E8 ?? ?? ?? ?? 50 E8 ?? ?? ?? ?? 83 C4 10 85 C0 0F 84 ?? ?? ?? ?? 8B 48 0C 8B 10 85 C9 0F 84 ?? ?? ?? ?? 8B 45 14 8B 5D 10" ), - get_length => universal_signature!("55 8B EC 8B 4D ?? 83 EC ?? 0F B6 C1 48 53 56 57 83 F8 ?? 0F 87 ?? ?? ?? ??" + get_length => version_dependent_signature!( + 1648.. => "55 8B EC 8B 4D ?? 83 EC ?? 0F B6 C1 48 53 56 57 83 F8 ?? 0F 87 ?? ?? ?? ??", + 1602..1648 => "55 8B EC 8B 4D 08 83 EC 18 0F B6 C1 48 53 56 57 83 F8 53 0F 87 F5 01 00 00", + ..1602 => "55 8B EC 8B 4D ?? 83 EC ?? 0F B6 C1 48 53 56 57 83 F8 ?? 0F 87 ?? ?? ?? ??" ), get_misc_by_id => version_dependent_signature!( 1615.. => (call, "E8 ?? ?? ?? ?? 83 C4 04 85 C0 74 08 0F B7 38 8B 70 08 EB 04 33 FF 33 F6 0F B7 C7 50 89 45 F8 E8 ?? ?? ?? ??"), ..1614 => (call, "E8 ?? ?? ?? ?? 83 C4 04 85 C0 75 ?? FF 75 ?? E8 ?? ?? ?? ?? FF 30 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? A1 ?? ?? ?? ??") ), - runtime => universal_signature!(call, "E8 ?? ?? ?? ?? 83 C4 04 8B 85 ?? ?? ?? ?? 0F B6 C0 51 66 0F 6E C0 0F 5B C0"), + runtime => version_dependent_signature!( + 1674.. => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 00 00 00 00 50 83 EC 44 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 00 00 00 00 89 65 ??", + ..1674 => (call, "E8 ?? ?? ?? ?? 83 C4 04 8B 85 ?? ?? ?? ?? 0F B6 C0 51 66 0F 6E C0 0F 5B C0") + ), suspended_procs => version_dependent_signature!( 1615.. => (2, "8B 1D ?? ?? ?? ?? 56 8B 75 ?? 57 8B 3D ?? ?? ?? ?? 89 7D ?? 8B 86"), ..1614 => (1, "A1 ?? ?? ?? ?? 8B D8 89 45 ?? 89 75 ?? 3B DA 73 ?? 8D 0C ?? D1 E9 8B 04 ??") @@ -131,8 +141,8 @@ signatures! { 1585..1602 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 83 EC ?? 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 ?? ?? ?? ?? 8B 1D ?? ?? ?? ??", 1561..1585 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 83 EC 18 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 ?? ?? ?? ?? 8B 4D ?? 0F B6 C1", 1543..1561 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 83 EC 14 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 ?? ?? ?? ?? 8B 4D ??", - ..1543 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 83 EC 10 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 ?? ?? ?? ?? 8B 5D ?? 0F B6 C3"), - + ..1543 => "55 8B EC 6A FF 68 ?? ?? ?? ?? 64 A1 ?? ?? ?? ?? 50 83 EC 10 53 56 57 A1 ?? ?? ?? ?? 33 C5 50 8D 45 ?? 64 A3 ?? ?? ?? ?? 8B 5D ?? 0F B6 C3" + ), current_execution_context => version_dependent_signature!( 1615.. => (1, "A1 ?? ?? ?? ?? 56 53 6A 00 8B 00 57 6A ?? 89 4D ?? FF 70 ?? 8B 4D ?? FF"), ..1614 => (1, "A1 ?? ?? ?? ?? FF 75 ?? 89 4D ?? 8B 4D ?? 8B 00 6A 00 52 6A 12 FF 70 ??") @@ -148,56 +158,116 @@ pub const BYONDCORE: &str = "libbyond.so"; #[cfg(unix)] signatures! { get_proc_array_entry => version_dependent_signature!( - 1584.. => (call, "E8 ?? ?? ?? ?? 0F B7 F6 89 C7 89 B5 ?? ?? ?? ?? 89 34 24 E8 ?? ?? ?? ??"), + 1647.. => "8B 44 24 04 39 05 ?? ?? ?? ?? 76 ?? 6B C0 2C 03 05 ?? ?? ?? ?? C3", + 1584..1647 => (call, "E8 ?? ?? ?? ?? 0F B7 F6 89 C7 89 B5 ?? ?? ?? ?? 89 34 24 E8 ?? ?? ?? ??"), ..1584 => (call, "E8 ?? ?? ?? ?? 8B 00 89 04 24 E8 ?? ?? ?? ?? 8B 00 89 44 24 ?? 8D 45 ??") ), - get_string_id => universal_signature!("55 89 E5 57 56 89 CE 53 89 D3 83 EC 5C 8B 55 ?? 85 C0 88 55 ?? 0F 84 ?? ?? ?? ??"), - call_proc_by_id => universal_signature!(call, "E8 ?? ?? ?? ?? 8B 45 ?? 8B 55 ?? 89 45 ?? 89 55 ?? 8B 55 ?? 8B 4D ?? 8B 5D ??"), - get_variable => universal_signature!("55 89 E5 81 EC C8 00 00 00 8B 55 ?? 89 5D ?? 8B 5D ?? 89 75 ?? 8B 75 ??"), + get_string_id => version_dependent_signature!( + 1647.. => "55 57 56 89 CE 53 89 D3 83 EC 4C 8B 54 24 60 88 54 24 0C 85 C0", + ..1647 => "55 89 E5 57 56 89 CE 53 89 D3 83 EC 5C 8B 55 ?? 85 C0 88 55 ?? 0F 84 ?? ?? ?? ??" + ), + call_proc_by_id => version_dependent_signature!( + 1647.. => "55 31 C0 57 56 53 81 EC AC 00 00 00 F3 0F 7E 8C 24 D8 00 00 00 8B 8C 24 D0 00 00 00", + ..1647 => (call, "E8 ?? ?? ?? ?? 8B 45 ?? 8B 55 ?? 89 45 ?? 89 55 ?? 8B 55 ?? 8B 4D ?? 8B 5D ??") + ), + hook_call_proc_by_id => version_dependent_signature!( + // 1647 normal DM dispatch enters through a ProcInstance executor, not the by-id wrapper. + 1647.. => "55 89 E5 57 56 53 81 EC ?? ?? ?? ?? 89 85 ?? ?? ?? ?? A1 ?? ?? ?? ??", + //1647.. => "55 31 C0 57 56 53 81 EC AC 00 00 00 F3 0F 7E 8C 24 D8 00 00 00 8B 8C 24 D0 00 00 00", + ..1647 => (call, "E8 ?? ?? ?? ?? 8B 45 ?? 8B 55 ?? 89 45 ?? 89 55 ?? 8B 55 ?? 8B 4D ?? 8B 5D ??") + ), + get_variable => version_dependent_signature!( + 1647.. => "55 89 E5 57 56 53 83 EC ?? F3 0F 7E ?? ?? 8B 75 ?? 8B 5D ??", + ..1647 => "55 89 E5 81 EC C8 00 00 00 8B 55 ?? 89 5D ?? 8B 5D ?? 89 75 ?? 8B 75 ??" + ), set_variable => version_dependent_signature!( - 1560.. => (call, "E8 ?? ?? ?? ?? 8B 45 ?? 8D 65 ?? 5B 5E 5F 5D C3 8D B4 26 00 00 00 00 8B 40 ??"), + 1647.. => "55 89 E5 57 56 53 83 EC ?? F3 0F 7E ?? ?? 8B ?? 10", + 1560..1647 => (call, "E8 ?? ?? ?? ?? 8B 45 ?? 8D 65 ?? 5B 5E 5F 5D C3 8D B4 26 00 00 00 00 8B 40 ??"), 1543..1560 => "55 89 E5 81 EC A8 00 00 00 8B 55 ?? 89 5D ?? 8B 4D ?? 89 7D ?? 8B 5D ??", ..1543 => "55 89 E5 81 EC A8 00 00 00 8B 55 ?? 8B 45 ?? 89 5D ?? 8B 5D ?? 89 7D ??" ), - get_string_table_entry => universal_signature!("55 89 E5 83 EC 18 8B 45 ?? 39 05 ?? ?? ?? ?? 76 ?? 8B 15 ?? ?? ?? ?? 8B 04 ??"), + get_string_table_entry => version_dependent_signature!( + 1647.. => "56 53 83 EC 14 8B 44 24 20 39 05 ?? ?? ?? ?? 76 ?? 8B 15 ?? ?? ?? ?? 8B 04 82 85 C0 74 ?? 83 C4 14 5B 5E C3", + ..1647 => "55 89 E5 83 EC 18 8B 45 ?? 39 05 ?? ?? ?? ?? 76 ?? 8B 15 ?? ?? ?? ?? 8B 04 ??" + ), call_datum_proc_by_name => version_dependent_signature!( - 1606.. => "55 89 E5 57 56 89 CE 53 89 D3 83 EC ?? 0F B6 55 ?? 89 45 ?? 8B 45 ?? 8B 7D ?? C6 45 E7 ?? 0F B6 CA 89 45 B0 8D 45 ?? 89 44 24 ?? 8B 45 ?? 89 ?? BC 31 C9 88 ?? BB 8B 55 ?? C7 44 24 ?? 01 00 00 00", + 1647.. => (call, "E8 ?? ?? ?? ?? 83 C4 ?? 53 6A 00 68 ?? ?? ?? ??"), + 1606..1647 => "55 89 E5 57 56 89 CE 53 89 D3 83 EC ?? 0F B6 55 ?? 89 45 ?? 8B 45 ?? 8B 7D ?? C6 45 E7 ?? 0F B6 CA 89 45 B0 8D 45 ?? 89 44 24 ?? 8B 45 ?? 89 ?? BC 31 C9 88 ?? BB 8B 55 ?? C7 44 24 ?? 01 00 00 00", + 1602..1603 => "55 89 E5 57 56 89 CE 53 89 D3 83 EC 6C 0F B6 55 08 89 45 B4 8B 45 14 8B 7D 10 C6 45 E7 00 0F B6 CA 89 45 B0 8D 45 E7 89 44 24 08 8B 45 B0 89 4D BC 31 C9 88 55 BB 8B 55 0C C7 44 24 0C 01 00 00 00", ..1606 => "55 89 E5 57 56 53 83 EC 5C 8B 55 ?? 0F B6 45 ?? 8B 4D ?? 8B 5D ?? 89 14 24 8B 55 ?? 88 45 ?? 0F B6 F8 8B 75 ?? 8D 45 ?? 89 44 24 ?? 89 F8 89 4C 24 ?? 31 C9 C6 45 ?? 00 C7 44 24 ?? 01 00 00 00" ), dec_ref_count => version_dependent_signature!( - 1543.. => (call, "E8 ?? ?? ?? ?? C7 06 00 00 00 00 C7 46 ?? 00 00 00 00 A1 ?? ?? ?? ?? 0F B7 50 ??"), + 1647.. => (call, "E8 ?? ?? ?? ?? 8D 85 ?? ?? ?? ?? 89 04 ?? E8 ?? ?? ?? ?? 83 C4 10 83 BD D8 ?? ?? ?? ??"), + 1543..1647 => (call, "E8 ?? ?? ?? ?? C7 06 00 00 00 00 C7 46 ?? 00 00 00 00 A1 ?? ?? ?? ?? 0F B7 50 ??"), ..1543 => (call, "E8 ?? ?? ?? ?? 8B 4D ?? C7 44 24 ?? 00 00 00 00 C7 44 24 ?? 00 00 00 00 89 0C 24") ), - inc_ref_count => universal_signature!(call, "E8 ?? ?? ?? ?? 8B 43 ?? 80 48 ?? 04 8B 5D ?? 8B 75 ?? 8B 7D ?? 89 EC 5D"), + inc_ref_count => version_dependent_signature!( + 1647.. => (call, "E8 ?? ?? ?? ?? 8B 4B ?? 83 C4 ?? 8D 51 01"), + ..1647 => (call, "E8 ?? ?? ?? ?? 8B 43 ?? 80 48 ?? 04 8B 5D ?? 8B 75 ?? 8B 7D ?? 89 EC 5D") + ), get_assoc_element => version_dependent_signature!( - 1602.. => "55 89 E5 83 EC ?? ?? ?? ?? ?? 5D F4 89 D3 89 75 F8 89 D6 89 7D FC 89 CF 89 45 B4 0F 84 B7 00 00 ??", + 1647.. => "55 89 E5 57 56 53 83 EC ?? F3 ?? ?? ?? ?? 8B 7D ?? 66 0F 6F ?? 66 0F 7E ?? 66 0F 7E ?? ??", + 1602..1647 => "55 89 E5 83 EC ?? ?? ?? ?? ?? 5D F4 89 D3 89 75 F8 89 D6 89 7D FC 89 CF 89 45 B4 0F 84 B7 00 00 ??", ..1602 => "55 89 E5 83 EC 68 89 4D ?? B9 7B 00 00 00 89 5D ?? 89 D3 89 75 ?? 89 C6" ), set_assoc_element => version_dependent_signature!( - 1602.. => "55 89 E5 83 EC 68 89 75 F8 8B 75 08 89 5D F4 89 C3 8B 45 0C 89 7D FC 80 FB 3C 89 D7 88 5D BF 89 ??", + 1660.. => "55 89 E5 57 56 53 83 EC ?? F3 ?? ?? ?? ?? 66 0F ?? ??", + 1647..1660 => "55 89 E5 57 56 53 83 EC 6C F3 ?? ?? ?? ?? 89 45 ?? 89 55 ??", + 1602..1647 => "55 89 E5 83 EC 68 89 75 F8 8B 75 08 89 5D F4 89 C3 8B 45 0C 89 7D FC 80 FB 3C 89 D7 88 5D BF 89 ??", ..1602 => "55 B9 7C 00 00 00 89 E5 83 EC 58 89 7D ?? 8B 7D ?? 89 5D ?? 89 C3 8B 45 ??" ), - create_list => universal_signature!("55 89 E5 57 56 53 83 EC 2C A1 ?? ?? ?? ?? 8B 75 ?? 85 C0 0F 84 ?? ?? ?? ??"), - append_to_list => universal_signature!("55 89 E5 83 EC 38 3C 54 89 5D ?? 8B 5D ?? 89 75 ?? 8B 75 ?? 89 7D ?? 76 ??"), - remove_from_list => universal_signature!("55 89 E5 83 EC 48 3C 54 89 5D ?? 89 C3 89 75 ?? 8B 75 ?? 89 7D ?? 8B 7D ??"), - get_length => universal_signature!("55 89 E5 57 56 53 83 EC 6C 8B 45 ?? 8B 5D ?? 3C 54 76 ?? 31 F6 8D 65 ??"), - get_misc_by_id => universal_signature!(call, "E8 ?? ?? ?? ?? 0F B7 55 ?? 03 1F 0F B7 4B ?? 89 8D ?? ?? ?? ?? 0F B7 5B ??"), - runtime => universal_signature!(call, "E8 ?? ?? ?? ?? 31 C0 8D B4 26 00 00 00 00 8B 5D ?? 8B 75 ?? 8B 7D ?? 89 EC"), - suspended_procs => universal_signature!(1, "A3 ?? ?? ?? ?? 8D 14 ?? 73 ?? 8D 74 26 00 83 C0 01 8B 14 ?? 39 C3 89 54 ?? ??"), - suspended_procs_buffer => universal_signature!(2, "89 35 ?? ?? ?? ?? C7 04 24 ?? ?? ?? ?? E8 ?? ?? ?? ?? 8B 45 ?? 83 C0 08"), + create_list => version_dependent_signature!( + 1647.. => "55 57 56 53 83 ?? 0C A1 ?? ?? ?? ?? 8B 7C ?? ?? 85 C0 0F ?? ?? ?? ??", + ..1647 => "55 89 E5 57 56 53 83 EC 2C A1 ?? ?? ?? ?? 8B 75 ?? 85 C0 0F 84 ?? ?? ?? ??" + ), + append_to_list => version_dependent_signature!( + 1647.. => "56 66 0F 6E ?? 66 0F 6E ?? 53 66 0F 62 ?? 66 0F 7E ??", + ..1647 => "55 89 E5 83 EC 38 3C 54 89 5D ?? 8B 5D ?? 89 75 ?? 8B 75 ?? 89 7D ?? 76 ??" + ), + remove_from_list => version_dependent_signature!( + 1674.. => "55 66 0F 6E CA 66 0F 6E C0 57 66 0F 62 C1 56 66 0F 6F C8 66 0F 7E C2 53", + 1647..1674 => (call, "E8 ?? ?? ?? ?? 83 C4 ?? 09 C6 39 5C ?? ??"), + ..1647 => "55 89 E5 83 EC 48 3C 54 89 5D ?? 89 C3 89 75 ?? 8B 75 ?? 89 7D ?? 8B 7D ??" + ), + get_length => version_dependent_signature!( + 1647.. => "55 57 56 53 83 EC ?? F3 0F ?? ?? ?? ?? 66 0F ?? ?? 66 0F ?? ??", + ..1647 => "55 89 E5 57 56 53 83 EC 6C 8B 45 ?? 8B 5D ?? 3C 54 76 ?? 31 F6 8D 65 ??" + ), + get_misc_by_id => version_dependent_signature!( + 1647.. => (call, "E8 ?? ?? ?? ?? 83 C4 ?? 85 C0 0F 84 ?? ?? ?? ?? 8D B4 26 ?? ?? ?? ?? 8D 76 ?? 0F B7 ??"), + ..1647 => (call, "E8 ?? ?? ?? ?? 0F B7 55 ?? 03 1F 0F B7 4B ?? 89 8D ?? ?? ?? ?? 0F B7 5B ??") + ), + runtime => version_dependent_signature!( + 1647.. => "55 89 E5 56 53 83 EC ?? 8B 15 ?? ?? ?? ?? 85 D2 0F 84 ?? ?? ?? ?? 0F B6 42 ?? 3C 01", + ..1647 => (call, "E8 ?? ?? ?? ?? 31 C0 8D B4 26 00 00 00 00 8B 5D ?? 8B 75 ?? 8B 7D ?? 89 EC") + ), + suspended_procs => version_dependent_signature!( + 1647.. => (1, "A3 ?? ?? ?? ?? 39 E8 0F 83 ?? ?? ?? ??"), + ..1647 => (1, "A3 ?? ?? ?? ?? 8D 14 ?? 73 ?? 8D 74 26 00 83 C0 01 8B 14 ?? 39 C3 89 54 ?? ??") + ), + suspended_procs_buffer => version_dependent_signature!( + 1647.. => (2, "89 35 ?? ?? ?? ?? 8D 5F ?? 68 ?? ?? ?? ??"), + ..1647 => (2, "89 35 ?? ?? ?? ?? C7 04 24 ?? ?? ?? ?? E8 ?? ?? ?? ?? 8B 45 ?? 83 C0 08") + ), to_string => version_dependent_signature!( - 1602.. => "55 89 E5 83 ?? ?? 89 5D F4 8D ?? ?? 89 75 F8 89 7D FC 80 ?? ?? ?? ?? ?? B8", + 1647.. => "55 89 E5 57 56 53 83 EC ?? 0F B6 ?? ?? 3C 45", + 1602..1647 => "55 89 E5 83 ?? ?? 89 5D F4 8D ?? ?? 89 75 F8 89 7D FC 80 ?? ?? ?? ?? ?? B8", 1560..1602 => (call, "E8 ?? ?? ?? ?? 89 04 24 E8 ?? ?? ?? ?? 8B 00 8D 4D ?? 89 0C 24"), 1543..1560 => "55 89 E5 83 EC 68 A1 ?? ?? ?? ?? 8B 15 ?? ?? ?? ?? 8B 0D ?? ?? ?? ?? 89 5D ??", ..1543 => "55 89 E5 83 EC 58 89 5D ?? 8B 5D ?? 89 75 ?? 8B 75 ?? 89 7D ?? 80 FB 54" ), - current_execution_context => universal_signature!(1, "A1 ?? ?? ?? ?? C7 44 24 ?? 00 00 00 00 C7 44 24 ?? 00 00 00 00 89 74 24"), + current_execution_context => version_dependent_signature!( + 1647.. => (2, "8B 15 ?? ?? ?? ?? 85 D2 0F 84 ?? ?? ?? ?? 0F B6 ?? ??"), + 1602..1647 => (2, "8B 15 ?? ?? ?? ?? 8B 12 8B 4A 0C 8B 52 08 89 44 24 04"), + ..1602 => (1, "A1 ?? ?? ?? ?? C7 44 24 ?? 00 00 00 00 C7 44 24 ?? 00 00 00 00 89 74 24") + ), variable_names => version_dependent_signature!( - 1543.. => (1, "A1 ?? ?? ?? ?? 8B 13 8B 39 8B 75 ?? 8B 14 ?? 89 7D ?? 8B 3C ?? 83 EE 02"), + 1647.. => (1, "A1 ?? ?? ?? ?? 8B 04 ?? 89 4C ?? ??"), + 1543..1647 => (1, "A1 ?? ?? ?? ?? 8B 13 8B 39 8B 75 ?? 8B 14 ?? 89 7D ?? 8B 3C ?? 83 EE 02"), ..1543 => (2, "8B 35 ?? ?? ?? ?? 89 5D ?? 0F B7 08 89 75 ?? 66 C7 45 ?? 00 00 89 7D ??") ) } @@ -230,6 +300,20 @@ fn pin_dll() -> Result<(), ()> { } byond_ffi_fn! { auxtools_init(_input) { + let ret = auxtools_init_impl(); + + match &ret { + Some(val) if val == "SUCCESS" => info!("Successfully loaded."), + Some(err) => error!("{}", err), + None => warn!("Main function returned None. This probably means something went wrong."), + } + + ret +} } + +fn auxtools_init_impl() -> Option { + colog::init(); + if get_init_level() == InitLevel::None { return Some("SUCCESS".to_owned()) } @@ -253,6 +337,7 @@ byond_ffi_fn! { auxtools_init(_input) { (suspended_procs as *mut raw_types::procs::SuspendedProcs), (suspended_procs_buffer as *mut raw_types::procs::SuspendedProcsBuffer), call_proc_by_id, + hook_call_proc_by_id, call_datum_proc_by_name, get_proc_array_entry, get_string_id, @@ -273,6 +358,8 @@ byond_ffi_fn! { auxtools_init(_input) { } unsafe { + raw_types::funcs::byond_build = version::get().build; + raw_types::funcs::CALL_PROC_BY_ID_HOOK_TARGET = hook_call_proc_by_id; raw_types::funcs::CURRENT_EXECUTION_CONTEXT = current_execution_context; raw_types::funcs::SUSPENDED_PROCS = suspended_procs; raw_types::funcs::SUSPENDED_PROCS_BUFFER = suspended_procs_buffer; @@ -347,7 +434,7 @@ byond_ffi_fn! { auxtools_init(_input) { } Some("SUCCESS".to_owned()) -} } +} byond_ffi_fn! { auxtools_shutdown(_input) { if get_init_level() != InitLevel::None { @@ -423,7 +510,9 @@ byond_ffi_fn! { auxtools_check_signatures(_input) { } let mut missing = Vec::<&'static str>::new(); for (name, found) in SIGNATURES0.check_all(&byondcore) { - if !found { + let active_detour = get_init_level() == InitLevel::Partial + && matches!(name, "call_proc_by_id" | "hook_call_proc_by_id" | "runtime"); + if !found && !active_detour { missing.push(name); } } diff --git a/auxtools/src/raw_types/funcs.cpp b/auxtools/src/raw_types/funcs.cpp index 1b225092..ca4dd3af 100644 --- a/auxtools/src/raw_types/funcs.cpp +++ b/auxtools/src/raw_types/funcs.cpp @@ -54,7 +54,9 @@ struct RestoreJmpBuf extern "C" { - DEFINE_byond_REGPARM3(call_proc_by_id, Value, (Value, uint32_t, uint32_t, uint32_t, Value, const Value *, uint32_t, uint32_t, uint32_t)); + uint32_t byond_build = 0; + + void *call_proc_by_id_byond = nullptr; DEFINE_byond(call_datum_proc_by_name, Value, (Value, uint32_t, uint32_t, Value, const Value *, uint32_t, uint32_t, uint32_t)); DEFINE_byond(get_proc_array_entry, void *, (uint32_t)); DEFINE_byond_REGPARM3(get_string_id, uint32_t, (const char *, uint8_t, uint8_t, uint8_t)); @@ -64,8 +66,8 @@ extern "C" DEFINE_byond(inc_ref_count, void, (Value)); DEFINE_byond(dec_ref_count, void, (Value)); DEFINE_byond_REGPARM3(get_list_by_id, void *, (uint32_t)); - DEFINE_byond_REGPARM3(get_assoc_element, Value, (Value, Value)); - DEFINE_byond_REGPARM3(set_assoc_element, void, (Value, Value, Value)); + void *get_assoc_element_byond = nullptr; + void *set_assoc_element_byond = nullptr; DEFINE_byond(create_list, uint32_t, (uint32_t)); DEFINE_byond_REGPARM2(append_to_list, void, (Value, Value)); DEFINE_byond_REGPARM2(remove_from_list, void, (Value, Value)); @@ -90,7 +92,21 @@ extern "C" uint8_t call_proc_by_id( BYOND_TRY { - *out = call_proc_by_id_byond(usr, proc_type, proc_id, unk_0, src, args, args_count, unk_1, unk_2); + #ifdef _WIN32 + using FnCallProcById = Value (*)(Value, uint32_t, uint32_t, uint32_t, Value, const Value *, uint32_t, uint32_t, uint32_t); + *out = reinterpret_cast(call_proc_by_id_byond)(usr, proc_type, proc_id, unk_0, src, args, args_count, unk_1, unk_2); + #else + if (byond_build >= 1647) + { + using FnCallProcById = Value (*)(Value, uint32_t, uint32_t, uint32_t, Value, const Value *, uint32_t, void *, uint32_t); + *out = reinterpret_cast(call_proc_by_id_byond)(usr, proc_type == 0 ? 0x12 : proc_type, proc_id, unk_0, src, args, args_count, nullptr, unk_2); + } + else + { + using FnCallProcById = Value (LINUX_REGPARM3 *)(Value, uint32_t, uint32_t, uint32_t, Value, const Value *, uint32_t, uint32_t, uint32_t); + *out = reinterpret_cast(call_proc_by_id_byond)(usr, proc_type, proc_id, unk_0, src, args, args_count, unk_1, unk_2); + } + #endif return 1; } BYOND_CATCH @@ -262,7 +278,21 @@ extern "C" uint8_t get_assoc_element(Value *out, Value datum, Value index) { clean(datum); clean(index); - *out = get_assoc_element_byond(datum, index); + #ifdef _WIN32 + using FnGetAssocElement = Value (*)(Value, Value); + *out = reinterpret_cast(get_assoc_element_byond)(datum, index); + #else + if (byond_build >= 1647) + { + using FnGetAssocElement = Value (*)(Value, Value); + *out = reinterpret_cast(get_assoc_element_byond)(datum, index); + } + else + { + using FnGetAssocElement = Value (LINUX_REGPARM3 *)(Value, Value); + *out = reinterpret_cast(get_assoc_element_byond)(datum, index); + } + #endif return 1; } BYOND_CATCH @@ -280,7 +310,21 @@ extern "C" uint8_t set_assoc_element(Value datum, Value index, Value value) clean(datum); clean(index); clean(value); - set_assoc_element_byond(datum, index, value); + #ifdef _WIN32 + using FnSetAssocElement = void (*)(Value, Value, Value); + reinterpret_cast(set_assoc_element_byond)(datum, index, value); + #else + if (byond_build >= 1647) + { + using FnSetAssocElement = void (*)(Value, Value, Value); + reinterpret_cast(set_assoc_element_byond)(datum, index, value); + } + else + { + using FnSetAssocElement = void (LINUX_REGPARM3 *)(Value, Value, Value); + reinterpret_cast(set_assoc_element_byond)(datum, index, value); + } + #endif return 1; } BYOND_CATCH diff --git a/auxtools/src/raw_types/funcs.rs b/auxtools/src/raw_types/funcs.rs index 6f1d58e5..1d69bd43 100644 --- a/auxtools/src/raw_types/funcs.rs +++ b/auxtools/src/raw_types/funcs.rs @@ -6,6 +6,7 @@ use super::{lists, misc, procs, strings, values, variables}; pub static mut CURRENT_EXECUTION_CONTEXT: *mut *mut procs::ExecutionContext = std::ptr::null_mut(); pub static mut SUSPENDED_PROCS_BUFFER: *mut procs::SuspendedProcsBuffer = std::ptr::null_mut(); pub static mut SUSPENDED_PROCS: *mut procs::SuspendedProcs = std::ptr::null_mut(); +pub static mut CALL_PROC_BY_ID_HOOK_TARGET: *const c_void = std::ptr::null(); pub static mut VARIABLE_NAMES: *const variables::VariableNameIdTable = std::ptr::null(); @@ -13,6 +14,7 @@ pub static mut VARIABLE_NAMES: *const variables::VariableNameIdTable = std::ptr: // Rust shouldn't call these so we're going to treat them as void ptrs for // simplicity extern "C" { + pub static mut byond_build: u32; pub static mut call_proc_by_id_byond: *const c_void; pub static mut call_datum_proc_by_name_byond: *const c_void; pub static mut get_proc_array_entry_byond: *const c_void; diff --git a/auxtools/src/raw_types/misc.rs b/auxtools/src/raw_types/misc.rs index f6509f77..66b93a69 100644 --- a/auxtools/src/raw_types/misc.rs +++ b/auxtools/src/raw_types/misc.rs @@ -129,10 +129,10 @@ pub fn set_bytecode(id: BytecodeId, new_bytecode: *mut u32, new_bytecode_count: assert_eq!(super::funcs::get_misc_by_id(&mut misc, id.as_misc_id()), 1); } - let (major, minor) = version::get(); + let (major, build) = version::get().into(); // Lame - if major > 513 || minor >= 1539 { + if major > 513 || build >= 1539 { let misc = misc as *mut Misc_V2; unsafe { (*misc).bytecode.bytecode = new_bytecode; @@ -153,10 +153,10 @@ pub fn get_bytecode(id: BytecodeId) -> (*mut u32, u16) { assert_eq!(super::funcs::get_misc_by_id(&mut misc, id.as_misc_id()), 1); } - let (major, minor) = version::get(); + let (major, build) = version::get().into(); // Lame - if major > 513 || minor >= 1539 { + if major > 513 || build >= 1539 { let misc = misc as *mut Misc_V2; return unsafe { ((*misc).bytecode.bytecode, (*misc).bytecode.count) }; } @@ -171,10 +171,10 @@ pub fn get_locals(id: LocalsId) -> (*const strings::VariableId, usize) { assert_eq!(super::funcs::get_misc_by_id(&mut misc, id.as_misc_id()), 1); } - let (major, minor) = version::get(); + let (major, build) = version::get().into(); // Lame - if major > 513 || minor >= 1539 { + if major > 513 || build >= 1539 { let misc = misc as *mut Misc_V2; return unsafe { ((*misc).locals.names, (*misc).locals.count as usize) }; } @@ -189,10 +189,10 @@ pub fn get_parameters(id: ParametersId) -> (*const ParametersData, usize) { assert_eq!(super::funcs::get_misc_by_id(&mut misc, id.as_misc_id()), 1); } - let (major, minor) = version::get(); + let (major, build) = version::get().into(); // Lame - if major > 513 || minor >= 1539 { + if major > 513 || build >= 1539 { let misc = misc as *mut Misc_V2; return unsafe { ((*misc).parameters.data, (*misc).parameters.count()) }; } diff --git a/auxtools/src/raw_types/procs.rs b/auxtools/src/raw_types/procs.rs index 6e915518..8a89f919 100644 --- a/auxtools/src/raw_types/procs.rs +++ b/auxtools/src/raw_types/procs.rs @@ -19,7 +19,7 @@ pub struct ProcEntry { } #[versioned( - Pre1630 if crate::version::BYOND_VERSION_MINOR <= 1627, + Pre1630 if crate::version::get().build <= 1627, Post1630, )] #[repr(C)] @@ -60,7 +60,7 @@ impl ProcInstance { } #[versioned( - Pre516 if crate::version::BYOND_VERSION_MAJOR < 516, + Pre516 if crate::version::get().major < 516, Post516, )] #[repr(C)] @@ -77,7 +77,7 @@ struct ProcInstanceInner { } #[versioned( - Pre1668 if crate::version::BYOND_VERSION_MAJOR <= 515 || (crate::version::BYOND_VERSION_MAJOR == 516 && crate::version::BYOND_VERSION_MINOR <= 1667), + Pre1668 if crate::version::get().major <= 515 || (crate::version::get().major == 516 && crate::version::get().build <= 1667), Post1668, )] #[repr(C)] diff --git a/auxtools/src/raw_types/values.rs b/auxtools/src/raw_types/values.rs index 7c88d9bf..40a3f77d 100644 --- a/auxtools/src/raw_types/values.rs +++ b/auxtools/src/raw_types/values.rs @@ -22,9 +22,16 @@ pub enum ValueTag { Image = 0x0D, World = 0x0E, - // Lists List = 0x0F, + // Lists ArgList = 0x10, + Unk0 = 0x11, + Unk1 = 0x12, + Unk2 = 0x13, + Unk3 = 0x14, + Unk4 = 0x15, + Unk5 = 0x16, + MobContents = 0x17, TurfContents = 0x18, AreaContents = 0x19, @@ -32,6 +39,13 @@ pub enum ValueTag { ObjContents = 0x1C, DatumTypepath = 0x20, + Unk6 = 0x22, + Unk7 = 0x24, + Unk8 = 0x25, + + Unk9 = 0x28, + Unk10 = 0x29, + Datum = 0x21, SaveFile = 0x23, ProcRef = 0x26, @@ -52,7 +66,12 @@ pub enum ValueTag { AreaOverlays = 0x38, AreaUnderlays = 0x39, Appearance = 0x3A, + Unk11 = 0x3B, Pointer = 0x3C, + Unk12 = 0x3D, + Unk13 = 0x3E, + Unk14 = 0x3F, + ImageOverlays = 0x40, ImageUnderlays = 0x41, ImageVars = 0x42, @@ -69,7 +88,7 @@ pub enum ValueTag { Alist = 0x55, PixLoc = 0x56, Vector = 0x57, - Callee = 0x58 + Callee = 0x58, } impl fmt::Display for Value { diff --git a/auxtools/src/sigscan.rs b/auxtools/src/sigscan.rs index c74182b1..d474524e 100644 --- a/auxtools/src/sigscan.rs +++ b/auxtools/src/sigscan.rs @@ -35,7 +35,7 @@ macro_rules! signatures { impl Signatures { #[allow(dead_code)] pub fn check_all(&self, scanner: &$crate::sigscan::Scanner) -> [(&'static str, bool); count!($($name)*)] { - let version = $crate::version::get().1; + let version = $crate::version::get().build; [$( (stringify!($name), self.$name.find(scanner, version).is_some()), )*] @@ -99,7 +99,7 @@ macro_rules! universal_signature { macro_rules! find_signature_inner { ($scanner:ident, $name:ident, $type:ty) => { let $name: $type; - if let Some(ptr) = SIGNATURES0.$name.find(&$scanner, $crate::version::get().1) { + if let Some(ptr) = SIGNATURES0.$name.find(&$scanner, $crate::version::get().build) { $name = ptr as $type; } else { return Some(format!("FAILED (Couldn't find {})", stringify!($name))); @@ -111,7 +111,7 @@ macro_rules! find_signature_inner { macro_rules! find_signature_inner_result { ($scanner:ident, $name:ident, $type:ty) => { let $name: $type; - if let Some(ptr) = SIGNATURES0.$name.find(&$scanner, $crate::version::get().1) { + if let Some(ptr) = SIGNATURES0.$name.find(&$scanner, $crate::version::get().build) { $name = ptr as $type; } else { return Err(format!("FAILED (Couldn't find {})", stringify!($name))); diff --git a/auxtools/src/sigscan/linux.rs b/auxtools/src/sigscan/linux.rs index 3446c0ec..02009b24 100644 --- a/auxtools/src/sigscan/linux.rs +++ b/auxtools/src/sigscan/linux.rs @@ -8,9 +8,7 @@ use libc::{dl_iterate_phdr, dl_phdr_info, Elf32_Phdr, PT_LOAD}; #[repr(C)] struct CallbackData { module_name_ptr: *const c_char, - memory_start: usize, - memory_len: usize, - memory_area: Option<&'static [u8]> + memory_areas: Vec<(usize, usize)> } pub struct Scanner { @@ -27,15 +25,12 @@ extern "C" fn dl_phdr_callback(info: *mut dl_phdr_info, _size: usize, data: *mut } let headers: &'static [Elf32_Phdr] = unsafe { std::slice::from_raw_parts(info.dlpi_phdr, info.dlpi_phnum as usize) }; - let elf_header = headers.iter().filter(|p| p.p_type == PT_LOAD).next().unwrap(); - let start = (info.dlpi_addr + elf_header.p_vaddr) as usize; - let end = start + elf_header.p_memsz as usize; - let len = end - start; - - cb_data.memory_start = start; - cb_data.memory_len = len; - cb_data.memory_area = Some(unsafe { std::slice::from_raw_parts(start as *const u8, len) }); + // checks whether the segment is loadable and executable (executable flag is 1), adds to memory areas if it is + for elf_header in headers.iter().filter(|p| p.p_type == PT_LOAD && p.p_flags & 1 == 1) { + let start = (info.dlpi_addr + elf_header.p_vaddr) as usize; + cb_data.memory_areas.push((start, elf_header.p_memsz as usize)); + } 0 } @@ -51,43 +46,47 @@ impl Scanner { let module_name_ptr = module_name.as_ptr(); let mut data = CallbackData { module_name_ptr, - memory_start: 0, - memory_len: 0, - memory_area: None + memory_areas: Vec::new() }; unsafe { dl_iterate_phdr(Some(dl_phdr_callback), &mut data as *mut CallbackData as *mut c_void) }; - let mut data_current = data.memory_start as *mut u8; - let data_end = (data.memory_start + data.memory_len) as *mut u8; - - if data_current.is_null() || data_end == data_current { - // There's no more bytes to scan or the module wasn't found. + if data.memory_areas.is_empty() { + // The module wasn't found. return None; } - let mut signature_offset = 0; let mut result: Option<*mut u8> = None; - unsafe { - while data_current <= data_end { - if signature[signature_offset] == None || signature[signature_offset] == Some(*data_current) { - if signature.len() <= signature_offset + 1 { - if result.is_some() { - // Found two matches. - return None; + for (memory_start, memory_len) in data.memory_areas { + if memory_len < signature.len() { + continue; + } + + let mut data_current = memory_start as *mut u8; + let data_end = (memory_start + memory_len - signature.len() + 1) as *mut u8; + let mut signature_offset = 0; + + unsafe { + while data_current < data_end { + if signature[signature_offset] == None || signature[signature_offset] == Some(*data_current) { + if signature.len() <= signature_offset + 1 { + if result.is_some() { + // Found two matches. + return None; + } + result = Some(data_current.offset(-(signature_offset as isize))); + data_current = data_current.offset(-(signature_offset as isize)); + signature_offset = 0; + } else { + signature_offset += 1; } - result = Some(data_current.offset(-(signature_offset as isize))); + } else { data_current = data_current.offset(-(signature_offset as isize)); signature_offset = 0; - } else { - signature_offset += 1; } - } else { - data_current = data_current.offset(-(signature_offset as isize)); - signature_offset = 0; - } - data_current = data_current.offset(1); + data_current = data_current.offset(1); + } } } diff --git a/auxtools/src/version.rs b/auxtools/src/version.rs index 70ceff66..0310d6ef 100644 --- a/auxtools/src/version.rs +++ b/auxtools/src/version.rs @@ -1,8 +1,19 @@ use super::*; use std::{ffi::CString, os::raw::c_char}; -pub static mut BYOND_VERSION_MAJOR: u32 = 0; -pub static mut BYOND_VERSION_MINOR: u32 = 0; +#[derive(Copy, Clone)] +pub struct ByondVersion { + pub major: u32, + pub build: u32, +} + +impl From for (u32, u32) { + fn from(value: ByondVersion) -> (u32, u32) { + (value.major, value.build) + } +} + +pub static mut BYOND_VERSION: ByondVersion = unsafe { std::mem::zeroed() }; #[cfg(windows)] static GET_BYOND_VERSION_SYMBOL: &[u8] = b"?GetByondVersion@ByondLib@@QAEJXZ\0"; @@ -75,13 +86,13 @@ pub fn init() -> Result<(), String> { } unsafe { - BYOND_VERSION_MAJOR = get_byond_version(); - BYOND_VERSION_MINOR = get_byond_build(); + BYOND_VERSION.major = get_byond_version(); + BYOND_VERSION.build = get_byond_build(); } Ok(()) } -pub fn get() -> (u32, u32) { - unsafe { (BYOND_VERSION_MAJOR, BYOND_VERSION_MINOR) } +pub fn get() -> ByondVersion { + unsafe { BYOND_VERSION } } diff --git a/check_sigs.py b/check_sigs.py new file mode 100644 index 00000000..9efbabc5 --- /dev/null +++ b/check_sigs.py @@ -0,0 +1,194 @@ +#!/bin/env python3 + +# This script will return JSON representing all the unmatched signatures, and for what reason. + +# The only library this script needs is `pyelftools`. + +from sys import argv, stderr, exit +import re +from os import SEEK_CUR + +from elftools.elf.elffile import ELFFile + +def print_stderr(text): + stderr.write(text + "\n") + + +UNIX_GET_BUILD_NUMBER_SYMBOL = "_ZN8ByondLib13GetByondBuildEv" + +def determine_platform(file): + file.seek(0) + magic_bytes = file.read(4) + + if magic_bytes.startswith(b"MZ"): + platform = "windows" + elif magic_bytes.startswith(b"\x7fELF"): + platform = "unix" + else: + print_stderr("Unknown BYOND binary provided.") + exit(1) + + return platform + +def determine_version(file): + platform = determine_platform(file) + + file.seek(0) + + # since the unix binaries apparently don't expose the version number in any metadata, + # we have to extract the build number off an exported function. + if platform == "unix": + elffile = ELFFile(file) + dynsym = elffile.get_section_by_name(".dynsym") + symbol_matches = dynsym.get_symbol_by_name(UNIX_GET_BUILD_NUMBER_SYMBOL) + + if symbol_matches is None: + print_stderr( + f"Failed to find '{UNIX_GET_BUILD_NUMBER_SYMBOL}'. Report this issue, with the BYOND version!" + ) + exit(1) + + func_addr = symbol_matches[0].entry.st_value + func_size = symbol_matches[0].entry.st_size + file.seek(func_addr) + + func_data = file.read(func_size) + build_number_offset = func_data.index(b"\xb8") + 1 + return int.from_bytes( + func_data[build_number_offset : build_number_offset + 4], "little" + ) + # windows is easier, there's .rsrc metadata exposing the build number. + # although, it is an unconventional "5.0.. ()" format, + # so you can do a simple regex to grab the build number off it. + elif platform == "windows": + KEY_NAME = "ProductVersion".encode("utf-16-le") + GET_BUILD_FROM_PRODUCTVERSION = re.compile(r"\d\.\d\.\d{3}\.(\d{4})") + + file_data = file.read() + key_index = file_data.index(KEY_NAME) + value_index = key_index + len(KEY_NAME) + 2 + value_end = file_data.index(b"\0\0", value_index) + 1 + value = file_data[value_index:value_end].decode("utf-16-le") + + return int(GET_BUILD_FROM_PRODUCTVERSION.match(value).group(1)) + +HEX_PAIR = "(?:[\\dabcdefABCDEF?]{2} ?)" + +STRIP_OFFSETS = re.compile(f'(!?)\\((?:\\d|call), "({HEX_PAIR}+)"\\)') +STRIP_COMMENTS = re.compile(r"\/*.+?\*\?|//.*") +SIGNATURE_DECLARATION = re.compile( + r"([\w_]+?) => (universal_signature|version_dependent_signature)!\((.+?)\)" +) +VERSION_DEPENDANT_RANGES = re.compile(f'([\\d.]+?) => "({HEX_PAIR}+)"') + + +def dump_signatures(data, prologue, version): + ret = {} + if prologue not in data: + print_stderr( + "signatures! macro call not found. Is the file badly written or is the script outdated?" + ) + exit(1) + + signatures_start = data.index(prologue) + len(prologue) + signatures_end = data.index("}", signatures_start) + + signatures = data[signatures_start:signatures_end] + + # the pattern also strips parenthesis, which is bad if the offset is directly within a macro call, so check for the exclamation mark and don't strip parenthesis if it has one. + signatures = STRIP_OFFSETS.sub( + lambda m: f'!("{m.group(2)}")' if m.group(1) else f'"{m.group(2)}"', signatures + ) + signatures = STRIP_COMMENTS.sub("", signatures) + signatures = signatures.replace("\n", "") + + for sig in SIGNATURE_DECLARATION.finditer(signatures): + match sig.group(2): + case "universal_signature": + ret.update({sig.group(1): sig.group(3).replace('"', "")}) + case "version_dependent_signature": + for version_range in VERSION_DEPENDANT_RANGES.finditer(sig.group(3)): + version_specifier = version_range.group(1) + if version_specifier.startswith(".."): + if version < int(version_specifier[2:]): + ret.update({sig.group(1): version_range.group(2)}) + break + elif version_specifier.endswith(".."): + if version >= int(version_specifier[0:-2]): + ret.update({sig.group(1): version_range.group(2)}) + break + + # if ".." isn't in the version specifier, assume it's something like `1234 => "AB CD EF"` + if ".." not in version_specifier: + if int(version_specifier) == version: + ret.update({sig.group(1): version_range.group(2)}) + break + + # at last, assume it's something like `1234..5678 => "AB CD EF"` + version_start, version_end = version_specifier.split("..") + if version >= int(version_start) and version < int(version_end): + ret.update({sig.group(1): version_range.group(2)}) + break + return ret + +if len(argv) < 3: + print_stderr(f"Usage: {argv[0]} [path to auxtools repo] [path to byondcode/libbyond]") + exit(1) + +auxtools_main_lib_path = argv[1] + "/auxtools/src/lib.rs" +auxtools_instruction_hooking_path = argv[1] + "/instruction_hooking/src/lib.rs" + +try: + auxtools_main_lib = open(auxtools_main_lib_path, "r").read() + auxtools_instruction_hooking = open(auxtools_instruction_hooking_path, "r").read() + byondlib = open(argv[2], "rb", buffering=16 * 1024) +except Exception as e: + print_stderr(f"Failure opening files: {e}\n") + exit(1) + + +platform = determine_platform(byondlib) +version = determine_version(byondlib) + +prelude = f"#[cfg({platform})]\nsignatures!" + +sigs = dump_signatures(auxtools_main_lib, prelude, version) +sigs.update(dump_signatures(auxtools_instruction_hooking, prelude, version)) + +byondlib.seek(0) +lib_data = byondlib.read() + +ret = {} + +for i, signature_name in enumerate(sigs): + signature_bytes = [] + + for text_byte in sigs[signature_name].split(" "): + if text_byte == "??": + signature_bytes.append(0x1337) + else: + signature_bytes.append(int(text_byte, base=16)) + + match_count = 0 + full_match = False + + i = 0 + for byte in lib_data: + if byte == signature_bytes[match_count] or signature_bytes[match_count] == 0x1337: + match_count += 1 + + if match_count >= len(signature_bytes): + if full_match: + ret.update({signature_name: "ERR(MULTIPLE MATCHES)"}) + break + + full_match = True + match_count = 0 + else: + match_count = 0 + i += 1 + + if not full_match: + ret.update({signature_name: "ERR(NO MATCH)"}) + +print(ret) diff --git a/debug_server/Cargo.toml b/debug_server/Cargo.toml index 46588e81..37079b8d 100644 --- a/debug_server/Cargo.toml +++ b/debug_server/Cargo.toml @@ -21,6 +21,7 @@ clap = "3" dmasm = { workspace = true } region = "3" retour = { workspace = true } +log = { version = "0.4", features = [ "release_max_level_off" ] } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winuser", "libloaderapi", "errhandlingapi"] } diff --git a/debug_server/src/instruction_hooking.rs b/debug_server/src/instruction_hooking.rs index e1834ed0..2cb7b459 100644 --- a/debug_server/src/instruction_hooking.rs +++ b/debug_server/src/instruction_hooking.rs @@ -108,6 +108,7 @@ fn get_proc_ctx(stack_id: u32) -> *mut raw_types::procs::ExecutionContext { } fn handle_breakpoint(ctx: *mut raw_types::procs::ExecutionContext, reason: BreakpointReason) -> DebuggerAction { + info!("Handling breakpoint ({reason:?})"); let action = unsafe { match &mut *DEBUG_SERVER.get() { Some(server) => server.handle_breakpoint(ctx, reason), @@ -355,8 +356,10 @@ fn find_instruction<'a>(env: &'a mut DisassembleEnv, proc: &'a Proc, offset: u32 } pub fn hook_instruction(proc: &Proc, offset: u32) -> Result<(), InstructionHookError> { + info!(target: "debug_server", "Debug Server: Hooking instruction ({}:{offset})", proc.path); let mut env = disassemble_env::DisassembleEnv; let (_, debug) = find_instruction(&mut env, proc, offset).ok_or(InstructionHookError::InvalidOffset)?; + info!("Debug Server: Found instruction: {debug:?}"); let instruction_length = debug.bytecode.len(); diff --git a/debug_server/src/server.rs b/debug_server/src/server.rs index 41883e34..bc9084c4 100644 --- a/debug_server/src/server.rs +++ b/debug_server/src/server.rs @@ -963,6 +963,7 @@ impl Server { // returns true if we need to break fn handle_request(&mut self, request: Request) -> bool { + info!("Debug Server: Received request ({request:?})"); match request { Request::Disconnect => unreachable!(), Request::CatchRuntimes { should_catch } => self.should_catch_runtimes = should_catch, diff --git a/instruction_hooking/build.rs b/instruction_hooking/build.rs index e369d6ee..f115ba05 100644 --- a/instruction_hooking/build.rs +++ b/instruction_hooking/build.rs @@ -1,15 +1,33 @@ use std::env; fn main() { + #[cfg(unix)] + { + println!("cargo:rerun-if-changed=src/execute_instruction_hook.unix.S"); + println!("cargo:rerun-if-changed=src/execute_instruction_hook_516.unix.S"); + } + + #[cfg(all(windows, target_env = "gnu"))] + { + println!("cargo:rerun-if-changed=src/execute_instruction_hook.windows.S"); + println!("cargo:rerun-if-changed=src/514/execute_instruction_hook.windows.S"); + } + + #[cfg(all(windows, target_env = "msvc"))] + { + println!("cargo:rerun-if-changed=src/execute_instruction_hook.windows.asm"); + println!("cargo:rerun-if-changed=src/514/execute_instruction_hook.windows.asm"); + } + let target_family = env::var("CARGO_CFG_TARGET_FAMILY").expect("CARGO_CFG_TARGET_FAMILY not set"); let target_env = env::var("CARGO_CFG_TARGET_ENV").expect("CARGO_CFG_TARGET_ENV not set"); let mut build = cc::Build::new(); - build.cpp(true).file("src/execute_instruction_data.cpp"); match target_family.as_str() { "unix" => { build.file("src/execute_instruction_hook.unix.S"); + build.file("src/execute_instruction_hook_516.unix.S"); } "windows" => match target_env.as_str() { "gnu" => { diff --git a/instruction_hooking/src/execute_instruction_data.cpp b/instruction_hooking/src/execute_instruction_data.cpp deleted file mode 100644 index faa71bae..00000000 --- a/instruction_hooking/src/execute_instruction_data.cpp +++ /dev/null @@ -1,4 +0,0 @@ -// TODO: Move to rust -extern "C" { - void* execute_instruction_original = nullptr; -} diff --git a/instruction_hooking/src/execute_instruction_hook_516.unix.S b/instruction_hooking/src/execute_instruction_hook_516.unix.S new file mode 100644 index 00000000..403856c3 --- /dev/null +++ b/instruction_hooking/src/execute_instruction_hook_516.unix.S @@ -0,0 +1,35 @@ +// for i686-unknown-linux-gnu +.intel_syntax noprefix +.global execute_instruction_hook_516 +.extern execute_instruction_original +.extern handle_instruction + +// this just stores some pretty important registers before calling the actual function +execute_instruction_hook_516: + //SUB ESP, 0x04 + PUSH EAX + PUSH EBX + PUSH ECX + PUSH EDX + PUSH EBP + + MOV EBP, ESP + AND ESP, 0xfffffff0 // clear last 4 bits, align for 16-bytes + SUB ESP, 0x10 + MOVDQA [ESP], XMM0 + SUB ESP, 0x10 + + CALL handle_instruction + + MOVDQA XMM0, [ESP + 0x10] + MOV ESP, EBP + + POP EBP + POP EDX + POP ECX + POP EBX + POP EAX + //ADD ESP, 0x04 + + MOV EDI, execute_instruction_original // EDI is overwritten before read again, it can be used as a volatile register + JMP EDI diff --git a/instruction_hooking/src/lib.rs b/instruction_hooking/src/lib.rs index e60f1449..87aab3e9 100644 --- a/instruction_hooking/src/lib.rs +++ b/instruction_hooking/src/lib.rs @@ -1,6 +1,6 @@ pub mod disassemble_env; -use auxtools::*; +use auxtools::{*, raw_types::funcs::CURRENT_EXECUTION_CONTEXT}; use retour::RawDetour; use std::{any::Any, cell::UnsafeCell, ffi::c_void}; @@ -16,6 +16,7 @@ signatures! { #[cfg(unix)] signatures! { execute_instruction => version_dependent_signature!( + 1647.. => "0F B7 46 ?? 8B 56 ?? 66 89 ?? ?? ?? ?? ?? 89 85 ?? ?? ?? ??", 1616.. => "0F B7 C0 8D 14 ?? 8B 02 8B 52 ?? 8B 4E ?? 8B 5E ?? 89 46 ?? 89 56 ?? 89 0C 24", ..1616 => "0F B7 47 ?? 8B 57 ?? 0F B7 D8 8B 0C ?? 81 F9 ?? ?? 00 00 77 ?? FF 24 8D ?? ?? ?? ??" ) @@ -38,9 +39,11 @@ pub trait InstructionHook: InstructionHookToAny { pub static mut INSTRUCTION_HOOKS: UnsafeCell>> = UnsafeCell::new(Vec::new()); +#[unsafe(no_mangle)] +pub static mut execute_instruction_original: *const c_void = std::ptr::null(); + extern "C" { // Trampoline to the original un-hooked BYOND execute_instruction code - static mut execute_instruction_original: *const c_void; // Our version of execute_instruction. It hasn't got a calling convention rust // knows about, so don't call it. @@ -49,6 +52,10 @@ extern "C" { // The 514 version of the instruction hook. #[cfg(windows)] fn execute_instruction_hook_514(); + + // 516 has a different call convention + #[cfg(unix)] + fn execute_instruction_hook_516(); } #[init(full)] @@ -60,13 +67,17 @@ fn instruction_hooking_init() -> Result<(), String> { } #[cfg(windows)] - let versioned_hook = if auxtools::version::get().0 == 514 { + let versioned_hook = if auxtools::version::get().major == 514 { execute_instruction_hook_514 as *const () } else { execute_instruction_hook as *const () }; #[cfg(unix)] - let versioned_hook = execute_instruction_hook as *const (); + let versioned_hook = if auxtools::version::get().build >= 1647 { + execute_instruction_hook_516 as *const () + } else { + execute_instruction_hook as *const () + }; unsafe { let hook = RawDetour::new(execute_instruction as *const (), versioned_hook).map_err(|_| "Couldn't detour execute_instruction")?; @@ -93,7 +104,13 @@ fn instruction_hooking_shutdown() { // This function has to leave `*CURRENT_EXECUTION_CONTEXT` in EAX, so make sure // to return it. #[no_mangle] -extern "C" fn handle_instruction(ctx: *mut raw_types::procs::ExecutionContext) -> *const raw_types::procs::ExecutionContext { +extern "C" fn handle_instruction() -> *const raw_types::procs::ExecutionContext { + let ctx = unsafe { *CURRENT_EXECUTION_CONTEXT }; + if ctx.is_null() { + error!("Cannot handle instruction, CURRENT_EXECUTION_CONTEXT is null."); + return ctx + } + unsafe { for vec_box in &mut *INSTRUCTION_HOOKS.get() { vec_box.handle_instruction(ctx); diff --git a/tests/auxtest/src/ffi_tests.rs b/tests/auxtest/src/ffi_tests.rs new file mode 100644 index 00000000..7000bf09 --- /dev/null +++ b/tests/auxtest/src/ffi_tests.rs @@ -0,0 +1,334 @@ +use std::{cell::RefCell, collections::HashMap, convert::TryFrom, ffi::CString, os::raw::c_char}; + +use auxtools::*; + +fn test_result(name: &str, test: impl FnOnce() -> Result<(), Runtime>) -> Option { + match test() { + Ok(()) => Some("SUCCESS".to_owned()), + Err(err) => Some(format!("FAILED: {}: {}", name, err.message)), + } +} + +thread_local! { + static RETURN_STRING: RefCell = RefCell::new(CString::default()); +} + +fn byond_return(value: Option) -> *const c_char { + RETURN_STRING.with(|cell| { + cell.replace(CString::new(value.unwrap_or_default()).unwrap_or_default()); + cell.borrow().as_ptr() + }) +} + +macro_rules! ffi_test { + ($name:ident, $body:block) => { + #[no_mangle] + extern "C" fn $name(_argc: std::os::raw::c_int, _argv: *const *const c_char) -> *const c_char { + byond_return($body) + } + }; +} + +ffi_test! { auxtest_ffi_runtime_globals, { + test_result("ffi_runtime_globals", || unsafe { + if raw_types::funcs::CURRENT_EXECUTION_CONTEXT.is_null() { + return Err(runtime!("CURRENT_EXECUTION_CONTEXT is null")); + } + + if (*raw_types::funcs::CURRENT_EXECUTION_CONTEXT).is_null() { + return Err(runtime!("current execution context is null")); + } + + if raw_types::funcs::SUSPENDED_PROCS.is_null() { + return Err(runtime!("SUSPENDED_PROCS is null")); + } + + if raw_types::funcs::SUSPENDED_PROCS_BUFFER.is_null() { + return Err(runtime!("SUSPENDED_PROCS_BUFFER is null")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_string_id_and_entry, { + test_result("ffi_string_id_and_entry", || unsafe { + use raw_types::{funcs, strings}; + + let contents = CString::new("relatively unique testing string").unwrap(); + let mut id = strings::StringId(0); + let mut entry: *mut strings::StringEntry = std::ptr::null_mut(); + + if funcs::get_string_id(&mut id, contents.as_ptr()) != 1 { + return Err(runtime!("get_string_id failed")); + } + + if funcs::get_string_table_entry(&mut entry, id) != 1 { + return Err(runtime!("get_string_table_entry failed")); + } + + if entry.is_null() { + return Err(runtime!("string entry is null")); + } + + if std::ffi::CStr::from_ptr((*entry).data).to_bytes() != contents.as_bytes() { + return Err(runtime!("string table entry data mismatch")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_string_value_refcount, { + test_result("ffi_string_value_refcount", || unsafe { + use raw_types::{funcs, strings, values}; + + let contents = CString::new("string refcount test").unwrap(); + let mut id = strings::StringId(0); + let mut entry: *mut strings::StringEntry = std::ptr::null_mut(); + + assert_eq!(funcs::get_string_id(&mut id, contents.as_ptr()), 1); + assert_eq!(funcs::get_string_table_entry(&mut entry, id), 1); + + if (*entry).ref_count != 0 { + return Err(runtime!("new string refcount != 0")); + } + + { + let value = Value::new(values::ValueTag::String, values::ValueData { string: id }); + if (*entry).ref_count != 1 { + return Err(runtime!("Value::new did not increment refcount")); + } + drop(value); + } + + if (*entry).ref_count != 0 { + return Err(runtime!("Value drop did not decrement refcount")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_string_value_roundtrip, { + test_result("ffi_string_value_roundtrip", || { + let value = Value::from_string("roundtrip string")?; + if value.as_string()? != "roundtrip string" { + return Err(runtime!("string did not roundtrip")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_proc_find, { + test_result("ffi_proc_find", || { + let proc = Proc::find("/proc/concat_strings").ok_or_else(|| runtime!("/proc/concat_strings not found"))?; + + if proc.path != "/concat_strings" { + return Err(runtime!("unexpected proc path {}", proc.path)); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_proc_call_global, { + test_result("ffi_proc_call_global", || { + let value_a = Value::from_string("relatively unique testing string")?; + let value_b = Value::from_string("another string that should be unique")?; + let concatenated = Proc::find("/proc/concat_strings") + .ok_or_else(|| runtime!("/proc/concat_strings not found"))? + .call(&[&value_a, &value_b])?; + + let expected = "relatively unique testing stringanother string that should be unique"; + let actual = concatenated.as_string()?; + if actual != expected { + return Err(runtime!("expected {:?}, got {:?}", expected, actual)); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_proc_metadata, { + test_result("ffi_proc_metadata", || { + let proc = Proc::find("/proc/auxtest_proc_metadata_subject") + .ok_or_else(|| runtime!("/proc/auxtest_proc_metadata_subject not found"))?; + + let parameter_names = proc + .parameter_names() + .into_iter() + .map(String::from) + .collect::>(); + if parameter_names != ["alpha", "beta"] { + return Err(runtime!("unexpected parameter names {:?}", parameter_names)); + } + + let local_names = proc.local_names().into_iter().map(String::from).collect::>(); + if !local_names.iter().any(|name| name == "local_one") { + return Err(runtime!("local_one not found in {:?}", local_names)); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_list_create_empty, { + test_result("ffi_list_create_empty", || { + let list = List::new(); + if !list.is_empty() { + return Err(runtime!("list len != 0")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_list_append_len, { + test_result("ffi_list_append_len", || { + let list = List::new(); + list.append(Value::from(101)); + list.append(Value::from(102)); + list.append(Value::from(103)); + + if list.len() != 3 { + return Err(runtime!("list len != 3")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_list_index_get, { + test_result("ffi_list_index_get", || { + let list = List::new(); + list.append(Value::from(101)); + list.append(Value::from(102)); + + if list.get(2)?.as_number()? != 102.0 { + return Err(runtime!("list[2] != 102")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_list_assoc_set_get, { + test_result("ffi_list_assoc_set_get", || { + let list = List::new(); + let key = Value::from_string("key")?; + let value = Value::from_string("value")?; + list.set(&key, &value)?; + + if list.get(&key)?.as_string()? != "value" { + return Err(runtime!("list[key] != value")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_list_remove, { + test_result("ffi_list_remove", || { + let list = List::new(); + list.append(Value::from(101)); + list.append(Value::from(102)); + list.append(Value::from(103)); + list.remove(Value::from(102)); + + if list.get(2)?.as_number()? != 103.0 { + return Err(runtime!("list[2] != 103 after remove")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_list_with_size, { + test_result("ffi_list_with_size", || { + let list = List::with_size(6); + + if list.len() != 6 { + return Err(runtime!("list len != 6")); + } + + for n in 1..=6 { + if list.get(n)? != Value::NULL { + return Err(runtime!("list[{}] != null", n)); + } + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_value_from_number, { + test_result("ffi_value_from_number", || { + let value = Value::from(30); + if value.as_number()? != 30.0 { + return Err(runtime!("Value failed to convert i32")); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_value_from_vec, { + test_result("ffi_value_from_vec", || { + let vector: Vec = vec![5.into()]; + let value = Value::from(&vector); + let list = List::from_value(&value)?; + if list.len() != 1 { + return Err(runtime!("Vec with one entry did not produce len 1")); + } + + let value = list.get(1)?.as_number()?; + if value != 5.0 { + return Err(runtime!("list[1] was {} instead of 5", value)); + } + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_value_from_hashmap_value_key, { + test_result("ffi_value_from_hashmap_value_key", || { + let mut hashmap: HashMap = HashMap::new(); + hashmap.insert(Value::from_string("meow")?, 1.into()); + let value = Value::try_from(&hashmap)?; + assert_meow_equals_one(value)?; + + Ok(()) + }) +} } + +ffi_test! { auxtest_ffi_value_from_hashmap_string_key, { + test_result("ffi_value_from_hashmap_string_key", || { + let mut hashmap: HashMap = HashMap::new(); + hashmap.insert("meow".to_owned(), 1.into()); + let value = Value::try_from(&hashmap)?; + assert_meow_equals_one(value)?; + + Ok(()) + }) +} } + +fn assert_meow_equals_one(value: Value) -> Result<(), Runtime> { + match value.raw.tag { + raw_types::values::ValueTag::List => (), + _ => return Err(runtime!("Hashmap became a {:?} instead of a list", value.raw.tag)), + } + + let list = List::from_value(&value)?; + if list.len() != 1 { + return Err(runtime!("Hashmap with one key did not produce len 1")); + } + + let value = list.get(Value::from_string("meow")?)?.as_number()?; + if value != 1.0 { + return Err(runtime!("list[meow] was {} instead of 1", value)); + } + + Ok(()) +} diff --git a/tests/auxtest/src/lib.rs b/tests/auxtest/src/lib.rs index eda00c56..3a483039 100644 --- a/tests/auxtest/src/lib.rs +++ b/tests/auxtest/src/lib.rs @@ -1,10 +1,42 @@ use auxtools::*; +use std::io::Write; +mod ffi_tests; mod lists; +mod procs; mod strings; mod value_from; +mod variables; mod weak; +#[hook("/proc/auxtest_hook_basic")] +fn hook_basic() { + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_runtime_globals")] +fn runtime_globals() { + unsafe { + if raw_types::funcs::CURRENT_EXECUTION_CONTEXT.is_null() { + return Err(runtime!("runtime_globals: CURRENT_EXECUTION_CONTEXT is null")); + } + + if (*raw_types::funcs::CURRENT_EXECUTION_CONTEXT).is_null() { + return Err(runtime!("runtime_globals: current execution context is null")); + } + + if raw_types::funcs::SUSPENDED_PROCS.is_null() { + return Err(runtime!("runtime_globals: SUSPENDED_PROCS is null")); + } + + if raw_types::funcs::SUSPENDED_PROCS_BUFFER.is_null() { + return Err(runtime!("runtime_globals: SUSPENDED_PROCS_BUFFER is null")); + } + } + + Ok(Value::from(true)) +} + #[hook("/proc/auxtest_inc_counter")] fn inc_counter() { static mut COUNTER: u32 = 0; @@ -17,6 +49,12 @@ fn inc_counter() { #[hook("/proc/auxtest_out")] fn out(msg: Value) { - eprintln!("\n{}", msg.as_string()?); + let msg = msg.as_string()?; + eprintln!("\n{}", msg); + if let Some(path) = std::env::var_os("AUXTEST_OUT") { + if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{}", msg); + } + } Ok(Value::NULL) } diff --git a/tests/auxtest/src/lists.rs b/tests/auxtest/src/lists.rs index 4160a735..aa37491c 100644 --- a/tests/auxtest/src/lists.rs +++ b/tests/auxtest/src/lists.rs @@ -54,3 +54,84 @@ fn test_lists() { Ok(Value::from(true)) } + +#[hook("/proc/auxtest_list_create_empty")] +fn list_create_empty() { + let list = List::new(); + if !list.is_empty() { + return Err(runtime!("list_create_empty: list len != 0")); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_list_append_len")] +fn list_append_len() { + let list = List::new(); + list.append(Value::from(101)); + list.append(Value::from(102)); + list.append(Value::from(103)); + + if list.len() != 3 { + return Err(runtime!("list_append_len: list len != 3")); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_list_index_get")] +fn list_index_get() { + let list = List::new(); + list.append(Value::from(101)); + list.append(Value::from(102)); + + if list.get(2)?.as_number()? != 102.0 { + return Err(runtime!("list_index_get: list[2] != 102")); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_list_assoc_set_get")] +fn list_assoc_set_get() { + let list = List::new(); + list.set(byond_string!("key"), byond_string!("value"))?; + + if list.get(byond_string!("key"))?.as_string()? != "value" { + return Err(runtime!("list_assoc_set_get: list[key] != value")); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_list_remove")] +fn list_remove() { + let list = List::new(); + list.append(Value::from(101)); + list.append(Value::from(102)); + list.append(Value::from(103)); + list.remove(Value::from(102)); + + if list.get(2)?.as_number()? != 103.0 { + return Err(runtime!("list_remove: list[2] != 103 after remove")); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_list_with_size")] +fn list_with_size() { + let list = List::with_size(6); + + if list.len() != 6 { + return Err(runtime!("list_with_size: list len != 6")); + } + + for n in 1..=6 { + if list.get(n)? != Value::NULL { + return Err(runtime!("list_with_size: list[{}] != null", n)); + } + } + + Ok(Value::from(true)) +} diff --git a/tests/auxtest/src/procs.rs b/tests/auxtest/src/procs.rs new file mode 100644 index 00000000..08d44b50 --- /dev/null +++ b/tests/auxtest/src/procs.rs @@ -0,0 +1,47 @@ +use auxtools::*; + +#[hook("/proc/auxtest_proc_find")] +fn proc_find() { + let proc = Proc::find("/proc/concat_strings").ok_or_else(|| runtime!("proc_find: /proc/concat_strings not found"))?; + + if proc.path != "/concat_strings" { + return Err(runtime!("proc_find: unexpected proc path {}", proc.path)); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_proc_call_global")] +fn proc_call_global() { + let value_a = Value::from_string("relatively unique testing string")?; + let value_b = Value::from_string("another string that should be unique")?; + let concatenated = Proc::find("/proc/concat_strings") + .ok_or_else(|| runtime!("proc_call_global: /proc/concat_strings not found"))? + .call(&[&value_a, &value_b])?; + + let expected = "relatively unique testing stringanother string that should be unique"; + let actual = concatenated.as_string()?; + if actual != expected { + return Err(runtime!("proc_call_global: expected {:?}, got {:?}", expected, actual)); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_proc_metadata")] +fn proc_metadata() { + let proc = + Proc::find("/proc/auxtest_proc_metadata_subject").ok_or_else(|| runtime!("proc_metadata: /proc/auxtest_proc_metadata_subject not found"))?; + + let parameter_names = proc.parameter_names().into_iter().map(String::from).collect::>(); + if parameter_names != ["alpha", "beta"] { + return Err(runtime!("proc_metadata: unexpected parameter names {:?}", parameter_names)); + } + + let local_names = proc.local_names().into_iter().map(String::from).collect::>(); + if !local_names.iter().any(|name| name == "local_one") { + return Err(runtime!("proc_metadata: local_one not found in {:?}", local_names)); + } + + Ok(Value::from(true)) +} diff --git a/tests/auxtest/src/strings.rs b/tests/auxtest/src/strings.rs index fc5ca5f3..3bceb84a 100644 --- a/tests/auxtest/src/strings.rs +++ b/tests/auxtest/src/strings.rs @@ -79,3 +79,74 @@ fn test_strings() { Ok(Value::from(true)) } } + +#[hook("/proc/auxtest_string_id_and_entry")] +fn string_id_and_entry() { + use raw_types::{funcs, strings}; + + unsafe { + let contents = CString::new("relatively unique testing string").unwrap(); + let mut id = strings::StringId(0); + let mut entry: *mut strings::StringEntry = std::ptr::null_mut(); + + if funcs::get_string_id(&mut id, contents.as_ptr()) != 1 { + return Err(runtime!("string_id_and_entry: get_string_id failed")); + } + + if funcs::get_string_table_entry(&mut entry, id) != 1 { + return Err(runtime!("string_id_and_entry: get_string_table_entry failed")); + } + + if entry.is_null() { + return Err(runtime!("string_id_and_entry: string entry is null")); + } + + if std::ffi::CStr::from_ptr((*entry).data).to_bytes() != contents.as_bytes() { + return Err(runtime!("string_id_and_entry: string table entry data mismatch")); + } + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_string_value_refcount")] +fn string_value_refcount() { + use raw_types::{funcs, strings, values}; + + unsafe { + let contents = CString::new("string refcount test").unwrap(); + let mut id = strings::StringId(0); + let mut entry: *mut strings::StringEntry = std::ptr::null_mut(); + + assert_eq!(funcs::get_string_id(&mut id, contents.as_ptr()), 1); + assert_eq!(funcs::get_string_table_entry(&mut entry, id), 1); + + if (*entry).ref_count != 0 { + return Err(runtime!("string_value_refcount: new string refcount != 0")); + } + + { + let value = Value::new(values::ValueTag::String, values::ValueData { string: id }); + if (*entry).ref_count != 1 { + return Err(runtime!("string_value_refcount: Value::new did not increment refcount")); + } + drop(value); + } + + if (*entry).ref_count != 0 { + return Err(runtime!("string_value_refcount: Value drop did not decrement refcount")); + } + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_string_value_roundtrip")] +fn string_value_roundtrip() { + let value = Value::from_string("roundtrip string")?; + if value.as_string()? != "roundtrip string" { + return Err(runtime!("string_value_roundtrip: string did not roundtrip")); + } + + Ok(Value::from(true)) +} diff --git a/tests/auxtest/src/value_from.rs b/tests/auxtest/src/value_from.rs index 0ddc4f29..c417e75b 100644 --- a/tests/auxtest/src/value_from.rs +++ b/tests/auxtest/src/value_from.rs @@ -71,3 +71,50 @@ fn assert_meow_equals_one(value: Value) -> Result<(), Runtime> { Ok(()) } + +#[hook("/proc/auxtest_value_from_number")] +fn value_from_number() { + let value = Value::from(30); + if value.as_number()? != 30.0 { + return Err(runtime!("value_from_number: Value failed to convert i32")); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_value_from_vec")] +fn value_from_vec() { + let vector: Vec = vec![5.into()]; + let value = Value::from(&vector); + let list = List::from_value(&value)?; + if list.len() != 1 { + return Err(runtime!("value_from_vec: Vec with one entry did not produce len 1")); + } + + let value = list.get(1)?.as_number()?; + if value != 5.0 { + return Err(runtime!("value_from_vec: list[1] was {} instead of 5", value)); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_value_from_hashmap_value_key")] +fn value_from_hashmap_value_key() { + let mut hashmap: HashMap = HashMap::new(); + hashmap.insert(Value::from_string("meow")?, 1.into()); + let value = Value::try_from(&hashmap)?; + assert_meow_equals_one(value)?; + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_value_from_hashmap_string_key")] +fn value_from_hashmap_string_key() { + let mut hashmap: HashMap = HashMap::new(); + hashmap.insert("meow".to_owned(), 1.into()); + let value = Value::try_from(&hashmap)?; + assert_meow_equals_one(value)?; + + Ok(Value::from(true)) +} diff --git a/tests/auxtest/src/variables.rs b/tests/auxtest/src/variables.rs new file mode 100644 index 00000000..f91c2ca2 --- /dev/null +++ b/tests/auxtest/src/variables.rs @@ -0,0 +1,22 @@ +use auxtools::*; + +#[hook("/proc/auxtest_variables_get_set")] +fn variables_get_set(datum: Value) { + datum.set(byond_string!("auxtest_number"), Value::from(42))?; + + if datum.get_number(byond_string!("auxtest_number"))? != 42.0 { + return Err(runtime!("variables_get_set: datum.auxtest_number != 42")); + } + + Ok(Value::from(true)) +} + +#[hook("/proc/auxtest_value_to_string")] +fn value_to_string(datum: Value) { + let string = datum.to_string()?; + if string.is_empty() { + return Err(runtime!("value_to_string: datum string was empty")); + } + + Ok(Value::from(true)) +} diff --git a/tests/auxtest_host/auxtest_host.dm b/tests/auxtest_host/auxtest_host.dm index e1b90976..3680ea17 100644 --- a/tests/auxtest_host/auxtest_host.dm +++ b/tests/auxtest_host/auxtest_host.dm @@ -4,16 +4,29 @@ /datum var/__auxtools_weakref_id + var/auxtest_number /proc/auxtools_test_dll() . = world.GetConfig("env", "AUXTEST_DLL") - + /proc/auxtools_stack_trace(msg) CRASH(msg) /proc/auxtest_out() // Graceful failure +/proc/auxtest_marker(msg) + world.log << msg + auxtest_out(msg) + +/proc/auxtest_run_ffi(auxtest_dll, name) + auxtest_marker("START: " + name) + var/result = call_ext(auxtest_dll, "auxtest_ffi_" + name)() + if (result != "SUCCESS") + auxtest_marker("FAILED: " + name + ": " + result) + CRASH(name) + auxtest_marker("PASS: " + name) + /proc/auxtest_inc_counter() CRASH() @@ -30,6 +43,13 @@ var/datum/weak_test_datum /proc/create_datum_for_weak() weak_test_datum = new +/proc/auxtest_proc_metadata_subject(alpha, beta) + var/local_one = alpha + return beta || local_one + +#define RUN_AUXTEST(NAME, EXPR) auxtest_marker("START: " + NAME); if ((EXPR) != TRUE) { auxtest_marker("FAILED: " + NAME); CRASH(NAME) }; auxtest_marker("PASS: " + NAME) +#define RUN_AUXTEST_FFI(NAME) auxtest_run_ffi(auxtest_dll, NAME) + // Tests /proc/auxtest_lists() CRASH() @@ -43,19 +63,125 @@ var/datum/weak_test_datum /proc/auxtest_value_from() CRASH() +/proc/auxtest_hook_basic() + CRASH() + +/proc/auxtest_runtime_globals() + CRASH() + +/proc/auxtest_string_id_and_entry() + CRASH() + +/proc/auxtest_string_value_refcount() + CRASH() + +/proc/auxtest_string_value_roundtrip() + CRASH() + +/proc/auxtest_proc_find() + CRASH() + +/proc/auxtest_proc_call_global() + CRASH() + +/proc/auxtest_proc_metadata() + CRASH() + +/proc/auxtest_variables_get_set() + CRASH() + +/proc/auxtest_value_to_string() + CRASH() + +/proc/auxtest_list_create_empty() + CRASH() + +/proc/auxtest_list_append_len() + CRASH() + +/proc/auxtest_list_index_get() + CRASH() + +/proc/auxtest_list_assoc_set_get() + CRASH() + +/proc/auxtest_list_remove() + CRASH() + +/proc/auxtest_list_with_size() + CRASH() + +/proc/auxtest_value_from_number() + CRASH() + +/proc/auxtest_value_from_vec() + CRASH() + +/proc/auxtest_value_from_hashmap_value_key() + CRASH() + +/proc/auxtest_value_from_hashmap_string_key() + CRASH() + /proc/do_tests() + world.log << "auxtest: do_tests start" var/auxtest_dll = auxtools_test_dll() + world.log << "auxtest: dll=[auxtest_dll]" + var/signature_result = call_ext(auxtest_dll, "auxtools_check_signatures")() + world.log << "signature_result = [signature_result]" + ASSERT(signature_result == "SUCCESS") + world.log << "auxtest: before auxtools_init" var/init_result = call_ext(auxtest_dll, "auxtools_init")() + world.log << "auxtest: after auxtools_init" world.log << "init_result = [init_result]" ASSERT(init_result == "SUCCESS") - // Tests - ASSERT(auxtest_lists() == TRUE) - ASSERT(auxtest_strings() == TRUE) - ASSERT(auxtest_value_from() == TRUE) + // These tests use call_ext instead of hooks, so broken hook dispatch does not + // hide lower-level signature, structure, and wrapper failures. + RUN_AUXTEST_FFI("runtime_globals") + RUN_AUXTEST_FFI("string_id_and_entry") + RUN_AUXTEST_FFI("string_value_refcount") + RUN_AUXTEST_FFI("string_value_roundtrip") + RUN_AUXTEST_FFI("proc_find") + RUN_AUXTEST_FFI("proc_metadata") + RUN_AUXTEST_FFI("list_create_empty") + RUN_AUXTEST_FFI("list_append_len") + RUN_AUXTEST_FFI("list_index_get") + RUN_AUXTEST_FFI("list_assoc_set_get") + RUN_AUXTEST_FFI("list_remove") + RUN_AUXTEST_FFI("list_with_size") + RUN_AUXTEST_FFI("value_from_number") + RUN_AUXTEST_FFI("value_from_vec") + RUN_AUXTEST_FFI("value_from_hashmap_value_key") + RUN_AUXTEST_FFI("value_from_hashmap_string_key") + RUN_AUXTEST_FFI("proc_call_global") + + // Hook tests are ordered so each case adds the smallest practical new dependency. + RUN_AUXTEST("hook_basic", auxtest_hook_basic()) + RUN_AUXTEST("runtime_globals", auxtest_runtime_globals()) + RUN_AUXTEST("string_id_and_entry", auxtest_string_id_and_entry()) + RUN_AUXTEST("string_value_refcount", auxtest_string_value_refcount()) + RUN_AUXTEST("string_value_roundtrip", auxtest_string_value_roundtrip()) + RUN_AUXTEST("proc_find", auxtest_proc_find()) + RUN_AUXTEST("proc_call_global", auxtest_proc_call_global()) + + var/datum/proc_test = new + RUN_AUXTEST("proc_metadata", auxtest_proc_metadata()) + RUN_AUXTEST("variables_get_set", auxtest_variables_get_set(proc_test)) + RUN_AUXTEST("value_to_string", auxtest_value_to_string(proc_test)) + RUN_AUXTEST("list_create_empty", auxtest_list_create_empty()) + RUN_AUXTEST("list_append_len", auxtest_list_append_len()) + RUN_AUXTEST("list_index_get", auxtest_list_index_get()) + RUN_AUXTEST("list_assoc_set_get", auxtest_list_assoc_set_get()) + RUN_AUXTEST("list_remove", auxtest_list_remove()) + RUN_AUXTEST("list_with_size", auxtest_list_with_size()) + RUN_AUXTEST("value_from_number", auxtest_value_from_number()) + RUN_AUXTEST("value_from_vec", auxtest_value_from_vec()) + RUN_AUXTEST("value_from_hashmap_value_key", auxtest_value_from_hashmap_value_key()) + RUN_AUXTEST("value_from_hashmap_string_key", auxtest_value_from_hashmap_string_key()) var/datum/weak_test = new - ASSERT(auxtest_weak_values(weak_test) == TRUE) + RUN_AUXTEST("weak_values", auxtest_weak_values(weak_test)) ASSERT(weak_test == null) // Stop testing after the 8th reboot diff --git a/tests/test_runner/src/main.rs b/tests/test_runner/src/main.rs index 813bd232..9761154d 100644 --- a/tests/test_runner/src/main.rs +++ b/tests/test_runner/src/main.rs @@ -1,6 +1,6 @@ mod paths; -use std::process::Command; +use std::{fs, process::Command}; trait ByondCommand { fn with_byond_paths(&mut self) -> &mut Self; @@ -13,17 +13,17 @@ impl ByondCommand for Command { let byond_system = paths::find_byond(); let byond_bin = paths::find_byond_bin(); + let old_path = std::env::var_os("PATH").unwrap_or_default(); let path = format!( "{}:{}", byond_bin.as_os_str().to_str().unwrap(), - std::env::var_os("PATH").unwrap().to_str().unwrap() + old_path.to_str().unwrap() ); - let ld_library_path = format!( - "{}:{}", - byond_bin.as_os_str().to_str().unwrap(), - std::env::var_os("LD_LIBRARY_PATH").unwrap().to_str().unwrap() - ); + let ld_library_path = match std::env::var_os("LD_LIBRARY_PATH") { + Some(old) if !old.is_empty() => format!("{}:{}", byond_bin.as_os_str().to_str().unwrap(), old.to_str().unwrap()), + _ => byond_bin.as_os_str().to_str().unwrap().to_owned() + }; self.env("BYOND_SYSTEM", byond_system) .env("PATH", path) @@ -39,25 +39,70 @@ impl ByondCommand for Command { } fn main() { - let res = Command::new(paths::find_dm()).with_byond_paths().arg(paths::find_dme()).status().unwrap(); - assert!(res.success(), "dreamdaemon build failed"); + let dme = paths::find_dme(); + let dmb = paths::dmb_path(); + let dreamdaemon = paths::find_dreamdaemon(); + let auxtest = paths::find_dll(); + let auxtest_out = dmb.with_file_name("auxtest_output.log"); + let world_log = dmb.with_file_name("auxtest_host.txt"); + let _ = fs::remove_file(&dmb); + let _ = fs::remove_file(&auxtest_out); + let _ = fs::remove_file(&world_log); + + let dreammaker_output = Command::new(paths::find_dm()) + .with_byond_paths() + .arg(&dme) + .output() + .unwrap(); + assert!( + dreammaker_output.status.success(), + "dreammaker build failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&dreammaker_output.stdout), + String::from_utf8_lossy(&dreammaker_output.stderr) + ); + assert!(dmb.is_file(), "dreammaker did not produce {}", dmb.display()); + let dmb_metadata = fs::metadata(&dmb).unwrap(); // Here we depend on BYOND not fucking with stderr too much so we can hijack it // for our own communication - let output = Command::new(paths::find_dreamdaemon()) + let mut dreamdaemon_command = Command::new(&dreamdaemon); + dreamdaemon_command .with_byond_paths() - .env("AUXTEST_DLL", paths::find_dll()) - .arg(paths::find_dmb()) - .arg("-trusted") + .current_dir(dmb.parent().unwrap()) + .env("AUXTEST_DLL", &auxtest) + .env("AUXTEST_OUT", &auxtest_out) + .arg(&dmb) + .arg("0") .arg("-close") - .output() - .unwrap() - .stderr; + .arg("-trusted") + .arg("-log") + .arg("auxtest_host.txt"); + + #[cfg(windows)] + let (status, output) = { + let status = dreamdaemon_command.status().unwrap(); + (status, Vec::new()) + }; + + #[cfg(unix)] + let (status, output) = { + let output = dreamdaemon_command.output().unwrap(); + (output.status, [output.stdout, output.stderr].concat()) + }; + + let world_log = fs::read_to_string(&world_log).unwrap_or_default(); + let marker_log = fs::read(&auxtest_out).unwrap_or_default(); + let output = [output, marker_log].concat(); + let output_with_world_log = [output.clone(), world_log.as_bytes().to_vec()].concat(); let res = std::str::from_utf8(&output).unwrap(); + let res_with_world_log = std::str::from_utf8(&output_with_world_log).unwrap(); // Check for any messages matching "FAILED: " - let errors = res.lines().filter(|x| x.starts_with("FAILED: ")).collect::>(); + let errors = res_with_world_log + .lines() + .filter(|x| x.starts_with("FAILED: ")) + .collect::>(); if !errors.is_empty() { panic!("TESTS FAILED\n{}", errors.join("\n")); @@ -65,7 +110,27 @@ fn main() { // Now make sure we have only one message matching "SUCCESS: " let successes = res.lines().filter(|x| x.starts_with("SUCCESS: ")).collect::>(); - assert_eq!(successes.len(), 1, "Tests didn't output success message"); + assert_eq!( + successes.len(), + 1, + "Tests didn't output success message\ndreamdaemon status: {}\ndreamdaemon: {}\ndmb: {}\nauxtest: {}\noutput:\n{}\nworld log:\n{}", + status, + dreamdaemon.display(), + format_args!("{} ({} bytes)", dmb.display(), dmb_metadata.len()), + auxtest.display(), + res, + world_log + ); + assert!( + status.success() || successes.len() == 1, + "dreamdaemon failed with status {}\ndreamdaemon: {}\ndmb: {}\nauxtest: {}\noutput:\n{}\nworld log:\n{}", + status, + dreamdaemon.display(), + dmb.display(), + auxtest.display(), + res, + world_log + ); println!("Tests Succeeded"); } diff --git a/tests/test_runner/src/paths.rs b/tests/test_runner/src/paths.rs index 00179f29..0b971df2 100644 --- a/tests/test_runner/src/paths.rs +++ b/tests/test_runner/src/paths.rs @@ -45,10 +45,19 @@ pub fn find_dll() -> PathBuf { path.pop(); #[cfg(unix)] - path.push("libauxtest.so"); + let filename = "libauxtest.so"; #[cfg(windows)] - path.push("auxtest.dll"); + let filename = "auxtest.dll"; + + path.push(filename); + if path.is_file() { + return path; + } + + path.pop(); + path.push("deps"); + path.push(filename); assert!(path.is_file(), "couldn't find auxtest"); path @@ -61,9 +70,8 @@ pub fn find_dme() -> PathBuf { path } -pub fn find_dmb() -> PathBuf { +pub fn dmb_path() -> PathBuf { let mut path = std::env::current_dir().unwrap(); path.push("tests/auxtest_host/auxtest_host.dmb"); - assert!(path.is_file(), "couldn't find auxtest_host.dmb"); path } diff --git a/tools/fetch-changelogs.py b/tools/fetch-changelogs.py new file mode 100755 index 00000000..f4a31798 --- /dev/null +++ b/tools/fetch-changelogs.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import re +from html.parser import HTMLParser +from pathlib import Path +from urllib.request import Request, urlopen + + +DEFAULT_VERSIONS = ("515", "516") +BASE_URL = "https://www.byond.com/docs/notes/{version}.html" + + +class NotesMarkdown(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.out: list[str] = [] + self.buf: list[str] = [] + self.href: str | None = None + self.link: list[str] = [] + self.ul = 0 + self.li: list[dict[str, object]] = [] + self.bold = 0 + + def escape(self, text: str) -> str: + text = text.replace("\\", "\\\\") + text = re.sub(r"(? str: + return re.sub(r"\s+", " ", text).strip() + + def blank(self) -> None: + if self.out and self.out[-1] != "": + self.out.append("") + + def line(self, text: str) -> None: + if text: + self.out.append(text) + + def add(self, text: str) -> None: + if not text: + return + if self.href is not None: + self.link.append(text) + return + if self.bold: + text = f"**{text}**" + if self.li: + self.li[-1]["text"].append(text) # type: ignore[index, union-attr] + else: + self.buf.append(text) + + def flush_buf(self, trailing_blank: bool = False) -> None: + text = self.normalize("".join(self.buf)) + self.buf = [] + if text: + self.line(text) + if trailing_blank: + self.blank() + + def flush_li(self) -> None: + if not self.li: + return + item = self.li[-1] + text_parts = item["text"] # type: ignore[index] + text = self.normalize("".join(text_parts)) + item["text"] = [] + if text: + indent = " " * max(self.ul - 1, 0) + prefix = " " * max(self.ul, 0) if item["emitted"] else indent + "- " + self.line(prefix + text) + item["emitted"] = True + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + attrs_dict = dict(attrs) + if tag in ("title", "h1", "h3"): + self.buf = [] + elif tag == "p": + self.buf = [] + elif tag == "ul": + if self.li: + self.flush_li() + elif self.buf: + self.flush_buf(trailing_blank=False) + self.ul += 1 + elif tag == "li": + self.li.append({"text": [], "emitted": False}) + elif tag == "a": + self.href = attrs_dict.get("href") or "" + self.link = [] + elif tag in ("b", "strong"): + self.bold += 1 + elif tag == "br": + self.add(" \n") + + def handle_endtag(self, tag: str) -> None: + if tag == "title": + text = self.normalize("".join(self.buf)) + self.buf = [] + if text: + self.line(text) + self.blank() + elif tag == "h1": + text = self.normalize("".join(self.buf)) + self.buf = [] + if text: + self.line("# " + text) + self.blank() + elif tag == "h3": + text = self.normalize("".join(self.buf)) + self.buf = [] + if text: + self.blank() + self.line("### " + text) + self.blank() + elif tag == "p": + if self.buf: + self.flush_buf(trailing_blank=False) + elif tag == "ul": + self.ul = max(0, self.ul - 1) + self.blank() + elif tag == "li": + self.flush_li() + if self.li: + self.li.pop() + elif tag == "a": + text = self.normalize("".join(self.link)) + href = self.href + self.href = None + self.link = [] + self.add(f"[{text}]({href})" if href else text) + elif tag in ("b", "strong"): + self.bold = max(0, self.bold - 1) + + def handle_data(self, data: str) -> None: + self.add(self.escape(data)) + + def markdown(self) -> str: + while self.out and self.out[-1] == "": + self.out.pop() + return "\n".join(self.out) + "\n" + + +SECTION_RE = re.compile(r"^(Fixes|Features) \(\[More Info\]\(.+\)\)$") +PLAIN_HEADING_RE = re.compile(r"^[A-Za-z][A-Za-z0-9 &]+$") + + +def promote_headings(markdown: str) -> str: + lines = markdown.splitlines() + promoted: list[str] = [] + + for index, line in enumerate(lines): + next_line = lines[index + 1] if index + 1 < len(lines) else "" + if SECTION_RE.match(line): + promoted.append("#### " + line) + elif PLAIN_HEADING_RE.match(line) and next_line.startswith("- "): + promoted.append("##### " + line) + else: + promoted.append(line) + + return "\n".join(promoted).rstrip() + "\n" + + +def fetch(version: str) -> str: + request = Request(BASE_URL.format(version=version), headers={"User-Agent": "Mozilla/5.0"}) + with urlopen(request) as response: + return response.read().decode("utf-8", "replace") + + +def convert(html: str) -> str: + parser = NotesMarkdown() + parser.feed(html) + return promote_headings(parser.markdown()) + + +def main() -> None: + arg_parser = argparse.ArgumentParser(description="Fetch BYOND release notes and convert them to Markdown.") + arg_parser.add_argument("versions", nargs="*", default=DEFAULT_VERSIONS, help="BYOND major versions to fetch") + arg_parser.add_argument("--output-dir", type=Path, default=Path("changelogs"), help="Directory for generated Markdown files") + args = arg_parser.parse_args() + + args.output_dir.mkdir(parents=True, exist_ok=True) + + for version in args.versions: + html = fetch(version) + output_path = args.output_dir / f"{version}.md" + output_path.write_text(convert(html), encoding="utf-8") + print(f"wrote {output_path}") + + +if __name__ == "__main__": + main() diff --git a/tools/run-auxtest.sh b/tools/run-auxtest.sh new file mode 100755 index 00000000..605e48f3 --- /dev/null +++ b/tools/run-auxtest.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TARGET="${1:-all}" + +version_from_minor() { + case "$1" in + 1602|1647) + printf '515.%s' "$1" + ;; + 1648|1649|1650|1651|1652|1653|1654|1655|1656|1666|1667|1681) + printf '516.%s' "$1" + ;; + 5??.*) + printf '%s' "$1" + ;; + *) + printf 'Unknown BYOND version or minor build: %s\n' "$1" >&2 + exit 2 + ;; + esac +} + +linux_byond_path() { + local version="$1" + printf '%s/byond/linux/%s_byond_linux/byond' "$ROOT" "$version" +} + +windows_byond_path() { + local version="$1" + printf '%s/byond/windows/%s_byond/byond' "$ROOT" "$version" +} + +run_linux() { + local label="$1" + local byond_path="$2" + + if [[ ! -x "$byond_path/bin/DreamMaker" || ! -x "$byond_path/bin/DreamDaemon" ]]; then + printf 'Missing Linux BYOND executables for %s at %s\n' "$label" "$byond_path" >&2 + exit 1 + fi + + printf '\n== Linux %s ==\n' "$label" + ( + cd "$ROOT" + BYOND_PATH="$byond_path" cargo run -p test_runner --target i686-unknown-linux-gnu + ) +} + +windows_vs_install() { + powershell.exe -NoProfile -Command \ + "\$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe'; if (!(Test-Path \$vswhere)) { exit 1 }; \$install = & \$vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath; if (!\$install) { exit 1 }; \$install" \ + | tr -d '\r' +} + +run_windows() { + local label="$1" + local byond_path="$2" + local byond_path_win + local root_win + local vs_install + + if [[ ! -f "$byond_path/bin/dm.exe" || ! -f "$byond_path/bin/dreamdaemon.exe" ]]; then + printf 'Missing Windows BYOND executables for %s at %s\n' "$label" "$byond_path" >&2 + exit 1 + fi + + byond_path_win="$(wslpath -w "$byond_path")" + root_win="$(wslpath -w "$ROOT")" + vs_install="$(windows_vs_install)" + + if [[ -z "$vs_install" ]]; then + printf 'Could not find Visual Studio with x86 C++ tools\n' >&2 + exit 1 + fi + + printf '\n== Windows %s ==\n' "$label" + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ + "\$ErrorActionPreference = 'Stop'; "\ +"\$repo = '$root_win'; "\ +"\$byond = '$byond_path_win'; "\ +"\$vsInstall = '$vs_install'; "\ +"\$work = Join-Path \$env:TEMP 'auxtools-wsl-$label'; "\ +"\$devShell = Join-Path \$vsInstall 'Common7\Tools\Launch-VsDevShell.ps1'; "\ +"if (!(Test-Path \$devShell)) { throw \"Could not find \$devShell\" }; "\ +"& \$devShell -Arch x86 -HostArch amd64 | Out-Null; "\ +"if (!(Test-Path \$work)) { New-Item -ItemType Directory -Path \$work | Out-Null }; "\ +"Get-ChildItem -LiteralPath \$work -Force | Where-Object { \$_.Name -ne 'target' } | Remove-Item -Recurse -Force; "\ +"Get-ChildItem -LiteralPath \$repo -Force | Where-Object { \$_.Name -notin @('target', '.git', 'byond') } | Copy-Item -Destination \$work -Recurse -Force; "\ +"Set-Location \$work; "\ +"\$env:BYOND_PATH = \$byond; "\ +"\$env:CARGO_INCREMENTAL = '0'; "\ +"cargo run -p test_runner --target i686-pc-windows-msvc; "\ +"\$status = \$LASTEXITCODE; "\ +"exit \$status" +} + +run_version() { + local platform="$1" + local version + version="$(version_from_minor "$2")" + + case "$platform" in + linux) + run_linux "$version" "$(linux_byond_path "$version")" + ;; + windows) + run_windows "$version" "$(windows_byond_path "$version")" + ;; + *) + printf 'Unknown platform: %s\n' "$platform" >&2 + exit 2 + ;; + esac +} + +case "$TARGET" in + linux-515) + run_version linux 1602 + ;; + linux-516) + run_version linux 1681 + ;; + windows-515) + run_version windows 1602 + ;; + windows-516) + run_version windows 1681 + ;; + linux-5??.*|linux-????) + run_version linux "${TARGET#linux-}" + ;; + windows-5??.*|windows-????) + run_version windows "${TARGET#windows-}" + ;; + 5??.*|????) + run_version windows "$TARGET" + run_version linux "$TARGET" + ;; + all) + run_version windows 1602 + run_version windows 1681 + run_version linux 1602 + run_version linux 1681 + ;; + *) + printf 'Usage: %s [all|linux-515|linux-516|windows-515|windows-516|linux-|windows-|]\n' "$0" >&2 + printf 'Examples: %s linux-515.1647; %s windows-1647; %s 1602\n' "$0" "$0" "$0" >&2 + exit 2 + ;; +esac