Repository navigation
Expand file tree
/
Copy pathgamedata.py
More file actions
123 lines (106 loc) · 5 KB
/
Copy pathgamedata.py
File metadata and controls
123 lines (106 loc) · 5 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
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2026 Nexxus Drako Multimedia
"""Reader for GAME/GAMEDATA.BIN of Let's Make a Soccer Team! (PS2): the match
engine's table of 4,131 0x30-byte records, each a way of touching the
ball: a kick request names one (+0x1c). See DOC/GAMEDATA_FORMAT.md.
Confirmed from GAMEPRG.REL: the file is loader slot 10 (0x5378); record n
is data + n * 0x30 (0x13abb8). 0x13abf0 / 0x13ac20 / 0x13ac58 compare a
record's s16 at +0x4 with an index and with other records' +0x4 (what it
means isn't settled: in records 0-41 it pairs them, 4 <-> 5, 6 <-> 7, but
later records hold small shared values); 0x13ab80 reads the s8 at +0x8 as
an angle in units of pi/32; 0x13ad00 reads s16 +0x20 / +0x22 as a range.
The kick code reads the body part, bits 16-18 of the u32 at +0xc (0x98e88:
0 the right and 1 the left foot, 2 and up scaled by the header
parameter), and the surface at +0x25 (0x45f74), an index into the kick kind's variant names
(0x58000). Empirically parts 0-4 occur and surfaces 0-16 or 255 (none).
Usage:
python gamedata.py info <GAMEDATA.BIN | dir>
python gamedata.py dump <GAMEDATA.BIN> [first [last]]
"""
import math
import os
import struct
import sys
FILE = "GAMEDATA.BIN"
RECORD = 0x30
PARTNER, ANGLE, PART_WORD, SPEED, RANGE, SURFACE = 0x4, 0x8, 0xC, 0x14, 0x20, 0x25
# Parts 0 and 1 are the right and left foot: the kick code (0x98e88) scales
# a kick by the weak foot when its part is the one 0x97be0 maps the player's
# leg to (table 0x26e060: left-footed 0, right-footed 1, two-footed none),
# and 0x97c00 maps the leg to the strong foot (0x26e070: 1, 0, 2 both).
# 2-4 are named from the surfaces each uses (empirical).
PARTS = ("right foot", "left foot", "head", "chest", "other")
# The variant names of the kick kinds' lists (GAMEPRG.REL 0x2680cc on).
SURFACES = ("INSIDE", "OUTSIDE", "HEEL", "SOLE", "TOE", "INSTEP", "INFRONT",
"OUTFRONT", "CHEST", "HEAD_STRONG", "HEAD_WEAK", "SLIDING", "CHIP",
"OVERHEAD", "JUMPING_VOLLEY", "HEEL_VOLLEY", "DIVING_HEAD")
NO_SURFACE = 0xFF
ANGLE_UNIT = math.pi / 32 # 0x3dc90fdc at 0x13ab94
def load(path):
if os.path.isdir(path):
path = os.path.join(path, FILE)
with open(path, "rb") as f:
data = f.read()
if len(data) % RECORD:
raise ValueError("%d bytes, not a whole number of 0x%x-byte records" % (len(data), RECORD))
return path, [data[i:i + RECORD] for i in range(0, len(data), RECORD)]
def fields(rec):
partner = struct.unpack_from("<h", rec, PARTNER)[0]
angle = struct.unpack_from("<b", rec, ANGLE)[0]
speed = struct.unpack_from("<f", rec, SPEED)[0]
lo, hi = struct.unpack_from("<hh", rec, RANGE)
return partner, angle, speed, (lo, hi)
def contact(rec):
"""(body part, surface) of a record."""
return (struct.unpack_from("<I", rec, PART_WORD)[0] >> 16) & 7, rec[SURFACE]
def cmd_info(path):
"""The record count and the body-part and surface tallies; a part above
4 or a surface past the names (other than 255) is marked."""
try:
path, recs = load(path)
except (ValueError, OSError) as e:
print("%s !! %s" % (path, e))
return
angles = sorted(set(fields(r)[1] for r in recs))
print("%s %d records of 0x%x, angles %d..%d (pi/32)" % (
path, len(recs), RECORD, angles[0], angles[-1]))
parts = {}
for i, r in enumerate(recs):
part, surface = contact(r)
parts.setdefault(part, {}).setdefault(surface, 0)
parts[part][surface] += 1
bad = []
if part >= len(PARTS):
bad.append("body part %d" % part)
if surface >= len(SURFACES) and surface != NO_SURFACE:
bad.append("surface %d" % surface)
if bad:
print(" record %d !! %s" % (i, "; ".join(bad)))
for part in sorted(parts):
tally = sorted(parts[part].items(), key=lambda kv: -kv[1])
print(" part %d %-10s %5d records: %s" % (
part, PARTS[part] if part < len(PARTS) else "?", sum(parts[part].values()),
", ".join("%s %d" % (SURFACES[s] if s < len(SURFACES) else s, n) for s, n in tally)))
def cmd_dump(path, first, last):
path, recs = load(path)
for i in range(first, min(last, len(recs) - 1) + 1):
partner, angle, speed, (lo, hi) = fields(recs[i])
part, surface = contact(recs[i])
print("%4d +0x4 %4d angle %4d (%6.1f deg) +0x14 %6.2f range %d..%d part %d %-14s %s" % (
i, partner, angle, math.degrees(angle * ANGLE_UNIT), speed, lo, hi, part,
SURFACES[surface] if surface < len(SURFACES) else "-", recs[i].hex(" ")))
def main(argv):
args = argv[2:]
cmd = argv[1] if len(argv) > 1 else ""
if cmd == "info" and len(args) == 1:
cmd_info(args[0])
elif cmd == "dump" and 1 <= len(args) <= 3:
first = int(args[1], 0) if len(args) > 1 else 0
last = int(args[2], 0) if len(args) > 2 else first + 19
cmd_dump(args[0], first, last)
else:
print(__doc__)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))