Skip to content

Commit 63647f7

Browse files
committed
feat(aiv): working roundtrip between aiv and json, needs adjustment, tile offsets are slightly off
1 parent 2ca5011 commit 63647f7

5 files changed

Lines changed: 616 additions & 197 deletions

File tree

resources/aiv/empty.aiv

52.6 KB
Binary file not shown.

sourcehold/tool/convert/aiv/__init__.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pathlib
22

3-
from .conversion import to_json
3+
from sourcehold.tool.convert.aiv.exports import to_json
4+
from sourcehold.tool.convert.aiv.imports import from_json
45

56

67
def convert_aiv(args):
@@ -19,16 +20,32 @@ def convert_aiv(args):
1920
if not inp_format:
2021
if inp.endswith(".aiv"):
2122
inp_format = 'aiv'
23+
elif inp.endswith(".json"):
24+
inp_format = "json"
2225
out_format = args.to_format
23-
if inp_format == "aiv":
24-
out_format = "json"
26+
if not out_format:
27+
if inp_format == "aiv":
28+
out_format = "json"
29+
elif inp_format == "json":
30+
out_format = "aiv"
2531
if args.output == None:
26-
args.output = f"{pathlib.Path(inp).name}.json"
32+
if out_format == "json":
33+
args.output = "-"
34+
elif out_format == "aiv":
35+
args.output = f"{pathlib.Path(inp).name}.aiv"
36+
#args.output = f"{pathlib.Path(inp).name}.json"
2737

2838
if args.debug:
2939
print(f"converting '{inp_format}' file '{inp}' to '{out_format}' file '{args.output}'")
3040
if inp_format == 'aiv' and out_format == "json":
31-
pathlib.Path(args.output).write_text(to_json(path = inp, include_extra=args.extra, report=args.debug))
41+
conv = to_json(path = inp, include_extra=args.extra, report=args.debug)
42+
if args.output == "-":
43+
print(conv)
44+
else:
45+
pathlib.Path(args.output).write_text(conv)
46+
elif inp_format == "json" and out_format == "aiv":
47+
conv = from_json(path = inp, report=args.debug)
48+
conv.to_file(args.output)
3249
else:
3350
raise NotImplementedError(f"combination of in-format '{inp_format}' and out-format '{out_format}' not implemented")
3451

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import sys
2+
3+
from sourcehold.tool.convert.aiv.info import *
4+
try:
5+
import sourcehold
6+
except:
7+
import os, pathlib
8+
sys.path.insert(0, str(pathlib.Path(".").parent))
9+
10+
import sourcehold.aivs
11+
from sourcehold.aivs.AIV import AIV
12+
13+
# from matplotlib import pyplot
14+
15+
import struct
16+
17+
import numpy as np
18+
19+
import json
20+
21+
22+
23+
24+
def to_json(aiv=None, path: str='', include_extra=False, invert_y=True, report=False):
25+
if aiv == None and not path:
26+
raise Exception()
27+
if aiv == None:
28+
aiv = AIV().from_file(path)
29+
30+
if report:
31+
print(f"INFO: aiv has version: {aiv.directory.version_number}")
32+
33+
select_all = np.ones((100,100), 'bool')
34+
35+
constructions = get_constructions_matrix()
36+
constructions[select_all] = struct.unpack(CONSTRUCTIONS_STRUCT_FORMAT, aiv.directory[INDEX_CONSTRUCTIONS].get_data())
37+
38+
steps = get_steps_matrix()
39+
steps[select_all] = struct.unpack(STEPS_STRUCT_FORMAT, aiv.directory[INDEX_STEPS].get_data())
40+
41+
# This value can lie!
42+
step_count = struct.unpack(STEP_COUNT_STRUCT_FORMAT, aiv.directory[INDEX_STEP_COUNT].get_data())[0]
43+
step_count_max = steps.max().item()
44+
if step_count < step_count_max:
45+
print(f"WARNING: step count ({step_count}) is lower than max step value from steps matrix ({step_count_max}), taking max step value as truth", file=sys.stderr)
46+
step_count = step_count_max
47+
48+
pauses_raw = aiv.directory[INDEX_PAUSES].get_data()
49+
if len(pauses_raw) != 50 * 4:
50+
print(f"WARNING: pauses is not expected size of {50*4} but {len(pauses_raw)}, adjusting", file=sys.stderr)
51+
pauses = list(struct.unpack(PAUSES_STRUCT_FORMAT_10, pauses_raw))
52+
else:
53+
pauses = list(struct.unpack(PAUSES_STRUCT_FORMAT_50, pauses_raw))
54+
55+
units = get_units_matrix(list(struct.unpack(f"<{24*10}i", aiv.directory[INDEX_MISC].get_data())))
56+
57+
pauseDelayAmount = struct.unpack(PAUSE_DELAY_AMOUNT_STRUCT_FORMAT, aiv.directory[INDEX_PAUSE].get_data())[0]
58+
59+
output = {}
60+
frames = [None] * step_count # + 1 is already stored in the aiv format
61+
miscItems = []
62+
63+
output["frames"] = frames
64+
output["miscItems"] = miscItems
65+
output["pauseDelayAmount"] = pauseDelayAmount
66+
67+
processed = {}
68+
69+
buildings = 0
70+
offset = -1
71+
for i in x_range():
72+
for j in y_range(invert_y=invert_y):
73+
offset += 1
74+
75+
if offset in processed:
76+
continue
77+
78+
construction = constructions[i, j].item()
79+
step = steps[i, j].item()
80+
81+
if construction == 0:
82+
processed[offset] = True
83+
continue
84+
85+
# Not in source code, but added here
86+
elif construction == 1:
87+
processed[offset] = True
88+
continue
89+
90+
elif construction == 2:
91+
processed[offset] = True
92+
continue
93+
94+
elif construction == 38: #keep2
95+
processed[offset] = True
96+
continue
97+
98+
buildingType = convertAIVEnumToMapperEnum(v = construction)
99+
100+
if buildingType in [25, 46, 26, 35, 106, 99]: # [10, 11, 12, 13, 20, 21, 22, 23, 24]:
101+
continue # not processed yet
102+
103+
processed[offset] = True
104+
105+
if step >= len(frames):
106+
raise Exception(f"step size too high at ({i}, {j}, {offset}): {step} => {len(frames)}")
107+
if frames[step] is None:
108+
buildings += 1
109+
shouldPause = step in pauses
110+
frames[step] = {'itemType': buildingType, 'tilePositionOfsets': [offset], 'shouldPause': shouldPause}
111+
112+
if report:
113+
print(f"INFO: buildings: {buildings}")
114+
115+
nonbuildings = 0
116+
117+
offset = -1
118+
for i in x_range():
119+
for j in y_range(invert_y=invert_y):
120+
offset += 1
121+
if offset not in processed:
122+
# do all the special stuff
123+
construction = constructions[i, j].item()
124+
step = steps[i, j].item()
125+
buildingType = convertAIVEnumToMapperEnum(v = construction)
126+
shouldPause = step in pauses
127+
if frames[step] is None:
128+
frames[step] = {'itemType': buildingType, 'tilePositionOfsets': [], 'shouldPause': shouldPause}
129+
frames[step]['tilePositionOfsets'].append(offset)
130+
nonbuildings += 1
131+
132+
if report:
133+
print(f"INFO: walls | pitch | moat: {nonbuildings}")
134+
135+
misc = 0
136+
for unitType in range(24):
137+
for entry in range(10):
138+
location = units[unitType, entry].item()
139+
if location == 0:
140+
continue
141+
misc += 1
142+
miscItems.append({'itemType': unitType, 'positionOfset': location, 'number': entry})
143+
144+
if report:
145+
print(f"INFO: units | flags | brazier | misc: {misc}")
146+
147+
emptyFrames = [index for index, frame in enumerate(output['frames']) if not frame]
148+
knownEmptyFrames = [0,1]
149+
unexpectedEmptyFrames = list(index for index in emptyFrames if index not in knownEmptyFrames)
150+
if len(unexpectedEmptyFrames) > 0:
151+
print(f"WARNING: unexpected count of empty frames, expected one (the first), but there were: {len(unexpectedEmptyFrames)} more: {','.join(str(v) for v in unexpectedEmptyFrames)}", file=sys.stderr)
152+
output['frames'] = [frame for frame in output['frames'] if frame]
153+
154+
if include_extra:
155+
output['extra'] = {
156+
'version' : aiv.directory.version_number,
157+
'directory_size' : aiv.directory.directory_size,
158+
'size' : aiv.directory.size,
159+
'u1' : aiv.directory.directory_u1,
160+
'u7' : aiv.directory.directory_u7,
161+
'sections': [i for i in aiv.directory.section_indices if i],
162+
# 'rng': aiv.directory[2003].get_data() # RNG
163+
'2001': int.from_bytes(aiv.directory[2001].get_data(), 'little'),
164+
'2002': int.from_bytes(aiv.directory[2001].get_data(), 'little'),
165+
'recent_selected_step_in_editor': int.from_bytes(aiv.directory[2010].get_data(), 'little'), # Last selected step in editor?
166+
'step_count': step_count,
167+
# 'typeData': np.frombuffer(aiv.directory[2004].get_data(), dtype = 'uint8').reshape((100,100)),
168+
# '2005': # wall edges or something?
169+
# '2006' : texture noise
170+
# '2013' : misc and unit locations in tilemap form?
171+
}
172+
173+
174+
return json.dumps(output, indent=2)
175+
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import json
2+
import struct
3+
from turtle import position
4+
from typing import Dict
5+
import numpy as np
6+
from sourcehold.aivs.AIV import AIV
7+
from sourcehold.tool.convert.aiv.info import AIV_HEIGHT, AIV_WIDTH, CONSTRUCTIONS_STRUCT_FORMAT, INDEX_CONSTRUCTIONS, INDEX_MISC, INDEX_PAUSE, INDEX_PAUSES, INDEX_STEP_COUNT, INDEX_STEPS, MAPPER_BUILDING_SIZES, MAPPERS_SH1_VK, PAUSES_STRUCT_FORMAT_50, STEPS_STRUCT_FORMAT, UNITS_STRUCT_FORMAT, convertMapperEnumToAIVEnum, get_constructions_matrix, get_steps_matrix, get_units_matrix, x_range, y_range
8+
9+
mapping_xyinversed_offset = np.zeros(shape=(AIV_WIDTH, AIV_HEIGHT), dtype='uint32')
10+
mapping_xy_offset = np.zeros(shape=(AIV_WIDTH, AIV_HEIGHT), dtype='uint32')
11+
12+
offset = -1
13+
for x in x_range():
14+
for y in y_range(invert_y=False):
15+
offset += 1
16+
mapping_xy_offset[x, y] = offset
17+
18+
offset = -1
19+
for x in x_range():
20+
for y in y_range(invert_y=True):
21+
offset += 1
22+
mapping_xyinversed_offset[x, y] = offset
23+
24+
def convert_offset(offset, invert_y=False):
25+
return (offset // AIV_WIDTH, offset % AIV_HEIGHT if not invert_y else (AIV_HEIGHT - 1) - (offset % AIV_HEIGHT))
26+
27+
def convert_offsets(offsets, invert_y=False):
28+
for loc in offsets:
29+
yield convert_offset(loc, invert_y=invert_y)
30+
31+
def from_json(path = None, data: Dict | None = None, invert_y = True, report = False):
32+
if not path and not data:
33+
raise Exception()
34+
35+
if path and not data:
36+
with open(path, 'rb') as f:
37+
data = json.load(f)
38+
39+
if data == None:
40+
raise Exception()
41+
42+
pauses = []
43+
pauseDelayAmount = data['pauseDelayAmount']
44+
constructions = get_constructions_matrix()
45+
steps = get_steps_matrix()
46+
frames = data['frames']
47+
units = get_units_matrix()
48+
for stepMin1, frame in enumerate(frames):
49+
step = stepMin1 + 1
50+
shouldPause = frame['shouldPause']
51+
if shouldPause:
52+
pauses.append(step)
53+
mapperID = frame['itemType']
54+
mapperName = MAPPERS_SH1_VK[mapperID]
55+
56+
if mapperID in [25, 46, 26, 35, 106, 99]:
57+
# Special type
58+
aivID = convertMapperEnumToAIVEnum(v = mapperID)
59+
locations = frame['tilePositionOfsets']
60+
for x, y in convert_offsets(locations, invert_y=invert_y):
61+
constructions[x, y] = aivID
62+
steps[x, y] = step
63+
else:
64+
# Buildings
65+
aivID = convertMapperEnumToAIVEnum(v = mapperID)
66+
if not mapperName in MAPPER_BUILDING_SIZES:
67+
raise Exception(f"unsupported mapper size: {mapperName}")
68+
size = MAPPER_BUILDING_SIZES[mapperName]
69+
if len(frame['tilePositionOfsets']) > 1:
70+
raise Exception(f'too many locations for a building: {frame['tilePositionOfsets']}')
71+
location = frame['tilePositionOfsets'][0]
72+
x, y = convert_offset(location, invert_y=invert_y)
73+
constructions[x:(x+size),y:(y+size)] = aivID
74+
steps[x:(x+size),y:(y+size)] = step
75+
for miscItem in data['miscItems']:
76+
itemType = miscItem['itemType']
77+
number = miscItem['number']
78+
positionOffset = miscItem['positionOfset']
79+
units[itemType, number] = positionOffset
80+
81+
pauses = sorted(pauses)
82+
if len(pauses) < 50:
83+
pauses += [0] * (50 - len(pauses))
84+
85+
select_all = np.ones((100,100), 'bool')
86+
aiv = AIV().from_file("resources/aiv/empty.aiv")
87+
aiv.directory[INDEX_CONSTRUCTIONS].set_data(struct.pack(CONSTRUCTIONS_STRUCT_FORMAT, *constructions[select_all]))
88+
aiv.directory[INDEX_STEPS].set_data(struct.pack(STEPS_STRUCT_FORMAT, *steps[select_all]))
89+
aiv.directory[INDEX_STEP_COUNT].set_data(struct.pack("<i", len(frames) + 1))
90+
aiv.directory[INDEX_PAUSES].set_data(struct.pack(PAUSES_STRUCT_FORMAT_50, *pauses))
91+
aiv.directory[INDEX_PAUSE].set_data(struct.pack("<i", pauseDelayAmount))
92+
select_all_misc = np.ones((24, 10), 'bool')
93+
aiv.directory[INDEX_MISC].set_data(struct.pack(UNITS_STRUCT_FORMAT, *units[select_all_misc]))
94+
return aiv
95+
96+

0 commit comments

Comments
 (0)