Skip to content

Commit

Permalink
Userdata-only parser for Touhou 9.5 (Shoot the Bullet).
Browse files Browse the repository at this point in the history
  • Loading branch information
n-rook committed Apr 28, 2024
1 parent 2718efe commit f037fbf
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 0 deletions.
1 change: 1 addition & 0 deletions project/thscoreboard/replays/game_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class GameIDs:
TH07 = "th07"
TH08 = "th08"
TH09 = "th09"
TH095 = "th095"
TH10 = "th10"
TH11 = "th11"
TH12 = "th12"
Expand Down
103 changes: 103 additions & 0 deletions project/thscoreboard/replays/kaitai_parsers/th095_encrypted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO


if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9):
raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__))

class Th095Encrypted(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
self._read()

def _read(self):
self.header = Th095Encrypted.FileHeader(self._io, self, self._root)

class FileHeader(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
self._read()

def _read(self):
self.magic = self._io.read_bytes(4)
if not self.magic == b"\x74\x39\x35\x72":
raise kaitaistruct.ValidationNotEqualError(b"\x74\x39\x35\x72", self.magic, self._io, u"/types/file_header/seq/0")
self.unknown_1 = self._io.read_bytes(8)
self.userdata_offset = self._io.read_u4le()


class Userdata(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
self._read()

def _read(self):
self.magic_user = self._io.read_bytes(4)
if not self.magic_user == b"\x55\x53\x45\x52":
raise kaitaistruct.ValidationNotEqualError(b"\x55\x53\x45\x52", self.magic_user, self._io, u"/types/userdata/seq/0")
self.user_length = self._io.read_u4le()
self.unknown = self._io.read_bytes(4)
self.user_desc = []
i = 0
while True:
_ = self._io.read_u1()
self.user_desc.append(_)
if _ == 13:
break
i += 1
self.user_desc_term = (self._io.read_bytes_term(10, False, True, True)).decode(u"ASCII")
self.version = Th095Encrypted.UserdataField(u"Version", self._io, self, self._root)
self.username = Th095Encrypted.UserdataField(u"Name", self._io, self, self._root)
self.level = Th095Encrypted.UserdataField(u"Level", self._io, self, self._root)
self.scene = Th095Encrypted.UserdataField(u"Scene", self._io, self, self._root)
self.date = Th095Encrypted.UserdataField(u"Date", self._io, self, self._root)
self.score = Th095Encrypted.UserdataField(u"Score", self._io, self, self._root)
self.slowdown = Th095Encrypted.UserdataField(u"Slow Rate", self._io, self, self._root)


class UserdataField(KaitaiStruct):
def __init__(self, expected_name, _io, _parent=None, _root=None):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
self.expected_name = expected_name
self._read()

def _read(self):
self.name = (self._io.read_bytes(len(self.expected_name))).decode(u"ASCII")
if not self.name == self.expected_name:
raise kaitaistruct.ValidationNotEqualError(self.expected_name, self.name, self._io, u"/types/userdata_field/seq/0")
self.name_value_separator_space = self._io.read_bytes(1)
if not self.name_value_separator_space == b"\x20":
raise kaitaistruct.ValidationNotEqualError(b"\x20", self.name_value_separator_space, self._io, u"/types/userdata_field/seq/1")
self.value_with_space = (self._io.read_bytes_term(10, False, True, True)).decode(u"ASCII")

@property
def value(self):
if hasattr(self, '_m_value'):
return self._m_value

self._m_value = (self.value_with_space)[0:(len(self.value_with_space) - 1)]
return getattr(self, '_m_value', None)


@property
def userdata(self):
if hasattr(self, '_m_userdata'):
return self._m_userdata

_pos = self._io.pos()
self._io.seek(self.header.userdata_offset)
self._m_userdata = Th095Encrypted.Userdata(self._io, self, self._root)
self._io.seek(_pos)
return getattr(self, '_m_userdata', None)


24 changes: 24 additions & 0 deletions project/thscoreboard/replays/replay_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .kaitai_parsers import th07
from .kaitai_parsers import th08
from .kaitai_parsers import th09
from .kaitai_parsers import th095_encrypted
from .kaitai_parsers import th10
from .kaitai_parsers import th11
from .kaitai_parsers import th12
Expand Down Expand Up @@ -446,6 +447,27 @@ def _Parse09(rep_raw):
return r


def _Parse095(rep_raw):
encrypted_replay = th095_encrypted.Th095Encrypted.from_bytes(rep_raw)

# This is goofy and probably not a good idea
spell_card_id = int(encrypted_replay.userdata.level.value) << 8 + int(
encrypted_replay.userdata.scene.value
)

return ReplayInfo(
game=game_ids.GameIDs.TH095,
shot="Aya",
difficulty=0,
score=int(encrypted_replay.userdata.score.value),
timestamp=time.strptime(encrypted_replay.userdata.date.value, "%y/%m/%d %H:%M"),
name=encrypted_replay.userdata.username.value,
replay_type=game_ids.ReplayTypes.SPELL_PRACTICE,
spell_card_id=spell_card_id,
slowdown=float(encrypted_replay.userdata.slowdown.value),
)


def _Parse10(rep_raw):
header = th_modern.ThModern.from_bytes(rep_raw)
comp_data = bytearray(header.main.comp_data)
Expand Down Expand Up @@ -1104,6 +1126,8 @@ def Parse(replay) -> ReplayInfo:
return _Parse08(replay)
elif gamecode == b"T9RP":
return _Parse09(replay)
elif gamecode == b"t95r":
return _Parse095(replay)
elif gamecode == b"t10r":
return _Parse10(replay)
elif gamecode == b"t11r":
Expand Down
Binary file not shown.
Binary file not shown.
24 changes: 24 additions & 0 deletions project/thscoreboard/replays/test_replay_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,30 @@ def testPVP(self):
self.assertEqual(s.th09_p2_score, 0)


class Th095ReplayTestCase(unittest.TestCase):
def testOne(self):
r = ParseTestReplay("th95_3-1")
self.assertEqual(r.game, "th095")
self.assertEqual(r.score, 132030)
self.assertEqual(
r.timestamp,
datetime.datetime(2024, 4, 27, 16, 42, tzinfo=datetime.timezone.utc),
)
self.assertEqual(r.name, "nrook3.1")
self.assertEqual(r.slowdown, 0.00)

def testTwo(self):
r = ParseTestReplay("th95_2-5")
self.assertEqual(r.game, "th095")
self.assertEqual(r.score, 128830)
self.assertEqual(
r.timestamp,
datetime.datetime(2024, 4, 27, 19, 54, tzinfo=datetime.timezone.utc),
)
self.assertEqual(r.name, "nrook ")
self.assertEqual(r.slowdown, 0.00)


class Th10ReplayTestCase(unittest.TestCase):
def testNormal(self):
r = ParseTestReplay("th10_normal")
Expand Down
71 changes: 71 additions & 0 deletions ref/threp-ksy/th095_encrypted.ksy
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
meta:
id: th095_encrypted
file-extension: rpy
endian: le
seq:
- id: header
type: file_header
instances:
userdata:
pos: header.userdata_offset
type: userdata
types:
file_header:
seq:
- id: magic
contents: t95r
# Probably includes version
- id: unknown_1
size: 8
- id: userdata_offset
type: u4
userdata:
seq:
- id: magic_user
contents: USER
- id: user_length
type: u4
- id: unknown
size: 4
- id: user_desc
type: u1
repeat: until
repeat-until: _ == 0xd
- id: user_desc_term
type: str
terminator: 0xa
encoding: ASCII
- id: version
type: userdata_field("Version")
- id: username
type: userdata_field("Name")
- id: level
type: userdata_field("Level")
- id: scene
type: userdata_field("Scene")
- id: date
type: userdata_field("Date")
- id: score
type: userdata_field("Score")
- id: slowdown
type: userdata_field("Slow Rate")
userdata_field:
params:
- id: expected_name
type: str
seq:
- id: name
type: str
size: expected_name.length
encoding: ASCII
valid: expected_name
- id: name_value_separator_space
contents: " "
- id: value_with_space
type: str
# Always ends with 0x0d0a; that is, space then LF
terminator: 0x0a
encoding: ASCII
instances:
value:
value: value_with_space.substring(0, value_with_space.length - 1)

0 comments on commit f037fbf

Please sign in to comment.