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
//! Memory command CLI definitions
use clap::Subcommand;
use std::path::PathBuf;
#[derive(Subcommand)]
pub enum PreloadAction {
/// Show the LD_PRELOAD command to intercept file I/O
Info,
/// Run a command with the preload library
Run {
/// Directory to save captured file writes
#[arg(short, long)]
capture: Option<PathBuf>,
/// Filter pattern for files to capture (e.g., "*.json,*.ncs")
#[arg(short, long)]
filter: Option<String>,
/// WINEDEBUG settings for Wine/Proton tracing (e.g., "+file", "+relay")
#[arg(short, long)]
winedebug: Option<String>,
/// The command to run
#[arg(trailing_var_arg = true, required = true)]
command: Vec<String>,
},
/// Watch the preload log file
Watch {
/// Path to log file
#[arg(short, long, default_value = "/tmp/bl4_preload.log")]
log_file: PathBuf,
},
}
#[derive(Subcommand)]
pub enum MemoryAction {
/// Show info about the attached process
Info,
/// Discover UE5 structures (GNames, GUObjectArray)
Discover {
/// What to discover (gnames, guobjectarray, all)
#[arg(default_value = "all")]
target: String,
},
/// List UObjects by class name
Objects {
/// Class name to filter by (e.g. "RarityWeightData", "ItemPoolDef")
#[arg(short, long)]
class: Option<String>,
/// Maximum number of objects to show
#[arg(short, long, default_value = "20")]
limit: usize,
},
/// Dump usmap mappings file from live process
DumpUsmap {
/// Output path for usmap file
#[arg(short, long, default_value = "BL4.usmap")]
output: PathBuf,
},
/// Look up an FName by index
Fname {
/// FName index to look up
index: u32,
/// Show raw bytes at the FName entry (for debugging)
#[arg(long)]
debug: bool,
},
/// Search for an FName by string
FnameSearch {
/// String to search for in the FName pool
query: String,
},
/// Search for Class UClass by scanning for self-referential objects
FindClassUClass,
/// List all UClass instances in memory (uses discovered metaclass address)
ListUClasses {
/// Maximum number of classes to show (0 = all)
#[arg(short, long, default_value = "50")]
limit: usize,
/// Filter by class name pattern (case-insensitive)
#[arg(short, long)]
filter: Option<String>,
},
/// Enumerate UObjects from GUObjectArray
ListObjects {
/// Maximum number of objects to show
#[arg(short, long, default_value = "20")]
limit: usize,
/// Filter by class name pattern (case-insensitive)
#[arg(short = 'c', long)]
class_filter: Option<String>,
/// Filter by object name pattern (case-insensitive)
#[arg(short = 'n', long)]
name_filter: Option<String>,
/// Show statistics only (don't list individual objects)
#[arg(long)]
stats: bool,
},
/// Analyze dump file: discover UObject layout, FName pool, and UClass metaclass
AnalyzeDump,
/// List current inventory items
ListInventory,
/// Read a value from game memory
Read {
/// Memory address (hex, e.g. 0x7f1234567890)
address: String,
/// Number of bytes to read
#[arg(short, long, default_value = "64")]
size: usize,
},
/// Write bytes to game memory
Write {
/// Memory address (hex, e.g. 0x7f1234567890)
address: String,
/// Hex bytes to write (e.g. "90 90 90" for NOPs)
bytes: String,
},
/// Scan for a pattern in memory
Scan {
/// Hex pattern to search for (e.g. "48 8B 05 ?? ?? ?? ??")
pattern: String,
},
/// Patch a single instruction (replaces with NOPs or custom bytes)
Patch {
/// Memory address to patch (hex)
address: String,
/// Number of bytes to NOP out
#[arg(short, long)]
nop: Option<usize>,
/// Custom replacement bytes (hex, e.g. "EB 05" for short jump)
#[arg(short, long)]
bytes: Option<String>,
},
/// Monitor the preload library log file
Monitor {
/// Path to log file
#[arg(short, long, default_value = "/tmp/bl4_preload.log")]
log_file: PathBuf,
/// Filter log entries by function name
#[arg(short, long)]
filter: Option<String>,
/// Only show entries from addresses in game code (not libraries)
#[arg(long)]
game_only: bool,
},
/// LD_PRELOAD library for intercepting file I/O (NCS extraction, etc.)
Preload {
#[command(subcommand)]
action: PreloadAction,
},
/// Search for a string in memory and dump context around matches
ScanString {
/// String to search for
query: String,
/// Bytes to show before the match
#[arg(short = 'B', long, default_value = "64")]
before: usize,
/// Bytes to show after the match
#[arg(short = 'A', long, default_value = "64")]
after: usize,
/// Maximum number of matches to show
#[arg(short, long, default_value = "10")]
limit: usize,
},
/// Extract part definitions from memory dump (searches for XXX_YY.part_* patterns)
DumpParts {
/// Output file for parts JSON
#[arg(short, long, default_value = "parts_dump.json")]
output: PathBuf,
},
/// Build parts database with Category/Index mappings
BuildPartsDb {
/// Input parts dump JSON (from dump-parts command)
#[arg(short, long, default_value = "share/manifest/parts_dump.json")]
input: PathBuf,
/// Output parts database JSON
#[arg(short, long, default_value = "share/manifest/parts_database.json")]
output: PathBuf,
/// Part categories mapping JSON (prefix -> category ID)
#[arg(short, long, default_value = "share/manifest/part_categories.json")]
categories: PathBuf,
},
/// Extract part definitions from UObjects with authoritative Category/Index from SerialIndex
ExtractParts {
/// Output file for extracted parts with categories
#[arg(short, long, default_value = "parts_with_categories.json")]
output: PathBuf,
/// Just list all FNames containing .part_ without extracting (for debugging)
#[arg(long)]
list_fnames: bool,
},
/// Extract raw part data without assumptions (stores actual bytes from memory)
ExtractPartsRaw {
/// Output file for raw extraction data
#[arg(short, long, default_value = "share/ncs/parts_raw.json")]
output: PathBuf,
},
/// Find objects matching a name pattern to discover their class
FindObjectsByPattern {
/// Name pattern to search for (e.g. ".part_")
pattern: String,
/// Maximum number of results
#[arg(short, long, default_value = "10")]
limit: usize,
},
/// Generate an object map JSON for fast lookups on subsequent runs
GenerateObjectMap {
/// Output file for object map JSON
#[arg(short, long)]
output: Option<PathBuf>,
},
}