Skip to content

Commit 7112715

Browse files
committed
Restore 3DS preprocessing tools
1 parent c52bd08 commit 7112715

8 files changed

Lines changed: 2337 additions & 0 deletions

File tree

datawin_py/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
DataWin - Python package for loading GameMaker data.win files
3+
Parses GMS1.4 and earlier binary game data files.
4+
"""
5+
6+
from .datawin import DataWin, DataWinParserOptions
7+
8+
__version__ = "0.1.0"
9+
__all__ = ["DataWin", "DataWinParserOptions"]

datawin_py/binary_reader.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Binary reader for parsing data.win files"""
2+
3+
import struct
4+
from typing import BinaryIO, Tuple
5+
6+
7+
class BinaryReader:
8+
"""Reads binary data from a file with little-endian byte order"""
9+
10+
def __init__(self, file: BinaryIO, file_size: int):
11+
self.file = file
12+
self.file_size = file_size
13+
self.position = 0
14+
15+
def read_bytes(self, count: int) -> bytes:
16+
"""Read a fixed number of bytes"""
17+
data = self.file.read(count)
18+
self.position += len(data)
19+
return data
20+
21+
def read_uint8(self) -> int:
22+
"""Read unsigned 8-bit integer"""
23+
data = self.read_bytes(1)
24+
return struct.unpack('<B', data)[0]
25+
26+
def read_uint16(self) -> int:
27+
"""Read unsigned 16-bit integer (little-endian)"""
28+
data = self.read_bytes(2)
29+
return struct.unpack('<H', data)[0]
30+
31+
def read_int16(self) -> int:
32+
"""Read signed 16-bit integer (little-endian)"""
33+
data = self.read_bytes(2)
34+
return struct.unpack('<h', data)[0]
35+
36+
def read_uint32(self) -> int:
37+
"""Read unsigned 32-bit integer (little-endian)"""
38+
data = self.read_bytes(4)
39+
return struct.unpack('<I', data)[0]
40+
41+
def read_int32(self) -> int:
42+
"""Read signed 32-bit integer (little-endian)"""
43+
data = self.read_bytes(4)
44+
return struct.unpack('<i', data)[0]
45+
46+
def read_uint64(self) -> int:
47+
"""Read unsigned 64-bit integer (little-endian)"""
48+
data = self.read_bytes(8)
49+
return struct.unpack('<Q', data)[0]
50+
51+
def read_int64(self) -> int:
52+
"""Read signed 64-bit integer (little-endian)"""
53+
data = self.read_bytes(8)
54+
return struct.unpack('<q', data)[0]
55+
56+
def read_float32(self) -> float:
57+
"""Read 32-bit float (little-endian)"""
58+
data = self.read_bytes(4)
59+
return struct.unpack('<f', data)[0]
60+
61+
def read_float64(self) -> float:
62+
"""Read 64-bit float (little-endian)"""
63+
data = self.read_bytes(8)
64+
return struct.unpack('<d', data)[0]
65+
66+
def read_bool32(self) -> bool:
67+
"""Read 32-bit boolean (nonzero = True)"""
68+
return self.read_uint32() != 0
69+
70+
def read_cstring(self, max_length: int = 256) -> str:
71+
"""Read null-terminated string"""
72+
chars = b''
73+
for _ in range(max_length):
74+
byte = self.read_bytes(1)
75+
if not byte or byte[0] == 0:
76+
break
77+
chars += byte
78+
return chars.decode('utf-8', errors='replace')
79+
80+
def skip(self, count: int):
81+
"""Skip bytes without reading"""
82+
self.file.seek(count, 1)
83+
self.position += count
84+
85+
def seek(self, offset: int):
86+
"""Seek to absolute file position"""
87+
self.file.seek(offset)
88+
self.position = offset
89+
90+
def tell(self) -> int:
91+
"""Get current position"""
92+
return self.position
93+
94+
def read_bytes_at(self, offset: int, count: int) -> bytes:
95+
"""Read bytes at specific offset"""
96+
current_pos = self.position
97+
self.seek(offset)
98+
data = self.read_bytes(count)
99+
self.seek(current_pos)
100+
return data
101+
102+
def at_end(self) -> bool:
103+
"""Check if at end of file"""
104+
return self.position >= self.file_size

datawin_py/datawin.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
"""Main DataWin class for loading and accessing game data"""
2+
3+
from dataclasses import dataclass, field
4+
from typing import Optional, Callable, Any
5+
from .binary_reader import BinaryReader
6+
from .structures import *
7+
from . import parsers
8+
9+
10+
@dataclass
11+
class DataWinParserOptions:
12+
"""Options for controlling which chunks to parse"""
13+
parse_gen8: bool = True
14+
parse_optn: bool = True
15+
parse_lang: bool = True
16+
parse_extn: bool = True
17+
parse_sond: bool = True
18+
parse_agrp: bool = True
19+
parse_sprt: bool = True
20+
parse_bgnd: bool = True
21+
parse_path: bool = True
22+
parse_scpt: bool = True
23+
parse_glob: bool = True
24+
parse_shdr: bool = True
25+
parse_font: bool = True
26+
parse_tmln: bool = True
27+
parse_objt: bool = True
28+
parse_room: bool = True
29+
parse_tpag: bool = True
30+
parse_code: bool = True
31+
parse_vari: bool = True
32+
parse_func: bool = True
33+
parse_strg: bool = True
34+
parse_txtr: bool = True
35+
parse_audo: bool = True
36+
skip_loading_precise_masks_for_non_precise_sprites: bool = False
37+
progress_callback: Optional[Callable[[str, int, int, Any], None]] = None
38+
39+
40+
class DataWin:
41+
"""Parsed GameMaker data.win file"""
42+
43+
def __init__(self):
44+
self.gen8 = Gen8()
45+
self.optn = Optn()
46+
self.lang = Lang()
47+
self.extn = Extn()
48+
self.sond = Sond()
49+
self.agrp = Agrp()
50+
self.sprt = Sprt()
51+
self.bgnd = Bgnd()
52+
self.path = PathChunk()
53+
self.scpt = Scpt()
54+
self.glob = Glob()
55+
self.shdr = Shdr()
56+
self.font = FontChunk()
57+
self.tmln = Tmln()
58+
self.objt = Objt()
59+
self.room = RoomChunk()
60+
self.tpag = Tpag()
61+
self.code = Code()
62+
self.vari = Vari()
63+
self.func = Func()
64+
self.strg = Strg()
65+
self.txtr = Txtr()
66+
self.audo = Audo()
67+
68+
self.strg_buffer = b''
69+
self.strg_buffer_base = 0
70+
self.file = None
71+
self.file_size = 0
72+
73+
@staticmethod
74+
def load(file_path: str, options: Optional[DataWinParserOptions] = None) -> 'DataWin':
75+
"""Load and parse a data.win file"""
76+
if options is None:
77+
options = DataWinParserOptions()
78+
79+
dw = DataWin()
80+
81+
try:
82+
with open(file_path, 'rb') as file:
83+
file.seek(0, 2)
84+
dw.file_size = file.tell()
85+
file.seek(0)
86+
87+
if dw.file_size <= 0:
88+
raise ValueError(f"Invalid file size: {dw.file_size}")
89+
90+
reader = BinaryReader(file, dw.file_size)
91+
92+
# Validate FORM header
93+
form_magic = reader.read_bytes(4)
94+
if form_magic != b'FORM':
95+
raise ValueError(f"Invalid file: expected FORM magic, got '{form_magic.decode('utf-8', errors='replace')}'")
96+
97+
form_length = reader.read_uint32()
98+
99+
# Pass 1: Find and load STRG only
100+
if options.parse_strg:
101+
reader.seek(8)
102+
total_chunks = 0
103+
104+
while reader.tell() < dw.file_size:
105+
if reader.tell() + 8 > dw.file_size:
106+
break
107+
108+
chunk_name = reader.read_bytes(4).decode('utf-8', errors='replace')
109+
chunk_length = reader.read_uint32()
110+
chunk_data_start = reader.tell()
111+
112+
if chunk_name == 'STRG':
113+
dw.strg_buffer_base = chunk_data_start
114+
dw.strg_buffer = reader.read_bytes(chunk_length)
115+
else:
116+
reader.skip(chunk_length)
117+
118+
total_chunks += 1
119+
120+
# Pass 2: Parse all chunks
121+
reader.seek(8)
122+
chunk_index = 0
123+
124+
while reader.tell() < dw.file_size:
125+
if reader.tell() + 8 > dw.file_size:
126+
break
127+
128+
chunk_name = reader.read_bytes(4).decode('utf-8', errors='replace')
129+
chunk_length = reader.read_uint32()
130+
chunk_data_start = reader.tell()
131+
chunk_end = chunk_data_start + chunk_length
132+
133+
if options.progress_callback:
134+
options.progress_callback(chunk_name, chunk_index, total_chunks if options.parse_strg else 0)
135+
136+
# Parse chunks based on options
137+
if chunk_name == 'GEN8' and options.parse_gen8:
138+
dw.gen8 = parsers.parse_gen8(reader, dw.strg_buffer, dw.strg_buffer_base)
139+
elif chunk_name == 'OPTN' and options.parse_optn:
140+
dw.optn = parsers.parse_optn(reader, dw.strg_buffer, dw.strg_buffer_base)
141+
elif chunk_name == 'LANG' and options.parse_lang:
142+
dw.lang = parsers.parse_lang(reader, dw.strg_buffer, dw.strg_buffer_base)
143+
elif chunk_name == 'EXTN' and options.parse_extn:
144+
dw.extn = parsers.parse_extn(reader, dw.strg_buffer, dw.strg_buffer_base)
145+
elif chunk_name == 'SOND' and options.parse_sond:
146+
dw.sond = parsers.parse_sond(reader, dw.strg_buffer, dw.strg_buffer_base)
147+
elif chunk_name == 'AGRP' and options.parse_agrp:
148+
dw.agrp = parsers.parse_agrp(reader, dw.strg_buffer, dw.strg_buffer_base)
149+
elif chunk_name == 'SPRT' and options.parse_sprt:
150+
dw.sprt = parsers.parse_sprt(
151+
reader,
152+
dw.strg_buffer,
153+
dw.strg_buffer_base,
154+
options.skip_loading_precise_masks_for_non_precise_sprites
155+
)
156+
elif chunk_name == 'BGND' and options.parse_bgnd:
157+
dw.bgnd = parsers.parse_bgnd(reader, dw.strg_buffer, dw.strg_buffer_base)
158+
elif chunk_name == 'PATH' and options.parse_path:
159+
dw.path = parsers.parse_path(reader, dw.strg_buffer, dw.strg_buffer_base)
160+
elif chunk_name == 'SCPT' and options.parse_scpt:
161+
dw.scpt = parsers.parse_scpt(reader, dw.strg_buffer, dw.strg_buffer_base)
162+
elif chunk_name == 'GLOB' and options.parse_glob:
163+
dw.glob = parsers.parse_glob(reader)
164+
elif chunk_name == 'SHDR' and options.parse_shdr:
165+
dw.shdr = parsers.parse_shdr(reader, dw.strg_buffer, dw.strg_buffer_base)
166+
elif chunk_name == 'FONT' and options.parse_font:
167+
dw.font = parsers.parse_font(reader, dw.strg_buffer, dw.strg_buffer_base)
168+
elif chunk_name == 'TMLN' and options.parse_tmln:
169+
dw.tmln = parsers.parse_tmln(reader, dw.strg_buffer, dw.strg_buffer_base)
170+
elif chunk_name == 'OBJT' and options.parse_objt:
171+
dw.objt = parsers.parse_objt(reader, dw.strg_buffer, dw.strg_buffer_base)
172+
elif chunk_name == 'ROOM' and options.parse_room:
173+
dw.room = parsers.parse_room(reader, dw.strg_buffer, dw.strg_buffer_base)
174+
elif chunk_name == 'TPAG' and options.parse_tpag:
175+
dw.tpag = parsers.parse_tpag(reader, dw.strg_buffer, dw.strg_buffer_base)
176+
elif chunk_name == 'CODE' and options.parse_code:
177+
dw.code = parsers.parse_code(reader, dw.strg_buffer, dw.strg_buffer_base, chunk_length, chunk_data_start)
178+
elif chunk_name == 'VARI' and options.parse_vari:
179+
dw.vari = parsers.parse_vari(reader, dw.strg_buffer, dw.strg_buffer_base, chunk_length)
180+
elif chunk_name == 'FUNC' and options.parse_func:
181+
dw.func = parsers.parse_func(reader, dw.strg_buffer, dw.strg_buffer_base)
182+
elif chunk_name == 'STRG' and options.parse_strg:
183+
dw.strg = parsers.parse_strg(reader, dw.strg_buffer, dw.strg_buffer_base)
184+
elif chunk_name == 'TXTR' and options.parse_txtr:
185+
dw.txtr = parsers.parse_txtr(reader, dw.file_size)
186+
elif chunk_name == 'AUDO' and options.parse_audo:
187+
dw.audo = parsers.parse_audo(reader, dw.strg_buffer, dw.strg_buffer_base)
188+
elif chunk_name == 'DAFL':
189+
# Empty chunk
190+
pass
191+
else:
192+
print(f"Unknown chunk: {chunk_name} (length {chunk_length} at offset {chunk_data_start - 8:08X})")
193+
194+
# Seek to chunk end
195+
reader.seek(chunk_end)
196+
chunk_index += 1
197+
198+
except Exception as e:
199+
raise RuntimeError(f"Failed to load data.win: {e}") from e
200+
201+
return dw
202+
203+
def get_string(self, index: int) -> Optional[str]:
204+
"""Get a string by index from STRG chunk"""
205+
if 0 <= index < len(self.strg.strings):
206+
return self.strg.strings[index]
207+
return None
208+
209+
def get_sprite(self, index: int) -> Optional[Sprite]:
210+
"""Get a sprite by index"""
211+
if 0 <= index < len(self.sprt.sprites):
212+
return self.sprt.sprites[index]
213+
return None
214+
215+
def get_room(self, index: int) -> Optional[Room]:
216+
"""Get a room by index"""
217+
if 0 <= index < len(self.room.rooms):
218+
return self.room.rooms[index]
219+
return None
220+
221+
def get_object(self, index: int) -> Optional[GameObject]:
222+
"""Get a game object by index"""
223+
if 0 <= index < len(self.objt.objects):
224+
return self.objt.objects[index]
225+
return None
226+
227+
def get_room_by_name(self, name: str) -> Optional[Room]:
228+
"""Get a room by name"""
229+
for room in self.room.rooms:
230+
if room.name == name:
231+
return room
232+
return None
233+
234+
def get_sprite_by_name(self, name: str) -> Optional[Sprite]:
235+
"""Get a sprite by name"""
236+
for sprite in self.sprt.sprites:
237+
if sprite.name == name:
238+
return sprite
239+
return None
240+
241+
def get_object_by_name(self, name: str) -> Optional[GameObject]:
242+
"""Get a game object by name"""
243+
for obj in self.objt.objects:
244+
if obj.name == name:
245+
return obj
246+
return None
247+
248+
def __repr__(self) -> str:
249+
return (
250+
f"<DataWin game={self.gen8.name or '?'} "
251+
f"version={self.gen8.major}.{self.gen8.minor}.{self.gen8.release}.{self.gen8.build} "
252+
f"rooms={len(self.room.rooms)} sprites={len(self.sprt.sprites)} objects={len(self.objt.objects)}>"
253+
)

0 commit comments

Comments
 (0)