Files
OpenFUT/fifa17-recon/docker/fifa17-python/tools/heat2.py
T
root 70a64e3709 fifa17-python: commit working FUT backend deployment (client/server split)
Freeze the running offline FUT backend into version control as
fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a
fresh checkout:

* OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders
  (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is
  required for remote mode (compose and entrypoint fail without it)
* docker-compose.yml reproducing the frozen baseline container exactly
  (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart)
* .env.example / .env for site config - the LAN IP is never hardcoded in source
* tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10,
  verified byte-identical to the running container at freeze time
* client_arm.sh (the 105 client-side arming counterpart)
* Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying
* docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record,
  restore instructions and rebuild-equivalence procedure

Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored.
The live container is untouched pending the .105 launcher audit.
2026-08-10 23:54:04 +00:00

479 lines
16 KiB
Python

#!/usr/bin/env python3
"""
heat2.py -- self-contained Blaze Heat2 TDF encoder/decoder + Fire2 framing.
CLEAN ROOM PROVENANCE
---------------------
Everything here was derived from:
* the wire bytes of our own FIFA17 client's first Blaze RPC
(fifa17-recon/captures/blaze/blaze_fire2_37161.bin), and
* our own decoder (decode_fire2.py) written against those bytes.
Complex-type layouts that the capture does NOT exercise (list/map/union/
varintlist/objtype/objid/float) are marked UNVERIFIED below; they are
consistent with independent third-party clean-room BlazeSDK-15.x
reimplementations (the `tdf` crate cloned in this scratchpad), which were used
only as a cross-check of *structure*, never copied.
NO EA/FIFA leaked source was consulted.
VALIDATED RULES (byte-exact round-trip against the 219-byte capture)
--------------------------------------------------------------------
Fire2 frame header, 16 bytes big-endian:
[0:4] u32 payload length (bytes after the header)
[4:6] u16 always 0 (observed)
[6:8] u16 component
[8:10] u16 command
[10:12]u16 error / msgId
[12] u8 msgType (0x01 ping, 0x02 request, 0x03 pong/response)
[13:16]3 reserved bytes (observed 00 00 00)
Heat2 field = 3-byte packed tag + 1 type byte + value.
TAG PACKING (validated):
Take the 4-char label, right-pad with spaces to exactly 4 chars, truncate
to 4. Each char c -> 6-bit code (ord(c) - 0x20) & 0x3F (so ' ' -> 0).
The four 6-bit codes are concatenated MSB-first into 24 bits = 3 bytes:
b0 = c0<<2 | c1>>4
b1 = (c1 & 0x0F)<<4 | c2>>2
b2 = (c2 & 0x03)<<6 | c3
Decode is the exact inverse; code 0 decodes to ' ' and trailing spaces are
stripped, so "ENV" round-trips as "ENV" (encoded as "ENV ").
VARINT (validated):
First byte carries only 6 data bits (mask 0x3F); bit 0x80 = "more".
Bit 0x40 of the first byte is the sign/negative flag (UNVERIFIED - never
set in our capture; we encode non-negative values only by default).
Every following byte carries 7 data bits (mask 0x7F) with bit 0x80 = more.
Little-endian group order: first byte = least significant 6 bits, then
7 bits per byte at shifts 6, 13, 20, 27, ...
Canonical form: emit the shortest sequence; value < 0x40 is one byte.
e.g. LANG = 0x656E5553 ("enUS") -> 93 d5 f2 d6 0c
STRING (validated):
varint length INCLUDING the NUL terminator, then that many bytes, the last
of which is 0x00. Empty string = varint 1 + b"\\x00".
STRUCT / group (validated):
type byte 0x03, then the member fields, then a single 0x00 terminator
byte. No group-start marker byte. The top-level payload is NOT
terminated (it is delimited by the Fire2 length).
FIELD ORDER (validated):
Members are serialized in ascending order of the *packed 3-byte tag*
(equivalently ascending by the space-padded label under this 6-bit
packing). Observed: CDAT<CINF<FCCR<LADD, and inside CINF:
BSDK<BTIM<CLNT<CPFT<CSKU<CVER<DSDK<ENV<LOC<PTVR.
VALUE REPRESENTATION (python side)
----------------------------------
A struct is an ordered dict { "TAG": (type, value) }.
INT -> int
STRING -> str (no trailing NUL) or bytes
BLOB -> bytes
STRUCT -> dict as above
LIST -> (elem_type, [value, ...])
MAP -> (key_type, val_type, [(k, v), ...])
UNION -> (active_key:int, (tag, type, value) | None)
VARLIST -> [int, ...]
OBJTYPE -> (component, type)
OBJID -> (component, type, id)
FLOAT -> float
"""
from __future__ import annotations
import struct
from collections import OrderedDict
# ---------------------------------------------------------------- types
INT = 0x00
STRING = 0x01
BLOB = 0x02
STRUCT = 0x03
LIST = 0x04
MAP = 0x05
UNION = 0x06
VARLIST = 0x07
OBJTYPE = 0x08
OBJID = 0x09
FLOAT = 0x0A
TYPE_NAMES = {
INT: "int", STRING: "string", BLOB: "blob", STRUCT: "struct",
LIST: "list", MAP: "map", UNION: "union", VARLIST: "varintlist",
OBJTYPE: "objtype", OBJID: "objid", FLOAT: "float",
}
UNION_UNSET = 0x7F # UNVERIFIED (not present in capture)
# ---------------------------------------------------------------- tags
def encode_tag(label) -> bytes:
"""4-char label -> 3 packed bytes. Shorter labels are space padded."""
if isinstance(label, bytes):
label = label.decode("ascii")
s = (label + " ")[:4]
c = [(ord(ch) - 0x20) & 0x3F for ch in s]
return bytes((
(c[0] << 2) | (c[1] >> 4),
((c[1] & 0x0F) << 4) | (c[2] >> 2),
((c[2] & 0x03) << 6) | c[3],
))
def decode_tag(b: bytes) -> str:
"""3 packed bytes -> label with trailing padding stripped."""
a, b1, c = b[0], b[1], b[2]
v = (
(a >> 2) & 0x3F,
((a & 0x03) << 4) | ((b1 >> 4) & 0x0F),
((b1 & 0x0F) << 2) | ((c >> 6) & 0x03),
c & 0x3F,
)
return "".join(chr(x + 0x20) if x else " " for x in v).rstrip()
def tag_key(label) -> bytes:
"""Sort key enforcing Blaze's ascending-tag member ordering."""
return encode_tag(label)
# ---------------------------------------------------------------- varint
def encode_varint(value: int) -> bytes:
"""Heat2 varint: 6 data bits in byte 0 (0x80=more), 7 bits thereafter."""
neg = value < 0
v = -value if neg else value
first = v & 0x3F
v >>= 6
if neg:
first |= 0x40 # UNVERIFIED sign convention
if v == 0:
return bytes((first,))
out = bytearray((first | 0x80,))
while v >= 0x80:
out.append((v & 0x7F) | 0x80)
v >>= 7
out.append(v)
return bytes(out)
def decode_varint(buf: bytes, i: int):
"""-> (value, next_index)"""
b = buf[i]
i += 1
val = b & 0x3F
neg = bool(b & 0x40)
if b & 0x80:
shift = 6
while True:
b = buf[i]
i += 1
val |= (b & 0x7F) << shift
shift += 7
if not (b & 0x80):
break
return (-val if neg else val), i
# ---------------------------------------------------------------- encoder
def _enc_value(typ: int, value, out: bytearray) -> None:
if typ == INT:
out += encode_varint(int(value))
elif typ == STRING:
raw = value.encode("utf-8") if isinstance(value, str) else bytes(value)
raw = raw.rstrip(b"\x00")
out += encode_varint(len(raw) + 1)
out += raw
out += b"\x00"
elif typ == BLOB:
raw = bytes(value)
out += encode_varint(len(raw))
out += raw
elif typ == STRUCT:
_enc_struct_body(value, out)
out += b"\x00"
elif typ == LIST: # UNVERIFIED
etype, items = value
out.append(etype & 0xFF)
out += encode_varint(len(items))
for it in items:
_enc_value(etype, it, out)
elif typ == MAP: # UNVERIFIED
ktype, vtype, items = value
out.append(ktype & 0xFF)
out.append(vtype & 0xFF)
out += encode_varint(len(items))
for k, v in items:
_enc_value(ktype, k, out)
_enc_value(vtype, v, out)
elif typ == UNION: # UNVERIFIED
key, member = value
out.append(key & 0xFF)
if key != UNION_UNSET and member is not None:
mtag, mtype, mval = member
out += encode_tag(mtag)
out.append(mtype & 0xFF)
_enc_value(mtype, mval, out)
elif typ == VARLIST: # UNVERIFIED
out += encode_varint(len(value))
for n in value:
out += encode_varint(int(n))
elif typ == OBJTYPE: # UNVERIFIED
comp, t = value
out += encode_varint(comp)
out += encode_varint(t)
elif typ == OBJID: # UNVERIFIED
comp, t, oid = value
out += encode_varint(comp)
out += encode_varint(t)
out += encode_varint(oid)
elif typ == FLOAT: # UNVERIFIED
out += struct.pack(">f", float(value))
else:
raise ValueError("cannot encode unknown TDF type 0x%02x" % typ)
def _enc_struct_body(fields, out: bytearray) -> None:
"""Serialize members in ascending packed-tag order (Blaze requirement)."""
if isinstance(fields, dict):
items = list(fields.items())
else: # allow [(tag, (type, value)), ...]
items = list(fields)
items.sort(key=lambda kv: tag_key(kv[0]))
for tag, tv in items:
typ, val = tv
out += encode_tag(tag)
out.append(typ & 0xFF)
_enc_value(typ, val, out)
def encode_tdf(fields) -> bytes:
"""Serialize a top-level TDF struct body (no trailing 0x00 terminator)."""
out = bytearray()
_enc_struct_body(fields, out)
return bytes(out)
# convenient aliases
build_tdf = encode_tdf
encode_struct = encode_tdf
# ---------------------------------------------------------------- decoder
def _dec_value(buf: bytes, i: int, typ: int):
if typ == INT:
return decode_varint(buf, i)
if typ == STRING:
ln, i = decode_varint(buf, i)
raw = buf[i:i + ln]
i += ln
return raw.rstrip(b"\x00").decode("utf-8", "replace"), i
if typ == BLOB:
ln, i = decode_varint(buf, i)
return bytes(buf[i:i + ln]), i + ln
if typ == STRUCT:
return _dec_struct_body(buf, i, terminated=True)
if typ == LIST:
etype = buf[i]; i += 1
n, i = decode_varint(buf, i)
items = []
for _ in range(n):
v, i = _dec_value(buf, i, etype)
items.append(v)
return (etype, items), i
if typ == MAP:
ktype = buf[i]; i += 1
vtype = buf[i]; i += 1
n, i = decode_varint(buf, i)
items = []
for _ in range(n):
k, i = _dec_value(buf, i, ktype)
v, i = _dec_value(buf, i, vtype)
items.append((k, v))
return (ktype, vtype, items), i
if typ == UNION:
key = buf[i]; i += 1
if key == UNION_UNSET:
return (key, None), i
mtag = decode_tag(buf[i:i + 3]); mtype = buf[i + 3]; i += 4
mval, i = _dec_value(buf, i, mtype)
return (key, (mtag, mtype, mval)), i
if typ == VARLIST:
n, i = decode_varint(buf, i)
out = []
for _ in range(n):
v, i = decode_varint(buf, i)
out.append(v)
return out, i
if typ == OBJTYPE:
c, i = decode_varint(buf, i)
t, i = decode_varint(buf, i)
return (c, t), i
if typ == OBJID:
c, i = decode_varint(buf, i)
t, i = decode_varint(buf, i)
o, i = decode_varint(buf, i)
return (c, t, o), i
if typ == FLOAT:
return struct.unpack(">f", buf[i:i + 4])[0], i + 4
raise ValueError("cannot decode unknown TDF type 0x%02x at %d" % (typ, i))
def _dec_struct_body(buf: bytes, i: int, terminated: bool, end: int = None):
"""Read fields until 0x00 terminator (nested) or `end` (top level)."""
if end is None:
end = len(buf)
fields = OrderedDict()
while i < end:
if terminated and buf[i] == 0x00:
i += 1
break
tag = decode_tag(buf[i:i + 3])
typ = buf[i + 3]
i += 4
val, i = _dec_value(buf, i, typ)
fields[tag] = (typ, val)
return fields, i
def decode_tdf(payload: bytes):
"""Decode a top-level TDF payload -> OrderedDict {tag: (type, value)}."""
fields, _ = _dec_struct_body(payload, 0, terminated=False)
return fields
# ---------------------------------------------------------------- Fire2
FIRE2_HEADER_LEN = 16
MSG_PING = 0x01
MSG_REQUEST = 0x02
MSG_RESPONSE = 0x03 # also seen as pong
MSG_NOTIFY = 0x04 # UNVERIFIED
MSG_ERROR = 0x05 # UNVERIFIED
def build_fire2_frame(component: int, command: int, msgType: int,
msgId: int, tdf_bytes: bytes) -> bytes:
"""16-byte big-endian Fire2 header + TDF payload."""
tdf_bytes = bytes(tdf_bytes)
hdr = struct.pack(">IHHHHB3s", len(tdf_bytes), 0, component & 0xFFFF,
command & 0xFFFF, msgId & 0xFFFF, msgType & 0xFF,
b"\x00\x00\x00")
return hdr + tdf_bytes
def parse_fire2_frame(data: bytes):
"""-> (dict header, bytes payload). Raises if the buffer is short."""
if len(data) < FIRE2_HEADER_LEN:
raise ValueError("short Fire2 frame")
(ln, zero, comp, cmd, msgid, mtype, reserved) = struct.unpack(
">IHHHHB3s", data[:FIRE2_HEADER_LEN])
payload = data[FIRE2_HEADER_LEN:FIRE2_HEADER_LEN + ln]
if len(payload) != ln:
raise ValueError("truncated Fire2 payload: want %d have %d"
% (ln, len(payload)))
hdr = {
"length": ln, "zero": zero, "component": comp, "command": cmd,
"msgId": msgid, "msgType": mtype, "reserved": reserved,
}
return hdr, payload
def decode_fire2(data: bytes):
"""-> (header dict, decoded TDF OrderedDict)"""
hdr, payload = parse_fire2_frame(data)
return hdr, decode_tdf(payload)
def encode_fire2(hdr: dict, fields) -> bytes:
"""Inverse of decode_fire2 (uses hdr's component/command/msgType/msgId)."""
return build_fire2_frame(hdr["component"], hdr["command"],
hdr["msgType"], hdr["msgId"], encode_tdf(fields))
# ---------------------------------------------------------------- pretty
def dump(fields, depth: int = 0) -> str:
pad = " " * depth
lines = []
for tag, (typ, val) in fields.items():
tn = TYPE_NAMES.get(typ, "0x%02x" % typ)
if typ == STRUCT:
lines.append("%s%s (struct) {" % (pad, tag))
lines.append(dump(val, depth + 1))
lines.append("%s}" % pad)
elif typ == BLOB:
lines.append("%s%s (blob[%d]) = %s" % (pad, tag, len(val), val.hex()))
else:
lines.append("%s%s (%s) = %r" % (pad, tag, tn, val))
return "\n".join(lines)
# ---------------------------------------------------------------- self-test
CAPTURE = ("/home/alex/Documents/OpenFUT/fifa17-recon/captures/blaze/"
"blaze_fire2_37161.bin")
def _selftest(path: str = CAPTURE) -> bool:
ok = True
# unit: tag packing
for lbl in ("CDAT", "CINF", "FCCR", "LADD", "ENV", "LOC", "BSDK", "PTVR"):
enc = encode_tag(lbl)
assert decode_tag(enc) == lbl, (lbl, enc.hex())
assert encode_tag("CDAT") == bytes.fromhex("8e4874"), encode_tag("CDAT").hex()
assert encode_tag("ENV") == bytes.fromhex("96ed80"), encode_tag("ENV").hex()
assert encode_tag("LADD") == bytes.fromhex("b21924"), encode_tag("LADD").hex()
# unit: varint
assert encode_varint(0) == b"\x00"
assert encode_varint(4) == b"\x04"
assert encode_varint(0x3F) == b"\x3f"
assert encode_varint(0x40) == bytes.fromhex("8001")
assert encode_varint(0x656E5553) == bytes.fromhex("93d5f2d60c")
for n in (0, 1, 63, 64, 127, 128, 8191, 0x656E5553, 2**40, 2**63 - 1):
v, j = decode_varint(encode_varint(n), 0)
assert v == n and j == len(encode_varint(n)), n
# round trip the real capture
original = open(path, "rb").read()
hdr, payload = parse_fire2_frame(original)
fields = decode_tdf(payload)
re_payload = encode_tdf(fields)
re_frame = encode_fire2(hdr, fields)
print("header:", hdr)
print(dump(fields))
print()
print("payload %d -> %d bytes" % (len(payload), len(re_payload)))
print("frame %d -> %d bytes" % (len(original), len(re_frame)))
if re_frame == original:
print("ROUND-TRIP: PASS (byte-identical, %d bytes)" % len(original))
else:
ok = False
print("ROUND-TRIP: FAIL")
n = min(len(re_frame), len(original))
for k in range(n):
if re_frame[k] != original[k]:
print(" first diff at 0x%04x: got %02x want %02x"
% (k, re_frame[k], original[k]))
print(" got %s" % re_frame[max(0, k - 8):k + 16].hex())
print(" want %s" % original[max(0, k - 8):k + 16].hex())
break
else:
print(" length differs only: %d vs %d" % (len(re_frame), len(original)))
return ok
if __name__ == "__main__":
import sys
p = sys.argv[1] if len(sys.argv) > 1 else CAPTURE
raise SystemExit(0 if _selftest(p) else 1)