-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcsb_binary.py
More file actions
313 lines (284 loc) · 14.6 KB
/
Copy pathcsb_binary.py
File metadata and controls
313 lines (284 loc) · 14.6 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
"""Binary controller/advanced settings support for the CoD editor.
Reads the MWII 'settings.*.csb' Connected-Storage blob, which holds the controller/
deadzone/aim-assist/movement/interaction settings that are NOT in the plaintext config.
File format (reverse-engineered, 28/28 findings verified):
- 16-byte static header, then value records [float32 LE][0x00][uint32 hash],
a length-prefixed enum pool [uint32 hash][len][ascii][NUL], and a trailing
CRC32(file[:-4]) little-endian. => writes are CRC-safe (see write_floats).
The dvar hash is a custom, non-invertible IW-engine FNV variant, so only settings we
have positively identified carry friendly names; the rest are labelled by their raw id.
"""
import os, re, struct, zlib, shutil, datetime
CRC_RESIDUE = 0x2144DF1C # zlib.crc32(whole self-sealing file)
# hash -> friendly in-game name (identified settings; extend as more are named)
FLOAT_NAMES = {
# Verified by live in-game correlation (change a setting, diff the save -- tools/csb_correlate.py),
# 2026-07-09 on BO7's g.p.cod25.1.0.b0; the ids are shared with the MWII .csb. This corrected
# several earlier position-guessed labels: the L/R and min/max were swapped, and 0x211c4e99 was
# mislabeled "Look Vertical Sensitivity" -- it is actually the Right Trigger deadzone.
# (all 8 confirmed exact by the user, incl. Left Stick Max = 0.80 for an in-game value of 80.)
0x3fbbba5c: "Stick Sensitivity - Horizontal (gamepad)",
0xfd578836: "Stick Sensitivity - Vertical (gamepad)",
0x93d9f49c: "Left Stick Min Input (deadzone)",
0xf83593ea: "Left Stick Max Input (deadzone)",
0x83680ae9: "Right Stick Min Input (deadzone)",
0xf4c4d5b7: "Right Stick Max Input (deadzone)",
0x23521cc0: "Left Trigger deadzone",
0x211c4e99: "Right Trigger deadzone",
0xc822f9e4: "ADS Sensitivity Multiplier",
0xb83b71ab: "ADS Sensitivity Multiplier (Focus)",
0xb8af9e7f: "Tac-Stance Sensitivity",
# Candidate ids NOT yet confirmed by correlation -- deliberately not shown to end users
# (would display a possibly-wrong name): 0xa4108446 (mouse H sens?), 0x06e0ad2f (aim response?).
}
ENUM_NAMES = {
0x30b3622f: "Automatic Sprint behavior",
0x8e0d45ec: "Interact / Reload behavior",
0x099b6793: "Button Layout preset", # verified live: buttons_default/buttons_lefty/...
0x09a55e2a: "Stick Layout preset", # verified live: thumbstick_default/thumbstick_southpaw/...
}
# Best-effort setting labels for decoded control VALUES, from the 3-account diff / MWII menu RE
# in lddc-analysis/DECODED-BINARY-SETTINGS.md. The exact dvar<->value pairing is NOT stored inline
# in the blob, so only distinctive values (each unique to one setting) are labeled here; everything
# else is shown by its raw value. Read-only / informational -- naming the rest needs live-game
# correlation (change a setting in-game, re-decode, and see which value moved).
VALUE_LABELS = {
"single_tap_sprint": "Sprint / Tac-Sprint Behavior",
"double_tap_sprint": "Sprint / Tac-Sprint Behavior",
"acceleration_speed": "Automatic Sprint",
"mount_binding": "Weapon Mount Activation",
"ads_melee": "Melee (ADS) Behavior",
"mantle_only": "Auto-Mantle",
"on_release": "Interact / Reload Behavior",
"use_tap": "Interact / Reload Behavior",
"contextual_tap": "Interact / Reload Behavior",
"tap_single": "Armor Plate / Equipment Behavior",
"tap_all": "Armor Plate / Equipment Behavior",
"simultaneous": "Equipment Behavior",
"buttons_default": "Controller Button Layout",
"thumbstick_default": "Stick Layout",
"hit_marker_3d": "Hit Marker Style",
"keyboard_mouse": "Input Device",
}
def crc_valid(data: bytes) -> bool:
return (zlib.crc32(data) & 0xFFFFFFFF) == CRC_RESIDUE
def _float_val(data, h):
i = data.find(struct.pack("<I", h))
if i >= 5 and data[i - 1] == 0:
return i - 5, round(struct.unpack("<f", data[i - 5:i - 1])[0], 4)
return None, None
def _enum_val(data, h):
i = data.find(struct.pack("<I", h))
if i >= 0:
ln = data[i + 4]
return i + 5, data[i + 5:i + 5 + ln].split(b"\0")[0].decode("latin1")
return None, None
def _scan_enum_pool(data):
"""Length-prefixed lowercase enum values ([len][chars]) in a .csb/.b0 string pool.
Returns [(offset, value), ...]. Only lowercase tokens match, so the CamelCase dvar
names in the name table are excluded."""
vals = []
for m in re.finditer(rb"[a-z][a-z0-9_]{2,}", data):
o = m.start()
s = m.group().decode("latin1")
if o >= 1 and data[o - 1] in (len(s), len(s) + 1):
vals.append((o, s))
return vals
def _scan_enum_records(data):
"""[id4][len1][ascii value] enum records (shared by .csb and .b0). Returns [(offset, id, value)].
Catches CamelCase values too (e.g. Alpha/Bravo); the name-table dvar names are padded with NUL
(not a length byte) so they don't match."""
out = []
for m in re.finditer(rb"[A-Za-z][A-Za-z0-9_]{2,}", data):
o, s = m.start(), m.group().decode("latin1")
if o >= 5 and data[o - 1] in (len(s), len(s) + 1):
out.append((o, struct.unpack("<I", data[o - 5:o - 1])[0], s))
return out
def _scan_float_records(data):
"""Treyarch .b0 float records: [id4][01]..[08 04][float4] (15 bytes). Returns {id: (offset, float)}.
Different layout from the IW .csb ([float][00][id]); reverse-engineered via live correlation."""
recs = {}
for p in range(len(data) - 15):
if data[p + 4] == 1 and data[p + 9] == 0x08 and data[p + 10] == 0x04:
f = struct.unpack("<f", data[p + 11:p + 15])[0]
if f == f and abs(f) < 1e4: # finite, plausible
recs.setdefault(struct.unpack("<I", data[p:p + 4])[0], (p + 11, round(f, 4)))
return recs
def decode(path):
"""Return (rows, crc_ok). rows: {name, hash, value, kind, offset}."""
data = open(path, "rb").read()
rows = []
for h, name in FLOAT_NAMES.items():
off, v = _float_val(data, h)
if off is not None:
rows.append({"name": name, "hash": h, "value": v, "kind": "float", "offset": off})
for h, name in ENUM_NAMES.items():
off, v = _enum_val(data, h)
if off is not None:
rows.append({"name": name, "hash": h, "value": v, "kind": "enum", "offset": off})
# Surface additional decoded control choices whose value maps to a known setting
# (read-only; the hash<->name pairing for these isn't recoverable offline).
seen = {str(r["value"]) for r in rows}
for off, s in _scan_enum_pool(data):
if s in VALUE_LABELS and s not in seen:
rows.append({"name": VALUE_LABELS[s], "value": s, "kind": "value", "offset": off})
seen.add(s)
return rows, crc_valid(data)
def float_range(name):
"""(min, max, decimals, step) input guard for a float setting, by friendly name.
These are UI guardrails to prevent fat-finger corruption; the field itself is a raw
float32. Ranges reflect the in-game menus for the identified settings."""
n = (name or "").lower()
if "deadzone" in n or "min input" in n or "max input" in n:
return 0.0, 1.0, 3, 0.01 # stick deadzones are a 0..1 fraction
if "sensitivity" in n:
return 0.0, 100.0, 2, 0.5 # mouse/gamepad sens (verified 8.0)
if "aim" in n:
return 0.0, 20.0, 3, 0.1 # aim response / aim-assist strength
return 0.0, 1000.0, 4, 0.1 # unknown float: wide but bounded
def write_floats(path, changes):
"""CRC-safe batch write of float settings. `changes` maps hash -> new value.
Applies every change, re-seals the CRC once, writes, then re-opens to verify the
CRC is valid. Returns [(hash, old, new), ...]. Raises on unknown hash or bad seal."""
data = bytearray(open(path, "rb").read())
applied = []
for h, new_val in changes.items():
off, old = _float_val(data, h)
if off is None:
raise KeyError(f"hash {h:#010x} not in {path}")
data[off:off + 4] = struct.pack("<f", float(new_val))
applied.append((h, old, round(float(new_val), 4)))
data[-4:] = struct.pack("<I", zlib.crc32(bytes(data[:-4])) & 0xFFFFFFFF)
if not crc_valid(bytes(data)):
raise ValueError("CRC re-seal failed; file not written")
open(path, "wb").write(bytes(data))
if not crc_valid(open(path, "rb").read()):
raise ValueError("post-write CRC verification failed")
return applied
def backup(path):
"""Copy `path` to a timestamped .bak beside it (metadata preserved). Returns the
backup path. Called before any binary write so a change is trivially reversible."""
stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
dst = f"{path}.{stamp}.bak"
shutil.copy2(path, dst)
return dst
def list_backups(path):
"""Backups previously made for `path`, newest first."""
d = os.path.dirname(path) or "."
base = os.path.basename(path)
outs = [os.path.join(d, f) for f in os.listdir(d)
if f.startswith(base + ".") and f.endswith(".bak")]
return sorted(outs, reverse=True)
def restore(path, backup_path):
"""Restore `path` from a chosen backup. Verifies the backup's CRC first so we never
write a corrupt file back over the live save. Returns True on success."""
data = open(backup_path, "rb").read()
if not crc_valid(data):
raise ValueError("backup is not a valid .csb (CRC check failed)")
shutil.copy2(backup_path, path)
return True
# ---------------------------------------------------------------------------
# Per-game binary settings: each title has its OWN file. IW engine (MWII/MWIII)
# uses the CRC-sealed .csb (editable floats); Treyarch (BO6/BO7) uses the .b0
# profile blob (read-only enum pool). We locate strictly by the game's cod tag so
# one game never shows another game's file.
# ---------------------------------------------------------------------------
GAME_META = {
"MW2 2022": {"cod": "cod22", "engine": "iw"},
"MW3 2023": {"cod": "cod23", "engine": "iw"},
"BO6 2024": {"cod": "cod24", "engine": "tr"},
"BO7 2025": {"cod": "cod25", "engine": "tr"},
}
def engine_for(game):
"""'iw' (editable .csb) or 'tr' (read-only .b0), or None for an unknown game."""
return GAME_META.get(game, {}).get("engine")
def expected_file_hint(game):
"""Human hint of the file name for a game, for the manual-pick dialog."""
m = GAME_META.get(game)
if not m:
return "the settings 'save' file"
if m["engine"] == "iw":
return "settings.<N>.pc.%s.csb (the 'save' file inside it)" % m["cod"]
return "g.p.%s.1.0.b0 (the 'save' file inside it)" % m["cod"]
def _iter_connected_storage():
"""Yield (dir_basename, save_path) for every Connected-Storage container holding a 'save'."""
lad = os.environ.get("LOCALAPPDATA", "")
pkgs = os.path.join(lad, "Packages")
if not os.path.isdir(pkgs):
return
for d in os.listdir(pkgs):
if not d.startswith("38985CA0."):
continue
for store in ("wgs", "xgs"):
root = os.path.join(pkgs, d, "SystemAppData", store)
if not os.path.isdir(root):
continue
for base, _dirs, files in os.walk(root):
if "save" in files:
yield os.path.basename(base).lower(), os.path.join(base, "save")
def find_binary_settings(game):
"""Locate the binary settings/profile file for THIS game only. Returns (path, engine);
path is None if not found. Matching is anchored on the game's cod tag so titles never
cross-contaminate."""
m = GAME_META.get(game)
if not m:
return None, None
cod, engine = m["cod"], m["engine"]
matches = []
for name, save in _iter_connected_storage():
if cod not in name:
continue
# IW: the BASE settings file is settings.<digit>.pc.cod..csb; skip the per-mode
# variants (settings.mp./.br./.cp./.dmz./.sp.). TR: the base profile, not the .pm. one.
if engine == "iw" and name.startswith("settings.") and name.endswith(".csb") and name[9:10].isdigit():
matches.append(save)
elif engine == "tr" and name.endswith(".b0") and ".pm." not in name:
matches.append(save)
if not matches:
return None, engine
# With multiple accounts, prefer the most recently modified save.
matches.sort(key=lambda p: os.path.getmtime(p), reverse=True)
return matches[0], engine
def decode_binary(path, engine):
"""Uniform decode for the GUI. Returns {rows, crc_ok, editable, engine}.
IW .csb -> named settings with editable floats (+ CRC). Treyarch .b0 -> the decoded
profile value pool, read-only (hash->name not yet recovered, no CRC self-seal)."""
if engine == "iw":
rows, crc_ok = decode(path)
return {"rows": rows, "crc_ok": crc_ok, "editable": True, "engine": "iw"}
data = open(path, "rb").read()
rows = []
# Named controller floats (deadzones / stick sens / triggers / multipliers). Read-only: the .b0
# has no CRC self-seal + is double-buffered, so we don't write it (change these in-game instead).
frecs = _scan_float_records(data)
for h, name in FLOAT_NAMES.items():
if h in frecs:
off, v = frecs[h]
rows.append({"name": name, "hash": h, "value": v, "kind": "value", "offset": off})
# Enum records: name by id (ENUM_NAMES, verified) first, then by value (VALUE_LABELS).
for i, (o, idv, s) in enumerate(_scan_enum_records(data), 1):
name = ENUM_NAMES.get(idv) or VALUE_LABELS.get(s) or ("Profile value %d" % i)
rows.append({"name": name, "hash": idv, "value": s, "kind": "value", "offset": o})
return {"rows": rows, "crc_ok": None, "editable": False, "engine": "tr"}
def find_csb():
"""Back-compat: locate the MWII settings.*.pc.cod22.csb specifically, or None."""
path, _ = find_binary_settings("MW2 2022")
return path
# ---------------------------------------------------------------------------
# BO7 / cod25 (Treyarch) profile binary: same length-prefixed enum pool as the
# MWII .csb, but a different container (no trailing CRC self-seal). Read-only.
# ---------------------------------------------------------------------------
def decode_bo7_enums(path):
"""Extract the length-prefixed enum control/movement/interaction values from BO7's
profile binary. Values are self-descriptive; hash->name mapping is not yet available."""
vals = _scan_enum_pool(open(path, "rb").read())
return vals
if __name__ == "__main__":
import sys
p = sys.argv[1] if len(sys.argv) > 1 else find_csb()
print("file:", p)
if p:
rows, ok = decode(p)
print(f"CRC valid: {ok}")
for r in rows:
print(f" {r['name']:42s} {r['value']}")