Files
openttd-cmclient/grf/alpine/readgrftest.py
2021-12-12 17:29:44 +03:00

561 lines
25 KiB
Python

import sys
import struct
from nml import lz77
def hex_str(s):
if isinstance(s, (bytes, memoryview)):
return ':'.join('{:02x}'.format(b) for b in s)
return ':'.join('{:02x}'.format(ord(c)) for c in s)
def read_extended_byte(data, offset):
res = data[offset]
if res != 0xff:
return res, offset + 1
return data[offset + 1] | (data[offset + 2] << 8), offset + 3
def read_dword(data, offset):
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24), offset + 4
FEATURES = {
0: 'Train',
1: 'RV',
2: 'Ship',
3: 'Aircraft',
4: 'Station',
5: 'Canal',
6: 'Bridge',
7: 'House',
8: 'Setting',
9: 'IndTiles',
0xa: 'Industry',
0xb: 'Cargo',
0xc: 'Sound',
0xd: 'Airport',
0xe: '?Signals?',
0xf: 'Object',
0x10: 'Railtype',
0x11: 'AirportTiles',
0x12: 'Roadtype',
0x13: 'Tramtype',
}
ACTION0_TRAIN_PROPS = {
0x05: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Track type (see below) should be same as front
0x08: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 AI special flag: set to 1 if engine is 'optimized' for passenger service (AI won't use it for other cargo), 0 otherwise no
0x09: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Speed in mph*1.6 (see below) no
0x0B: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Power (0 for wagons) should be zero
0x0D: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Running cost factor (0 for wagons) should be zero
0x0E: 'D', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Running cost base, see below should be zero
0x12: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Sprite ID (FD for new graphics) yes
0x13: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Dual-headed flag; 1 if dual-headed engine, 0 otherwise should be zero also for front
0x14: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Cargo capacity yes
0x15: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Cargo type, see CargoTypes
0x16: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Weight in tons should be zero
0x17: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 Cost factor should be zero
0x18: 'B', # Supported by OpenTTD <0.7<0.7 Supported by TTDPatch 2.02.0[1] Engine rank for the AI (AI selects the highest-rank engine of those it can buy) no
0x19: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 GRFv≥1 Engine traction type (see below) no
0x1A: 'B*', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 GRFv≥1 Not a property, but an action: sort the purchase list. no
0x1B: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Power added by each wagon connected to this engine, see below should be zero
0x1C: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Refit cost, using 50% of the purchase price cost base yes
0x1D: 'D', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Bit mask of cargo types available for refitting, see column 2 (bit value) in CargoTypes yes
0x1E: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Callback flags bit mask, see below yes
0x1F: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 19)2.5 Coefficient of tractive effort should be zero
0x20: 'B', # Supported by OpenTTD 1.11.1 Supported by TTDPatch 2.5 (alpha 27)2.5 Coefficient of air drag should be zero
0x21: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.02.0 GRFv≥2 Make vehicle shorter by this amount, see below yes
0x22: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Set visual effect type (steam/smoke/sparks) as well as position, see below yes
0x23: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.52.5 GRFv≥6 Set how much weight is added by making wagons powered (i.e. weight of engine), see below should be zero
0x24: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 44)2.5 High byte of vehicle weight, weight will be prop.24*256+prop.16 should be zero
0x25: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 44)2.5 User-defined bit mask to set when checking veh. var. 42 yes
0x26: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 44)2.5 Retire vehicle early, this many years before the end of phase 2 (see Action0General) no
0x27: 'B', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 58)2.5 Miscellaneous flags partly
0x28: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 58)2.5 Refittable cargo classes yes
0x29: 'W', # Supported by OpenTTD 0.60.6 Supported by TTDPatch 2.5 (alpha 58)2.5 Non-refittable cargo classes yes
0x2A: 'D', # Supported by OpenTTD 0.6 (r7191)0.6 Supported by TTDPatch 2.5 (r1210)2.5 Long format introduction date no
0x2B: 'W', # Supported by OpenTTD 1.2 (r22713)1.2 Not supported by TTDPatch Custom cargo ageing period yes
0x2C: 'n*B', # Supported by OpenTTD 1.2 (r23291)1.2 Not supported by TTDPatch List of always refittable cargo types yes
0x2D: 'n*B', # Supported by OpenTTD 1.2 (r23291)1.2 Not supported by TTDPatch List of never refittable cargo types yes
0x2E: 'W', # Supported by OpenTTD 12 (g2183fd4dab)12 Not supported by TTDPatch Maximum curve speed modifier yes
}
ACTION0_OBJECT_PROPS = {
0x08: ('label', 'L'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Class label, see below
0x09: ('class_name_id', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Text ID for class
0x0A: ('name_id', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Text ID for this object
0x0B: ('climate', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Climate availability
0x0C: ('size', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Byte representing size, see below
0x0D: ('build_cost_factor', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Object build cost factor (sets object removal cost factor as well)
0x0E: ('intro_date', 'D'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Introduction date, see below
0x0F: ('eol_date', 'D'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 End of life date, see below
0x10: ('flags', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Object flags, see below
0x11: ('anim_info', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Animation information
0x12: ('anim_speed', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Animation speed
0x13: ('anim_trigger', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Animation triggers
0x14: ('removal_cost_factor', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Object removal cost factor (set after object build cost factor)
0x15: ('cb_flags', 'W'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Callback flags, see below
0x16: ('building_height', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Height of the building
0x17: ('num_views', 'B'), # Supported by OpenTTD 1.1 (r20670)1.1 Supported by TTDPatch 2.6 (r2340)2.6 Number of object views
0x18: ('num_objects', 'B'), # Supported by OpenTTD 1.4 (r25879)1.4 Not supported by TTDPatch Measure for number of objects placed upon map creation
}
ACTION0_PROPS = {
0: ACTION0_TRAIN_PROPS,
0xf: ACTION0_OBJECT_PROPS,
}
def str_feature(feature):
return f'{FEATURES[feature]}<{feature:02x}>'
def str_sprite(sprite):
sprite_id = sprite & 0x1fff
draw = {0: 'N', 1 : 'T', 2: 'R'}[(sprite >> 14) & 3]
color_translation = (sprite >> 16) & 0x3fff
normal_in_transparent = bool(sprite & (1 << 30))
sprite_type = sprite >> 31
ntstr = ['', ' NF'][normal_in_transparent]
return f'[{sprite_id} {draw}{ntstr} {color_translation}-{sprite_type}]'
def read_property(data, ofs, fmt):
if fmt == 'B':
return data[ofs], ofs + 1
if fmt == 'W':
return data[ofs] | (data[ofs + 1] << 8), ofs + 2
if fmt == 'L':
return data[ofs: ofs + 4], ofs + 4
if fmt == 'D':
return read_dword(data, ofs)
if fmt == 'B*':
return read_extended_byte(data, ofs)
if fmt == 'n*B':
n = data[ofs]
return data[ofs + 1: ofs + 1 + n], ofs + 1 + n
assert False, fmt
def decode_action0(data):
num = data[0]
feature = data[0]
num_props = data[1]
num_info = data[2]
first_id, ofs = read_extended_byte(data, 3)
props = {}
for _ in range(num_props):
prop = data[ofs]
propdict = ACTION0_PROPS[feature]
name, fmt = propdict[prop]
value, ofs = read_property(data, ofs + 1, fmt)
key = f'{name}<{prop:02x}>'
assert key not in props, key
props[key] = value
print(f' <0>FEATURE feature:{str_feature(feature)} num_info:{num_info} first_id:{first_id} props:{props}')
def decode_action1(data):
num = data[0]
feature = data[0]
num_sets = data[1]
num_ent, _ = read_extended_byte(data, 2)
print(f' <1>SPRITESET feature:{str_feature(feature)} num_sets:{num_sets} num_ent:{num_ent}')
SPRITE_GROUP_OP = [
'ADD', # a + b
'SUB', # a - b
'SMIN', # (signed) min(a, b)
'SMAX', # (signed) max(a, b)
'UMIN', # (unsigned) min(a, b)
'UMAX', # (unsigned) max(a, b)
'SDIV', # (signed) a / b
'SMOD', # (signed) a % b
'UDIV', # (unsigned) a / b
'UMOD', # (unsigned) a & b
'MUL', # a * b
'AND', # a & b
'OR', # a | b
'XOR', # a ^ b
'STO', # store a into temporary storage, indexed by b. return a
'RST', # return b
'STOP', # store a into persistent storage, indexed by b, return a
'ROR', # rotate a b positions to the right
'SCMP', # (signed) comparison (a < b -> 0, a == b = 1, a > b = 2)
'UCMP', # (unsigned) comparison (a < b -> 0, a == b = 1, a > b = 2)
'SHL', # a << b
'SHR', # (unsigned) a >> b
'SAR', # (signed) a >> b
]
class DataReader:
def __init__(self, data, offset=0):
self.data = data
self.offset = offset
def get_byte(self):
self.offset += 1
return self.data[self.offset - 1]
def get_extended_byte(self):
res, self.offset = read_extended_byte(self.data, self.offset)
return res
def get_word(self):
return self.get_byte() | (self.get_byte() << 8)
def get_var(self, n):
size = 1 << n
res = struct.unpack_from({0: '<B', 1: '<H', 2: '<I'}[n], self.data, offset=self.offset)[0]
self.offset += size
return res
TLF_NOTHING = 0x00
TLF_DODRAW = 0x01 # Only draw sprite if value of register TileLayoutRegisters::dodraw is non-zero.
TLF_SPRITE = 0x02 # Add signed offset to sprite from register TileLayoutRegisters::sprite.
TLF_PALETTE = 0x04 # Add signed offset to palette from register TileLayoutRegisters::palette.
TLF_CUSTOM_PALETTE = 0x08 # Palette is from Action 1 (moved to SPRITE_MODIFIER_CUSTOM_SPRITE in palette during loading).
TLF_BB_XY_OFFSET = 0x10 # Add signed offset to bounding box X and Y positions from register TileLayoutRegisters::delta.parent[0..1].
TLF_BB_Z_OFFSET = 0x20 # Add signed offset to bounding box Z positions from register TileLayoutRegisters::delta.parent[2].
TLF_CHILD_X_OFFSET = 0x10 # Add signed offset to child sprite X positions from register TileLayoutRegisters::delta.child[0].
TLF_CHILD_Y_OFFSET = 0x20 # Add signed offset to child sprite Y positions from register TileLayoutRegisters::delta.child[1].
TLF_SPRITE_VAR10 = 0x40 # Resolve sprite with a specific value in variable 10.
TLF_PALETTE_VAR10 = 0x80 # Resolve palette with a specific value in variable 10.
TLF_KNOWN_FLAGS = 0xFF # Known flags. Any unknown set flag will disable the GRF.
# /** Flags which are still required after loading the GRF. */
TLF_DRAWING_FLAGS = ~TLF_CUSTOM_PALETTE
# /** Flags which do not work for the (first) ground sprite. */
TLF_NON_GROUND_FLAGS = TLF_BB_XY_OFFSET | TLF_BB_Z_OFFSET | TLF_CHILD_X_OFFSET | TLF_CHILD_Y_OFFSET
# /** Flags which refer to using multiple action-1-2-3 chains. */
TLF_VAR10_FLAGS = TLF_SPRITE_VAR10 | TLF_PALETTE_VAR10
# /** Flags which require resolving the action-1-2-3 chain for the sprite, even if it is no action-1 sprite. */
TLF_SPRITE_REG_FLAGS = TLF_DODRAW | TLF_SPRITE | TLF_BB_XY_OFFSET | TLF_BB_Z_OFFSET | TLF_CHILD_X_OFFSET | TLF_CHILD_Y_OFFSET
# /** Flags which require resolving the action-1-2-3 chain for the palette, even if it is no action-1 palette. */
TLF_PALETTE_REG_FLAGS = TLF_PALETTE
def read_sprite_layout_registers(d, flags, is_parent):
regs = {'flags': flags & TLF_DRAWING_FLAGS}
if flags & TLF_DODRAW: regs['dodraw'] = d.get_byte();
if flags & TLF_SPRITE: regs['sprite'] = d.get_byte();
if flags & TLF_PALETTE: regs['palette'] = d.get_byte();
if is_parent:
delta = [d.get_byte(), d.get_byte(), 0] if flags & TLF_BB_XY_OFFSET else [0, 0, 0]
if flags & TLF_BB_Z_OFFSET: delta[2] = d.get_byte()
regs['delta_parent'] = tuple(delta)
else:
delta, delta_set = [0, 0], False
if flags & TLF_CHILD_X_OFFSET: delta[0], delta_set = d.get_byte(), True
if flags & TLF_CHILD_Y_OFFSET: delta[1], delta_set = d.get_byte(), True
if delta_set: regs['delta_child'] = tuple(delta)
if flags & TLF_SPRITE_VAR10: regs['sprite_var10'] = d.get_byte()
if flags & TLF_PALETTE_VAR10: regs['palette_var10'] = d.get_byte()
return regs
def read_sprite_layout(d, num, no_z_position):
has_z_position = not no_z_position
has_flags = bool((num >> 6) & 1)
num &= 0x3f
def read_sprite():
sprite = d.get_word()
pal = d.get_word()
flags = d.get_word() if has_flags else TLF_NOTHING
return {'sprite': sprite, 'pal': pal, 'flags': flags}
ground = read_sprite()
ground_regs = read_sprite_layout_registers(d, ground['flags'], False)
sprites = []
for _ in range(num):
seq = {}
seq['sprite'] = read_sprite()
delta = seq['delta'] = (d.get_byte(), d.get_byte(), d.get_byte() if has_z_position else 0)
is_parent = (delta[2] != 0x80)
if is_parent:
seq['size'] = (d.get_byte(), d.get_byte(), d.get_byte())
seq['regs'] = read_sprite_layout_registers(d, seq['sprite']['flags'], is_parent)
sprites.append(seq)
return {
'ground': {
'sprite': ground,
'regs': ground_regs,
},
'sprites': sprites
}
def decode_action2(data):
feature = data[0]
set_id = data[1]
num_ent1 = data[2]
d = DataReader(data, 3)
print(f' <2>SPRITEGROUP feature:{str_feature(feature)} set_id:{set_id} ', end='')
if feature in (0x07, 0x09, 0x0f, 0x11):
if num_ent1 == 0:
ground_sprite, building_sprite, xofs, yofs, xext, yext, zext = struct.unpack_from('<IIBBBBB', data, offset=3)
ground_sprite = str_sprite(ground_sprite)
building_sprite = str_sprite(building_sprite)
print(f'ground_sprite:{ground_sprite} building_sprite:{building_sprite} '
f'xofs:{xofs} yofs:{yofs} extent:({xext}, {yext}, {zext})')
return
if num_ent1 < 0x3f:
raise NotImplemented
if num_ent1 in (0x81, 0x82, 0x85, 0x86, 0x89, 0x8a):
group_size = (num_ent1 >> 2) & 3
first = True
ofs = 3
adjusts = []
while True:
res = {}
res['op'] = 0 if first else d.get_byte()
var = res['var'] = d.get_byte()
if var == 0x7e:
res['subroutine'] = d.get_byte()
else:
res['parameter'] = d.get_byte() if 0x60 <= var < 0x80 else 0
varadj = d.get_byte()
res['shift_num'] = varadj & 0x1f
has_more = bool(varadj & 0x20)
res['type'] = varadj >> 6
res['and_mask'] = d.get_var(group_size)
if res['type'] != 0:
res['add_val'] = d.get_var(group_size)
res['divmod_val'] = d.get_var(group_size)
adjusts.append(res)
if not has_more:
break
n_ranges = d.get_byte()
ranges = []
for _ in range(n_ranges):
group = d.get_word()
low = d.get_var(group_size)
high = d.get_var(group_size)
ranges.append((group, low, high))
default_group = d.get_word()
print(f'default_group: {default_group} adjusts:{adjusts} ranges:{ranges} ')
return
layout = read_sprite_layout(d, max(num_ent1, 1), num_ent1 == 0)
print(f'layout:{layout}')
return
# num_loaded = num_ent1
# num_loading = get_byte()
# [get_word() for i in range(num_loaded)]
# [get_word() for i in range(num_loading)]
# assert False, num_ent1
# # assert num_ent1 < 0x3f + 0x40, num_ent1
# return
num_ent2 = data[3]
ent1 = struct.unpack_from('<' + 'H' * num_ent1, data, offset=4)
ent2 = struct.unpack_from('<' + 'H' * num_ent2, data, offset=4 + 2 * num_ent1)
print(f'ent1:{ent1} ent2:{ent2}')
def decode_action4(data):
fmt = '<BBB' + ('H' if data[1] & 0xf0 else 'B')
feature, lang, num, offset = struct.unpack_from(fmt, data)
strings = [s.decode('utf-8') for s in data[struct.calcsize(fmt):].split(b'\0')[:-1]]
print(f' <4>STRINGS feature:{str_feature(feature)} lang:{lang} num:{num} offset:{offset} strings:{strings}')
def decode_action5(data):
t = data[0]
offset = 0
num, dataofs = read_extended_byte(data, 1)
if t & 0xf0:
offset, _ = read_extended_byte(data, dataofs)
t &= ~0xf0
print(f' <5>REPLACENEW type:{t} num:{num}, offset:{offset}')
def decode_action6(data):
d = DataReader(data, 0)
params = []
while True:
param_num = d.get_byte()
if param_num == 0xFF:
break
param_size = d.get_byte()
offset = d.get_extended_byte()
params.append({'num': param_num, 'size': param_size, 'offset': offset})
print(f' <6>EDITPARAM params:{params}')
def decode_actionA(data):
num = data[0]
sets = [struct.unpack_from('<BH', data, offset=3*i + 1) for i in range(num)]
print(f' <A>REPLACEBASE sets:<{num}>{sets}')
OPERATIONS = {
0x00: '{target} = {source1}', # Supported by OpenTTD Supported by TTDPatch Assignment target = source1
0x01: '{target} = {source1} + {source2}', # Supported by OpenTTD Supported by TTDPatch Addition target = source1 + source2
0x02: '{target} = {source1} - {source2}', # Supported by OpenTTD Supported by TTDPatch Subtraction target = source1 - source2
0x03: '{target} = {source1} * {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch Unsigned multiplication target = source1 * source2, with both sources being considered to be unsigned
0x04: '{target} = {source1} * {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch Signed multiplication target = source1 * source2, with both sources considered signed
0x05: '{target} = {source1} <</>> {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch Unsigned bit shift target = source1 << source2 if source2>0, or target = source1 >> abs(source2) if source2 < 0. source1 is considered to be unsigned
0x06: '{target} = {source1} <</>> {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch Signed bit shift same as 05, but source1 is considered signed)
0x07: '{target} = {source1} & {source2}', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 48)2.5 Bitwise AND target = source1 AND source2
0x08: '{target} = {source1} | {source2}', # Suported by OpenTTD Supported by TTDPatch 2.5 (alpha 48)2.5 Bitwise OR target = source1 OR source2
0x09: '{target} = {source1} / {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Unsigned division target = source1 / source2
0x0A: '{target} = {source1} / {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Signed division target = source1 / source2
0x0B: '{target} = {source1} % {source2} (Unsigned)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Unsigned modulo target = source1 % source2
0x0C: '{target} = {source1} % {source2} (Signed)', # Supported by OpenTTD Supported by TTDPatch 2.5 (alpha 59)2.5 Signed modulo target = source1 % source2
}
def decode_actionD(data):
target = data[0]
operation = data[1]
source1 = data[2]
source2 = data[3]
if source1 == 0xff or source2 == 0xff:
value, _ = read_dword(data, 4)
fmt = OPERATIONS[operation]
sf = lambda x: f'[{x:02x}]' if x != 0xff else str(value)
target_str = f'[{target:02x}]'
op_str = fmt.format(target=target_str, source1=sf(source1), source2=sf(source2))
print(f' <A>OP {op_str}')
def decode_action14(data):
res = {}
ofs = 0
def decode_chunk(res):
nonlocal ofs
chunk_type = data[ofs]
ofs += 1
if chunk_type == 0: return False
chunk_id = data[ofs: ofs + 4]
ofs += 4
if chunk_type == b'C'[0]:
res[chunk_id] = {}
while decode_chunk(res[chunk_id]):
pass
elif chunk_type == b'B'[0]:
l = data[ofs] | (data[ofs + 1] << 8)
res[chunk_id] = data[ofs + 2: ofs + 2 + l]
ofs += 2 + l
# elif chunk_type == b'T'[0]:
else:
assert False, chunk_type
return True
while decode_chunk(res):
pass
print(f' <14>INFO {res}')
ACTIONS = {
0x00: decode_action0,
0x01: decode_action1,
0x02: decode_action2,
0x04: decode_action4,
0x05: decode_action5,
0x06: decode_action6,
0x0a: decode_actionA,
0x0d: decode_actionD,
0x14: decode_action14,
}
def read_pseudo_sprite(f):
l = struct.unpack('<I', f.read(4))[0]
if l == 0:
print('End of pseudo sprites')
return False
grf_type = f.read(1)[0]
grf_type_str = hex(grf_type)[2:]
data = f.read(l)
print(f'Sprite({l}, {grf_type_str}): ', hex_str(data[:100]))
if grf_type == 0xff:
decoder = ACTIONS.get(data[0])
if decoder:
decoder(data[1:])
return True
def decode_sprite(f, num):
data = b''
while num > 0:
code = f.read(1)[0]
if code >= 128: code -= 256
# print(f'Code {code} num {num}')
if code >= 0:
size = 0x80 if code == 0 else code
num -= size
if num < 0: raise RuntimeError('Corrupt sprite')
data += f.read(size)
else:
data_offset = ((code & 7) << 8) | f.read(1)[0]
#if (dest - data_offset < dest_orig.get()) return WarnCorruptSprite(file, file_pos, __LINE__);
size = -(code >> 3)
num -= size
if num < 0: raise RuntimeError('Corrupt sprite')
data += data[-data_offset:size - data_offset]
if num != 0: raise RuntimeError('Corrupt sprite')
return data
def read_real_sprite(f):
sprite_id = struct.unpack('<I', f.read(4))[0]
if sprite_id == 0:
print(f'End of real sprites')
return False
print(f'Real sprite({sprite_id}): ', end='')
num, t = struct.unpack('<IB', f.read(5))
start_pos = f.tell()
print(f'({num}, {t:02x}): ', end='')
if t == 0xff:
print('non-real (skip)')
f.seek(start_pos + num - 1, 0)
return True
zoom, height, width, x_offs, y_offs = struct.unpack('<BHHhh', f.read(9))
bpp = 1 # TODO
decomp_size = struct.unpack('<I', f.read(4))[0] if t & 0x08 else width * height * bpp
print(f'{width}x{height} zoom={zoom} x_offs={x_offs} y_offs={y_offs} bpp={bpp} decomp_size={decomp_size}')
# data = decode_sprite(f, decomp_size)
# print('Data: ', hex_str(data[:40]))
f.seek(start_pos + num - 1, 0)
return True
with open(sys.argv[1], 'rb') as f:
print('Header:', hex_str(f.read(10)))
data_offest, compression = struct.unpack('<IB', f.read(5))
header_offset = f.tell() - 1
print(f'Offset: {data_offest} compresion: {compression}')
while read_pseudo_sprite(f):
pass
real_data_offset = f.tell() - header_offset
# while read_real_sprite(f):
# pass
if data_offest != real_data_offset:
print(f'[ERROR] Data offset check failed: {data_offest} {real_data_offset}')