Files
slinky/docs/init.md
T
2026-11-05 19:45:34 +01:00

109 lines
8.8 KiB
Markdown

# Start-up, hooks and shutdown
How the client gets from "DLL loaded into the JVM" to "game calls into the modules", and back. Most of the original
start-up is protector-virtualised, so it could not be translated; it is written as new code
(`src/engine/protected/init.cpp`, `src/platform/win/entry.cpp`) from what the surrounding readable code expects.
Facts below come from the image unless marked **inferred**. The licence check is not part of this client.
## What was found where
| Piece | Function | State |
|---|---|---|
| Class-hook installer | `install_class_hooks` | **real code**, was missed by the first gap scan. Rewritten readably (tables are data), checked against the original by running both (`tools/emu_install.py`) |
| Class-hook remover | `remove_class_hooks` | real code, translated exactly |
| `ClassFileLoadHook` + class rewriter | `class_file_load_hook`, `rewrite_class_file`, `rewrite_instruction`, ... | real code, translated |
| `SlinkyChannelHandler.onPacket` native | `on_packet` | real code (also missed by the scan), translated |
| Key handlers for profile / module binds | `on_profile_bind`, `on_module_bind`, `bind_profile_to_key` | real code (missed), translated |
| `JNI_GetCreatedJavaVMs` lookup | `get_java_vm` | real code (missed), translated |
| Hooks on/off | `set_java_hooks` | virtualised -> `init.cpp` (**inferred**: GetEnv JVMTI, install or remove, log failure) |
| Start-up thread body | inside the original image's start-up routine at RVA 0x1000 | virtualised -> `slinky_setup` in `init.cpp` (**inferred** order) |
| First-frame GUI setup | inside `hook_get_mouse_over` | virtualised -> `gui_init` in `protected/hooks.cpp` (**inferred**) |
| Global teardown | DLL detach | virtualised -> `slinky_teardown` (**inferred**) |
The first gap scan only accepted a function that follows `0xcc/0x90/0xc3`, so functions after multi-byte NOP padding
or directly after a tail `jmp` were skipped; 33 real functions (7 KB) were found by fixing that
(`tools/extract_engine.py`). 43 protector stubs remain; `empty_placeholder` is still an empty placeholder (it has no callers).
## Entry (Windows)
`DllMain` (`src/platform/win/entry.cpp`) starts a thread on `DLL_PROCESS_ATTACH`; that thread runs `slinky_setup`.
On failure it prints `Initialization failed: <code>` and starts the unload thread (`start_unload_thread`).
## `slinky_setup` (init.cpp)
1. `get_java_vm`: JavaVM from `jvm.dll` -> `init_javavm`; attach the thread.
2. JNI ID table: `calloc(JIds)` -> `java_natives_world_cached_jni_ids`, `init_jni_ids` (game class loader, mapping detection, every class/method/field
ID). Mapping: `ids->variant` 0 = 1.7.10, 1 = 1.8.9; `ids->names` 0 = obfuscated, 1 = MCP, 2 = SRG.
3. Module manager: `create_module_manager(java_natives_world_module_manager, set_module_bind)` builds the lists from the static module list (67 descriptors);
`set_module_bind` is the key-bind setter it stores. Immediately afterwards `save_module_states` runs the modules' settings
visitors in "remember" mode, copying the static values from `.data` into each setting's saved slot. Those slots are what the
built-in Default profile restores and what a profile with missing fields uses as its fallback; the image's static `.data`
only contains the live values, so without this step both paths would reset settings to zero. This is part of the inferred
start-up order: the original's start-up lives in protector-virtualised code, so the call is not visible in `.text`.
4. State the frame and input code read: key table `java_natives_world_key_state_table_one_byte` (one byte per key), key map `java_natives_frame_profile_key_binds_key_profile`
(profile binds, handler `on_profile_bind`, items are strings freed with `free`), key map `java_natives_frame_module_key_binds_key_module` (module binds,
handler `on_module_bind`, several modules per key), profile list `init_profile_list` (`load_profiles` reads `~/.slinky/profiles`).
5. Chat commands `init_chat_commands`, GLFW entry points `init_glfw_entrypoints` (absent with LWJGL 2, not an error).
6. Saved settings `~/.slinky/settings` (**inferred**, mirror of `save_settings`): per-module state for global modules
through `load_module_settings`, `profile_binds` through `bind_profile_to_key`.
7. `input_input_handler_hooked_into_game = on_input_event`: the input handler the window hook (`hook_game_window`, installed lazily every frame) calls.
8. Java side: `install_packet_channel` defines `SlinkyChannelHandler` (class bytes in the image) and registers `onPacket = on_packet`.
9. `install_class_hooks`: define `SlinkyHooks13`, register its 23 natives, add the `can_retransform_classes` capability, set the
`ClassFileLoadHook` callback, enable the event, build the hook records for the detected mapping, retransform the 9
classes, disable the event again. Errors are `12000 + jvmtiError`.
The hooks go last: from the first retransformation on, the game calls the natives.
## First frame (`hook_get_mouse_over`, hooks.cpp)
The first `getMouseOver` runs `gui_init` once, on the render thread: GL entry points
(`slinky_load_gl` + `wglGetProcAddress`), NanoVG context (`nvgCreateGL3`), window object `frame_render_target` with the two
embedded fonts (Inter Medium = slot 0, Inter Bold = slot 1). NanoVG flags are **inferred** (antialias + stencil strokes).
## The class hooks
`SlinkyHooks13` (class file in the image, 1196 bytes) declares 23 `public static native` methods; their bodies are
`hook_get_mouse_over .. hook_update_controller` (`kNatives` in `src/engine/jni/hook_tables.inc`, same order as the class file). The game classes are
not detoured: through JVMTI `RetransformClasses` the `ClassFileLoadHook` (`class_file_load_hook`) rewrites matching call and field
instructions into `invokestatic SlinkyHooks13.<native>`, so the receiver becomes the first `Object` argument.
What is rewritten is data: for each mapping (1.7.10 / 1.8.9 x obfuscated / MCP / SRG) the 9 classes
(Minecraft, EntityRenderer, RenderManager, RendererLivingEntity, RenderPlayer, NetHandlerPlayClient, WorldClient, Entity,
EntityLivingBase), per class the natives it may call and per method the instructions to replace
(`src/engine/jni/hook_tables.inc`, generated by `tools/gen_hooks.py` from an emulated run of the original installer,
`tools/dump_hooks.py`). 16 method patterns, 23 sites per mapping.
Rewritten call sites, by native: `getMouseOver` (EntityRenderer.getMouseOver in updateCameraAndRender),
`updateDisplay` (Display.update), `clickMouse`/`rightClickMouse`/`updateEntities`/`updateController` (inside Minecraft.runTick
and the input tick), `postWorldClear_1_7/_1_8` (glClear / GlStateManager.clear), `orientCamera`, `updateActiveRenderInfo`,
`isDisplayActivePreRender`, `setRenderX/Y` (RenderManager fields), `canRenderName` (any method of RendererLivingEntity),
`renderPlayer`, `unloadChunk`, `setBlock_1_7/_1_8`, `invalidateChunk`, `shouldClipLedge` (Entity.isSneaking), `moveFlying`,
`sendPacket_1_7/_1_8`.
**Verification:** `tools/emu_install.py` loads the built `slinky.dll`, runs `install_class_hooks` under Unicorn for all six mappings and
compares the records it builds with the original's: identical (9 classes, 16 patterns, 23 sites, same 23 natives).
## Packets
`SlinkyChannelHandler` (netty `ChannelDuplexHandler`, class bytes in the image) is put in the connection's pipeline as
`slinky_handler` (`register_channel_class`, every frame). It queues packets and calls `onPacket(Object packet, boolean outbound,
long time, int id)` = `on_packet`: under the lock `frame_spinlock`, and unless shutting down (`java_natives_frame_client_shutting_down_natives_check`) it dispatches module
event 2 (inbound) or 3 (outbound) and returns the handlers' verdict.
## Shutdown
`hook_update_display` (the `updateDisplay` hook) unloads when the GUI's confirmation was given: saves settings, notifies and disables the
modules, removes the netty handler, `set_java_hooks(env, 0)` (hooks off), `restore_game_window` (window hook off), then `start_unload_thread` starts a
thread that waits one second, runs `slinky_teardown` and calls `FreeLibraryAndExitThread`.
`slinky_teardown` (**inferred**) drops the netty class and the cached class references and frees the manager, both key maps,
the profile list, the chat command table, the profile name, the mesh cache, the key table and the ID table.
GL objects (NanoVG context, framebuffer) are not destroyed: that needs the render thread's context.
## Not verified / open
* The whole start-up order and the object sizes (`KeyMap` 0x30, key table 0x100, profile list 0x20) are inferred from their readers;
nothing has run inside a game yet.
* `empty_placeholder` (decrypts a short string, only reached through `reset_key_state`, which nothing calls): empty placeholder.
* The NanoVG creation flags, JVMTI version (1.2) and the settings loader are inferred.
* Linux: no platform layer yet (JVMTI agent entry, input hook, file paths, GL loader).