Skip to content

Commit 263047e

Browse files
committed
Initial commit
1 parent e56292a commit 263047e

File tree

8 files changed

+2064
-0
lines changed

8 files changed

+2064
-0
lines changed

pygbx/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pygbx.headers import CGameHeader, CGameCtnCollectorList, CollectorStock, MapBlock, Vector3, CGameChallenge, CGameBlockItem, CGameWaypointSpecialProperty, CGameCommon, CGameReplayRecord, CGameGhost, CGameCtnGhost, ControlEntry, GhostSampleRecord
2+
from pygbx.bytereader import ByteReader
3+
from pygbx.stadium_blocks import STADIUM_BLOCKS
4+
from pygbx.stadium_block_offsets import STADIUM_BLOCK_OFFSETS
5+
from pygbx.gbx import Gbx, GbxType, GbxLoadError

pygbx/bytereader.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import logging
2+
import struct
3+
from io import IOBase
4+
from pygbx.headers import Vector3
5+
6+
class PositionInfo(object):
7+
"""
8+
This classes holds information that is mainly private to
9+
the Gbx class but can still be retrieved through the positions member.
10+
11+
The PositionInfo marks a specific section in the file through it's position and size.
12+
"""
13+
14+
def __init__(self, pos, size):
15+
"""Constructs a new PositionInfo"""
16+
17+
self.pos = pos
18+
self.size = size
19+
20+
@property
21+
def valid(self):
22+
"""Checks if the instance of the section is valid
23+
24+
Returns:
25+
True if the instance points to a valid section in the file, False otherwise
26+
"""
27+
return self.pos > -1 and self.size > 0
28+
29+
30+
class ByteReader(object):
31+
"""The ByteReader class is used by the Gbx class to read specific data types supported by the GBX file format.
32+
33+
The class provides convinience methods for reading raw types such as integers, strings and vectors, which
34+
are the main data types of the GBX file format. While reading the file, the Gbx class may instantiate multiple
35+
instances of ByteReader to read different parts of the file. This is because some chunks depend on the state
36+
of the reader, this state can be e.g: lookback strings.
37+
38+
ByteReader accepts reading from raw bytes as well as from a file handle.
39+
"""
40+
def __init__(self, obj):
41+
"""Constructs a new ByteReader with the provided object.
42+
43+
Args:
44+
obj (file/bytes): a file handle opened through open() or a bytes object
45+
"""
46+
self.data = obj
47+
if isinstance(obj, IOBase):
48+
self.get_bytes = self.__get_bytes_file
49+
else:
50+
self.get_bytes = self.__get_bytes_generic
51+
52+
self.pos = 0
53+
self.seen_loopback = False
54+
self.stored_strings = []
55+
self.current_info = PositionInfo(-1, 0)
56+
57+
def push_info(self):
58+
"""Begins a section that can be then retrieved with pop_info."""
59+
self.current_info = PositionInfo(self.pos, 0)
60+
61+
62+
def pop_info(self):
63+
"""Ends the section began with push_info.
64+
65+
Returns:
66+
a PositionInfo marking the section
67+
"""
68+
self.current_info.size = self.pos - self.current_info.pos
69+
info = self.current_info
70+
self.current_info = PositionInfo(-1, 0)
71+
return info
72+
73+
def read(self, num_bytes, typestr=None):
74+
"""Reads an arbitrary amount of bytes from the buffer.
75+
76+
Reads the buffer of length num_bytes and optionally
77+
takes a type string that is passed to struct.unpack if not None.
78+
79+
Args:
80+
num_bytes (int): the number of bytes to read from the buffer
81+
typestr (str): the format character used by the struct module, passing None does not unpack the bytes
82+
83+
Returns:
84+
the bytes object, if no type string was provided, type returned by struct.unpack otherwise
85+
"""
86+
val = self.get_bytes(num_bytes)
87+
self.pos += num_bytes
88+
if typestr == None:
89+
return val
90+
try:
91+
return struct.unpack(typestr, val)[0]
92+
except Exception as e:
93+
logging.error(e)
94+
return 0
95+
96+
def __get_bytes_file(self, num_bytes):
97+
self.data.seek(self.pos)
98+
return self.data.read(num_bytes)
99+
100+
def __get_bytes_generic(self, num_bytes):
101+
return self.data[self.pos:self.pos + num_bytes]
102+
103+
def read_int32(self):
104+
"""Reads a signed int32.
105+
106+
Returns:
107+
the integer read from the buffer
108+
"""
109+
return self.read(4, 'i')
110+
111+
def read_uint32(self):
112+
"""Reads an unsigned int32.
113+
114+
Returns:
115+
the integer read from the buffer
116+
"""
117+
return self.read(4, 'I')
118+
119+
def read_int16(self):
120+
"""Reads a signed int16.
121+
122+
Returns:
123+
the integer read from the buffer
124+
"""
125+
return self.read(2, 'h')
126+
127+
def read_uint16(self):
128+
"""Reads an unsigned int16.
129+
130+
Returns:
131+
the integer read from the buffer
132+
"""
133+
return self.read(2, 'H')
134+
135+
def read_int8(self):
136+
"""Reads a signed int8.
137+
138+
Returns:
139+
the integer read from the buffer
140+
"""
141+
return self.read(1, 'b')
142+
143+
def read_float(self):
144+
"""Reads a 32 bit float.
145+
146+
Returns:
147+
the float read from the buffer
148+
"""
149+
return self.read(4, 'f')
150+
151+
def read_vec3(self):
152+
"""Reads 12 bytes as 3 floats from the buffer and packs them into a Vector3.
153+
154+
Returns:
155+
the vector read from the buffer
156+
"""
157+
return Vector3(self.read_float(), self.read_float(), self.read_float())
158+
159+
def read_string(self):
160+
"""Reads a string from the buffer, first reading the length, then it's data.
161+
162+
Returns:
163+
the string read from the buffer, None if there was an error
164+
"""
165+
strlen = self.read_uint32()
166+
try:
167+
return self.read(strlen, str(strlen) + 's').decode('utf-8')
168+
except UnicodeDecodeError as e:
169+
logging.error(f'Failed to read string: {e}')
170+
return None
171+
172+
def read_byte(self):
173+
"""Reads a single byte from the buffer.
174+
175+
Returns:
176+
the single byte read from the buffer
177+
"""
178+
val = self.get_bytes(1)[0]
179+
self.pos += 1
180+
return val
181+
182+
def skip(self, num_bytes):
183+
"""Skips provided amount of bytes in the buffer
184+
185+
Args:
186+
num_bytes (int): the number of bytes to skip
187+
"""
188+
self.pos += num_bytes
189+
190+
def read_string_lookback(self):
191+
"""Reads a special string type in the GBX file format called the lookbackstring.
192+
193+
Such type is used to reference already read strings, or introduce them if they were not
194+
read yet. A ByteReader instance keeps track of lookback strings previously read and
195+
returns an already existing string, if the data references it. For more information,
196+
see the lookbackstring type in the GBX file format: https://wiki.xaseco.org/wiki/GBX.
197+
198+
Returns:
199+
the lookback string read from the buffer
200+
"""
201+
if not self.seen_loopback:
202+
self.read_uint32()
203+
204+
self.seen_loopback = True
205+
inp = self.read_uint32()
206+
if (inp & 0xc0000000) != 0 and (inp & 0x3fffffff) == 0:
207+
s = self.read_string()
208+
self.stored_strings.append(s)
209+
return s
210+
211+
if inp == 0:
212+
s = self.read_string()
213+
self.stored_strings.append(s)
214+
return s
215+
216+
if inp == -1:
217+
return ''
218+
219+
if (inp & 0x3fffffff) == inp:
220+
if inp == 11:
221+
return 'Valley'
222+
elif inp == 12:
223+
return 'Canyon'
224+
elif inp == 17:
225+
return 'TMCommon'
226+
elif inp == 202:
227+
return 'Storm'
228+
elif inp == 299:
229+
return 'SMCommon'
230+
elif inp == 10003:
231+
return 'Common'
232+
233+
inp &= 0x3fffffff
234+
if inp - 1 >= len(self.stored_strings):
235+
return ''
236+
return self.stored_strings[inp - 1]

0 commit comments

Comments
 (0)