Repository navigation
Expand file tree
/
Copy pathops.rs
More file actions
323 lines (304 loc) · 11.3 KB
/
Copy pathops.rs
File metadata and controls
323 lines (304 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
//! Unity-side generic primitive op handlers. Register against
//! the shared `modforge::ops::OP_REGISTRY`.
//!
//! Ops shipped here mirror the ueforge surface where the concept
//! maps cleanly, and add Unity-flavored variants where it does
//! not:
//! - walk_class, inspect_object, list_singletons
//! - read_field, write_field, invoke_method
//! - list_ops, list_selectors (auto-generated)
use std::ffi::c_void;
use serde_json::{Value as Json, json};
use modforge::args::{arg_str, arg_u64};
use modforge::ops::{OP_REGISTRY, OpDef};
use crate::bridge::MonoHandle;
use crate::hook::{self, HookCtx};
use crate::main_thread_queue::MAIN_QUEUE;
use crate::mono::{MonoObject, MonoType};
/// Register every framework-shipped Unity op. Game crates call
/// this once at worker init, typically before their own
/// per-game `OP_REGISTRY.register(...)` calls.
pub fn register_builtins() {
// `timing` and `timing_report`. Shared with every other
// forge, so a mod for any game is measured the same way.
modforge::counters::register_ops();
OP_REGISTRY.register_many([
OpDef::new(
"ping",
"Liveness probe",
"{}",
|_args| Ok(json!({"pong": true})),
),
OpDef::new(
"walk_class",
"Enumerate every live instance of a Mono type",
"{class: str, include_inactive?: bool}",
walk_class,
),
OpDef::new(
"inspect_object",
"Dump an object's fields + property values",
"{handle: i32}",
inspect_object,
),
OpDef::new(
"read_field",
"Read a typed field on a Mono object handle",
"{handle: i32, field: str}",
read_field,
),
OpDef::new(
"write_field",
"Write a typed field on a Mono object handle (main-thread queued)",
"{handle: i32, field: str, value: any}",
write_field,
),
OpDef::new(
"invoke_method",
"Invoke a method on a Mono object handle (main-thread queued)",
"{handle: i32, method: str, args?: array}",
invoke_method,
),
OpDef::new(
"invoke_static",
"Invoke a STATIC method on a named class (main-thread queued)",
"{class: str, method: str, args?: array}",
invoke_static,
),
OpDef::new(
"list_singletons",
"Get the live Singleton<T>.Instance for each named type",
"{types: [str]}",
list_singletons,
),
OpDef::new(
"harmony_probe",
"Try a Harmony prefix patch on class.method with a no-op callback, report ok or the failure, then unpatch. Diagnoses per-target patchability without a game restart; the shim logs the full exception to the player log.",
"{class: str, method: str, ctx_kind?: 0|1}",
harmony_probe,
),
OpDef::new(
"release_handle",
"Free one handle from the shim's handle table. Complex values returned by read_field / invoke_method / inspect_object carry a live handle for chaining; release it when done with a research walk.",
"{handle: u64}",
release_handle,
),
OpDef::new(
"list_ops",
"Auto-generated catalog of every registered debug op",
"{}",
|_args| Ok(OP_REGISTRY.list_json()),
),
OpDef::new(
"op_metrics",
"Per-op latency metrics (sorted by total_ns)",
"{}",
|_args| Ok(modforge::ops::metrics_json()),
),
OpDef::new(
"list_selectors",
"Auto-generated catalog of every registered selector kind",
"{}",
|_args| Ok(crate::selector::list_json()),
),
OpDef::new(
"list_methods",
"Enumerate methods on a type (walks inheritance chain)",
"{class: str}",
list_methods,
),
]);
}
/// Run `f` on the game's main thread via MAIN_QUEUE and wait for
/// the answer (bounded oneshot). Every op that touches Unity or
/// game code goes through here: on IL2CPP, native calls from the
/// HTTP thread are a process crash, not an exception (seen live
/// 2026-08-07: 0xc0000005 during an off-main inspect_object).
fn on_main<T: Send + 'static>(
op_name: &str,
timeout: std::time::Duration,
f: impl FnOnce() -> T + Send + 'static,
) -> Result<T, String> {
MAIN_QUEUE.run(op_name, timeout, f)
}
fn walk_class(args: &Json) -> Result<Json, String> {
let class = arg_str(args, "class")?.to_string();
let include_inactive = args
.get("include_inactive")
.and_then(Json::as_bool)
.unwrap_or(false);
on_main("walk_class", std::time::Duration::from_secs(2), move || {
let ty = MonoType::find(&class).ok_or_else(|| format!("type '{class}' not found"))?;
ty.walk(include_inactive)
})?
}
fn list_methods(args: &Json) -> Result<Json, String> {
use std::ffi::CString;
let class = arg_str(args, "class")?;
let bridge = crate::bridge::try_get()?;
let c_name = CString::new(class).map_err(|e| format!("bad class: {e}"))?;
// 256KB: CartelGoon (Schedule 1) overflowed 64KB (found
// 2026-08-07); interop game classes carry huge RPC surfaces.
let mut buf = vec![0u8; 262144];
let n = (bridge.list_methods)(
c_name.as_ptr(),
buf.as_mut_ptr() as *mut std::os::raw::c_char,
buf.len() as i32,
);
if n < 0 {
return Err("list_methods: buffer too small".into());
}
buf.truncate(n as usize);
serde_json::from_slice::<Json>(&buf).map_err(|e| format!("list_methods json: {e}"))
}
fn inspect_object(args: &Json) -> Result<Json, String> {
let h = arg_u64(args, "handle", None)? as i32;
on_main(
"inspect_object",
std::time::Duration::from_secs(2),
move || {
// SAFETY: caller asserts handle is live; if not, the shim's
// dictionary lookup returns null and the op surfaces an
// error.
let obj = unsafe { MonoObject::from_handle(MonoHandle(h)) };
let r = obj.dump();
std::mem::forget(obj); // don't release; caller may reuse the handle
r
},
)?
}
fn read_field(args: &Json) -> Result<Json, String> {
let h = arg_u64(args, "handle", None)? as i32;
let field = arg_str(args, "field")?.to_string();
on_main("read_field", std::time::Duration::from_secs(2), move || {
let obj = unsafe { MonoObject::from_handle(MonoHandle(h)) };
let r = obj.read_field(&field);
std::mem::forget(obj);
r
})?
}
fn write_field(args: &Json) -> Result<Json, String> {
let h = arg_u64(args, "handle", None)? as i32;
let field = arg_str(args, "field")?.to_string();
let value = args.get("value").cloned().unwrap_or(Json::Null);
on_main(
"write_field",
std::time::Duration::from_secs(1),
move || {
let obj = unsafe { MonoObject::from_handle(MonoHandle(h)) };
let res = obj.write_field(&field, &value);
std::mem::forget(obj);
res.map(|_| json!({"written": true}))
},
)?
}
extern "C" fn probe_prefix_noop(ctx: *const c_void) -> i32 {
// ctx-kind probes hand us an owned handle; release it so the
// probe doesn't leak table entries.
let h = ctx as isize as i32;
if h != 0 {
// SAFETY: the shim's ctx dispatcher acquired this handle
// for this call and we own it; Drop releases it.
let obj = unsafe { MonoObject::from_handle(MonoHandle(h)) };
drop(obj);
}
0 // never skip the original
}
fn harmony_probe(args: &Json) -> Result<Json, String> {
let class = arg_str(args, "class")?.to_string();
let method = arg_str(args, "method")?.to_string();
let ctx_kind = args.get("ctx_kind").and_then(Json::as_i64);
on_main(
"harmony_probe",
std::time::Duration::from_secs(5),
move || {
let res = match ctx_kind {
Some(0) => {
hook::patch_prefix_ctx(&class, &method, HookCtx::Instance, probe_prefix_noop)
}
Some(1) => {
hook::patch_prefix_ctx(&class, &method, HookCtx::Arg0, probe_prefix_noop)
}
_ => hook::patch_prefix(&class, &method, probe_prefix_noop),
};
match res {
Ok(hook) => {
drop(hook); // unpatch immediately; the probe only tests applicability
Ok(json!({"patched": true, "unpatched": true}))
}
Err(e) => Err(format!("{e} (full exception in the player log)")),
}
},
)?
}
fn invoke_method(args: &Json) -> Result<Json, String> {
let h = arg_u64(args, "handle", None)? as i32;
let method = arg_str(args, "method")?.to_string();
let m_args = args.get("args").cloned().unwrap_or(Json::Array(vec![]));
on_main(
"invoke_method",
std::time::Duration::from_secs(2),
move || {
let obj = unsafe { MonoObject::from_handle(MonoHandle(h)) };
let res = obj.invoke(&method, &m_args);
std::mem::forget(obj);
res
},
)?
}
fn invoke_static(args: &Json) -> Result<Json, String> {
let class = arg_str(args, "class")?.to_string();
let method = arg_str(args, "method")?.to_string();
let m_args = args.get("args").cloned().unwrap_or(Json::Array(vec![]));
on_main(
"invoke_static",
std::time::Duration::from_secs(2),
move || crate::mono::invoke_static(&class, &method, &m_args),
)?
}
fn release_handle(args: &Json) -> Result<Json, String> {
let h = arg_u64(args, "handle", None)? as i32;
on_main(
"release_handle",
std::time::Duration::from_secs(1),
move || {
// SAFETY: releasing is what Drop does; a stale handle is a
// no-op in the shim's table.
let obj = unsafe { MonoObject::from_handle(MonoHandle(h)) };
drop(obj);
Ok(json!({"released": true}))
},
)?
}
fn list_singletons(args: &Json) -> Result<Json, String> {
let types = args
.get("types")
.and_then(Json::as_array)
.cloned()
.ok_or("missing arg 'types' (array of class names)")?;
on_main(
"list_singletons",
std::time::Duration::from_secs(2),
move || {
let mut out = Vec::with_capacity(types.len());
for t in &types {
let Some(name) = t.as_str() else {
continue;
};
let entry = match MonoType::find(name) {
None => json!({"class": name, "found": false}),
Some(ty) => match ty.singleton_instance() {
None => json!({"class": name, "found": false, "type_found": true}),
Some(obj) => {
let h = obj.handle();
std::mem::forget(obj); // keep alive; caller may inspect_object
json!({"class": name, "found": true, "handle": h.0})
}
},
};
out.push(entry);
}
Ok(json!({"singletons": out}))
},
)?
}