Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

DarkClient is a Minecraft (Java Edition) modification framework written in Rust. It injects native libraries into a running Minecraft JVM and drives the game through JNI. One build supports both **obfuscated** Minecraft (≤ 1.21.11, via bundled Mojmap mappings) and **unobfuscated** Minecraft (26.1+, via runtime JNI reflection) — see the mapping system below. It is a Cargo workspace of three crates.

## Build & Common Commands

```bash
cargo build --release # build all three crates
cargo build -p client --release # build a single crate
cargo test -p client # tests live only in client/src/mapping/class.rs
cargo test -p client test_type_compatibility # run one test
cargo fmt
cargo clippy
python conversion.py # regenerate mappings.json (needs the `requests` package)
```

- **Always build `--release`.** The release profile (`opt-level = "s"`, `lto = true`) is what CI and runtime expect; debug builds also silence `dead_code` warnings via `lib.rs`.
- **Windows requires the nightly toolchain** (see `.github/workflows/build.yml`) and a discoverable `jvm.lib`. `client/build.rs` and `agent_loader/build.rs` locate it via `JAVA_HOME` or `JVM_LIB_DIR`; Linux links `libjvm.so` directly. JDK 21+ required.
- Running the `injector` needs root (`sudo`) on Linux / Administrator on Windows. `libagent_loader` and `libclient` must sit in the injector's working directory.

## Crate Roles

- **`injector/`** — standalone GUI binary (egui/eframe; `--tui` flag for a crossterm TUI). Finds Java processes whose command line contains `minecraft`, injects `agent_loader`, then drives the client.
- **`agent_loader/`** — `cdylib` injected first. A `#[ctor]` runs on load: starts a JVM health monitor and a TCP command server. Owns the lifecycle of the client library (load/unload/hot-reload).
- **`client/`** — `cdylib`, the actual mod framework. JNI-driven game interaction, OpenGL overlay, module system.

## Injection & Hot-Reload Flow

This is the core control flow and spans all three crates:

1. `injector` injects `libagent_loader.so`/`.dll` into the JVM process — ptrace (`ptrace-inject`) on Linux, `dll-syringe` on Windows.
2. `agent_loader`'s `#[ctor]` `agent_onload()` starts a TCP server on **`127.0.0.1:7878`** (constant duplicated in `injector/src/platform/mod.rs::SOCKET_ADDRESS`).
3. `injector` connects and sends `reload <absolute-path-to-libclient>`.
4. `agent_loader` copies the library to a temp file (avoids file locks), `dlopen`s it, and calls the exported `initialize_client`.
5. Re-injecting repeats step 3 → `reload_client_library` calls `cleanup_client` on the old library before loading the new one. This is the hot-reload path.

`client` exposes exactly two `#[no_mangle] extern "C"` symbols: `initialize_client` and `cleanup_client`. `initialize_client` spawns a thread that builds `Minecraft::instance()`, calls `register_modules()`, and installs hooks.

## client/ Internals

**Rendering & ticking** (`graphic/hook.rs`): `install_hooks` hooks `glfwSwapBuffers` (via `ilhook`) so `on_frame` runs every frame — it renders the egui overlay (`ui_engine.rs`) and calls `check_tick`. `check_tick` compares the player's tick count to detect new game ticks and calls `DarkClient::tick()`, which ticks every enabled module. Tick logic runs on the render thread, not a Minecraft thread.

**Input** (`graphic/input.rs`): swaps GLFW key/mouse/cursor callbacks. **Right Shift** (key `344`) toggles the GUI; while the GUI is open, input events are consumed instead of forwarded to Minecraft. Module keybinds toggle modules on key press.

**Module system** (`module/mod.rs`): implement the `Module` trait (`on_start`/`on_stop`/`on_tick`, all returning `anyhow::Result<()>`) plus `ModuleData` accessors. Register new modules in `register_modules()` in `client/src/lib.rs`. Modules carry typed `ModuleSetting`s (Toggle/Slider/Choice/Color). Note: the trait example in `README.md` is stale — the real trait methods return `anyhow::Result<()>`.

**Mapping system** (`mapping/`): bridges deobfuscated (Mojmap) names — what `MinecraftClassType` and the rest of the code use — to whatever the running JVM actually exposes. `Mapping::new()` auto-detects the build by probing `find_class("net/minecraft/client/Minecraft")` and picks one of two modes:

- **Obfuscated** (`Mode::Obfuscated`): the probe fails. `mappings.json` and `java_mappings.json` (project root, **`include_str!`'d at compile time**) are parsed into a class map; names are translated deobfuscated → obfuscated.
- **Reflected** (`Mode::Reflected`): the probe succeeds (Minecraft 26.1+, unobfuscated). No JSON is used; class/method/field names are identity, and method signatures — still required by JNI — are discovered lazily via `java.lang.Class` reflection in `reflect.rs` and cached. No mapping file is ever needed for new versions.

Both modes share one code path: a `RwLock<HashMap<String, Arc<MinecraftClass>>>` populated up-front (obfuscated) or lazily by reflection (reflected). `Mapping` wraps all JNI calls (`call_method`, `call_static_method`, `get_field`, `set_field`, etc.); `class.rs` does overload resolution by scoring argument-type compatibility against JNI signatures.

**Lifecycle safety**: the global `RUNNING: AtomicBool` gates `on_frame` and the agent's loops. A panic hook in `initialize_client` calls `cleanup_client` so input/render hooks are always uninstalled and GLFW callbacks restored, even on panic.

## Mappings

`conversion.py` downloads official Mojang mappings for a chosen **obfuscated** Minecraft version (≤ 1.21.11) and writes the custom `mappings.json` format. The committed `mappings.json` is ~18 MB. `java_mappings.json` is a small hand-written supplement for `java.*` classes, merged in at load time. Unobfuscated versions (26.1+) need none of this — they go through the reflected mapping path. The 26.1 runtime requires JDK 25.

## Logs

- `injector` → `app.log` (in its working directory)
- `agent_loader` → `agent_loader.log`
- `client` → `dark_client.log` (in `.minecraft`)
61 changes: 6 additions & 55 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

A Minecraft hacked client built in Rust, using JNI (Java Native Interface) for seamless integration with Minecraft's Java runtime. DarkClient provides a robust architecture for developing game modifications through dynamic library injection.

### Minecraft Version Mappings: 1.21.10
### Supported Minecraft Versions

- **Obfuscated builds** (≤ 1.21.11): bundled Mojmap mappings (`mappings.json`, currently 1.21.10).
- **Unobfuscated builds** (26.1+): no mappings needed — names are resolved directly, method signatures via runtime JNI reflection.

The build auto-detects which mode to use at runtime. A single binary works on both.

## 🚀 Features

Expand Down
10 changes: 8 additions & 2 deletions agent_loader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,14 @@ fn setup_signal_handlers() {
}

unsafe {
libc::signal(libc::SIGTERM, handle_signal as libc::sighandler_t);
libc::signal(libc::SIGINT, handle_signal as libc::sighandler_t);
libc::signal(
libc::SIGTERM,
handle_signal as *const () as libc::sighandler_t,
);
libc::signal(
libc::SIGINT,
handle_signal as *const () as libc::sighandler_t,
);
}

info!("Signal handlers installed");
Expand Down
12 changes: 9 additions & 3 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ crate-type = ["cdylib"]

[dependencies]
egui.workspace = true
eframe.workspace = true
winit = "0.30"
#eframe.workspace = true
egui_glow = "0.29.0"
glow = "0.14.0"
#winit = "0.30"
log.workspace = true
simplelog.workspace = true
jni = "0.21.1"
Expand All @@ -20,4 +22,8 @@ anyhow = "1.0"
libc = "0.2.178"
libloading = "0.9.0"
cfg-if = "1.0.4"
ilhook = "2.3.0"
ilhook = "2.3.0"
lazy_static = "1.4.0"

[build-dependencies]
gl_generator = "0.14"
13 changes: 12 additions & 1 deletion client/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
// It finds the `jvm.lib` import library that is required to link JNI functions.
// On Linux, this is unnecessary because the linker can directly use libjvm.so.

use gl_generator::{Api, Fallbacks, GlobalGenerator, Profile, Registry};
use std::env;
use std::fs::File;
use std::path::Path;

#[cfg(windows)]
fn main() {
use std::path::PathBuf;
Expand Down Expand Up @@ -70,7 +75,13 @@ fn main() {

#[cfg(not(windows))]
fn main() {
// On non-Windows systems this build script does nothing.
let dest = env::var("OUT_DIR").unwrap();
let mut file = File::create(&Path::new(&dest).join("bindings.rs")).unwrap();

// Ask for OpenGL 3.3 Compatibility so we get VAOs (GenVertexArrays) and modern shader API
Registry::new(Api::Gl, (3, 3), Profile::Compatibility, Fallbacks::All, [])
.write_bindings(GlobalGenerator, &mut file)
.unwrap();
}

#[cfg(windows)]
Expand Down
108 changes: 0 additions & 108 deletions client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,111 +92,3 @@ impl DarkClient {
}
}
}

// Module for handling keyboard inputs
pub mod keyboard {
use super::*;
use crate::mapping::client::minecraft::Minecraft;
use jni::objects::JValue;
use jni::sys::jlong;
use log::info;
use std::collections::HashSet;
use std::sync::atomic::AtomicBool;
use std::thread;
use std::time::Duration;

static RUNNING: OnceLock<AtomicBool> = OnceLock::new();

pub fn start_keyboard_handler() {
if RUNNING.get().is_none() {
RUNNING.set(AtomicBool::new(true)).unwrap();
}
thread::spawn(|| {
let minecraft = Minecraft::instance();
let client = DarkClient::instance();
let mut env = client.get_env().unwrap();

let glfw_window = match minecraft.window.get_window() {
Ok(window) => window,
Err(e) => {
error!("Failed to get GLFW window: {}", e);
return;
}
};

let mut keys: HashSet<i32> = HashSet::new();
while RUNNING
.get()
.unwrap()
.load(std::sync::atomic::Ordering::Relaxed)
{
thread::sleep(Duration::from_millis(100));

client.modules.read().unwrap().values().for_each(|module| {
let mut module = module.lock().unwrap();
let module_data = module.get_module_data();
let key = module_data.key_bind as i32;

if is_key_down(&mut env, glfw_window, key) {
if !keys.contains(&key) {
keys.insert(key);

let enabled = !module_data.enabled;
info!(
"{} {}",
module_data.name,
if enabled { "enabled" } else { "disabled" }
);
if enabled {
match module.on_start() {
Ok(_) => {}
Err(e) => error!(
"Failed to start module {}: {}",
module.get_module_data().name,
e
),
}
} else {
match module.on_stop() {
Ok(_) => {}
Err(e) => error!(
"Failed to stop module {}: {}",
module.get_module_data().name,
e
),
}
}
module.get_module_data_mut().set_enabled(enabled);
}
} else {
keys.remove(&key);
}
});
}
});
}

pub fn stop_keyboard_handler() {
if RUNNING.get().is_none() {
return;
}
RUNNING
.get()
.unwrap()
.store(false, std::sync::atomic::Ordering::Relaxed);
}

fn is_key_down(env: &mut JNIEnv, glfw_window: jlong, key: i32) -> bool {
let glfw = env.find_class("org/lwjgl/glfw/GLFW").unwrap();
env.call_static_method(
glfw,
"glfwGetKey",
"(JI)I",
&[JValue::Long(glfw_window), JValue::Int(key)],
)
.unwrap()
.i()
.unwrap()
== 1
}
}
Loading
Loading