Skip to content

Instantly share code, notes, and snippets.

@scturtle
Last active December 25, 2025 09:25
Show Gist options
  • Select an option

  • Save scturtle/3ac8798853566c4954c7350d49fb0359 to your computer and use it in GitHub Desktop.

Select an option

Save scturtle/3ac8798853566c4954c7350d49fb0359 to your computer and use it in GitHub Desktop.
pal
from struct import pack, unpack_from
class MKFDecoder(object):
def __init__(self, path=None):
assert path or data
with open(path, "rb") as f:
self._content = memoryview(f.read())
self.count = unpack_from("<I", self._content, 0)[0] // 4
self.indexes = tuple(
unpack_from("<I", self._content, i << 2)[0] for i in range(self.count)
)
self.count -= 1
def __len__(self):
return self.count
def read(self, index):
data = self._content[self.indexes[index] : self.indexes[index + 1]]
return bytearray(data.tobytes())
class Palette(MKFDecoder):
def __init__(self):
super(Palette, self).__init__("Pat.mkf")
def get_palette(self, index=0, is_night=False):
data = self.read(index)
assert len(data) == 768 or len(data) == 1536 # with night
palette_data = bytearray()
for i in range(0, 768, 3):
r = data[(256 * 3 if is_night else 0) + i] << 2
g = data[(256 * 3 if is_night else 0) + i + 1] << 2
b = data[(256 * 3 if is_night else 0) + i + 2] << 2
palette_data.extend(pack("BBB", b, g, r))
return palette_data
pat = Palette()
class RLEDecoder(object):
def __init__(self, data):
self.data = data
@property
def width(self):
return unpack_from("H", self.data, 0)[0]
@property
def height(self):
return unpack_from("H", self.data, 2)[0]
def dump_tga(self, fn):
width = self.width
height = self.height
idx = 4
to_idx = 0
to_data = bytearray(width * height * 4) # BGRA
palette = pat.get_palette(0, is_night=False)
while idx < len(self.data) and to_idx < width * height:
code = unpack_from("B", self.data, idx)[0]
idx += 1
if code > 0x80: # transparent
count = code - 0x80
to_idx += count # skip
else:
count = code
for i in range(count):
color = self.data[idx]
dest_pos = to_idx * 4
to_data[to_idx * 4 + 0] = palette[color * 3]
to_data[to_idx * 4 + 1] = palette[color * 3 + 1]
to_data[to_idx * 4 + 2] = palette[color * 3 + 2]
to_data[to_idx * 4 + 3] = 255
idx += 1
to_idx += 1
header = pack("<BBBHHBHHHHBB", 0, 0, 2, 0, 0, 0, 0, 0, width, height, 32, 0x20)
with open(fn, "wb") as f:
f.write(header)
f.write(to_data)
class SubPlace(MKFDecoder):
def __init__(self, data):
(self.count,) = unpack_from("H", data, 0)
self._content = memoryview(data)
self.indexes = [
(x << 1 if x != 0x18444 >> 1 else 0x18444 & 0xFFFF)
if i < self.count
else len(data)
for i, x in enumerate(unpack_from("H" * (self.count + 1), self._content, 0))
]
if self.indexes[-2] <= self.indexes[-3]:
self.indexes = self.indexes[:-2] + self.indexes[-1:]
self.count -= 1
def __getitem__(self, index):
return RLEDecoder(self.read(index))
class Data(MKFDecoder):
def __init__(self):
super(Data, self).__init__("Data.mkf")
if __name__ == "__main__":
sub = SubPlace(Data().read(9))
for i in range(len(sub)):
sub[i].dump_tga(f"{i}.tga")
import os
import struct
class PalMessages:
def __init__(self, pal_path):
self.pal_path = pal_path
mkf_path = os.path.join(self.pal_path, 'Sss.mkf')
self._index_data = self._read_mkf_subfile(mkf_path, 3)
msg_path = os.path.join(self.pal_path, 'M.msg')
self._message_data = open(msg_path, 'rb').read()
word_path = os.path.join(self.pal_path, 'Word.dat')
self._words_data = open(word_path, 'rb').read()
def _read_mkf_subfile(self, mkf_file, sub_id):
with open(mkf_file, 'rb') as f:
f.seek(0)
f.seek(sub_id * 4)
offset_bytes = f.read(8)
start_offset = struct.unpack('<I', offset_bytes[0:4])[0]
end_offset = struct.unpack('<I', offset_bytes[4:8])[0]
length = end_offset - start_offset
f.seek(start_offset)
return f.read(length)
def get_text(self, index):
pos = index * 4
start_pos = struct.unpack('<I', self._index_data[pos:pos+4])[0]
end_pos = struct.unpack('<I', self._index_data[pos+4:pos+8])[0]
length = end_pos - start_pos
raw_msg = self._message_data[start_pos:end_pos]
return raw_msg.decode('gbk', errors='replace')
def get_word(self, index):
record_size = 10
pos = index * record_size
raw_word = self._words_data[pos : pos + record_size]
return raw_word.decode('gbk')
def __getitem__(self, index):
return self.get_text(index)
if __name__ == "__main__":
pal_msg = PalMessages(pal_path=".")
for i in range(13513):
print(f"Text[{i}]: {repr(pal_msg[i])}")
for i in range(565):
print(f"Word[{i}]: {repr(pal_msg.get_word(i))}")
import sys
import struct
import os
import argparse
import io
ID_FILES = {
'MusicID': "MusicID.txt", 'DirectionID': "DirectionID.txt", 'SceneID': "SceneID_Win.txt",
'FaceID': "FaceID.txt", 'AttributeID': "AttributeID.txt", 'CharacterID': "CharacterID.txt",
'TriggerMethodID': "TriggerMethodID.txt", 'BattleResultID': "BattleResultID.txt",
'BattleSpriteID': "BattleSpriteID.txt", 'PaletteID': "PaletteID.txt", 'CDID': "CDID.txt",
'ObjectID': "ObjectID.txt", 'EnemyTeamID': "EnemyTeamID.txt", 'StatusID': "StatusID.txt",
'BodyPartID': "BodyPartID.txt", 'ConditionID': "ConditionID.txt", 'TeamMemberID': "TeamMemberID.txt",
'SpriteID': "SpriteID.txt", 'RoleID': "RoleID.txt", 'MovieID': "MovieID.txt",
'MusicParamID': "MusicParamID.txt", 'PictureID': "PictureID.txt", 'ShopID': "ShopID.txt"
}
LOOKUPS = {}
DESCS = {}
def load_resources():
# 加载所有 ID 定义文件
for name, fname in ID_FILES.items():
if os.path.exists(fname):
d = {}
for line in open(fname, 'r', encoding='utf-8-sig', errors='ignore'):
if not line.startswith('@'): continue
val, desc = line[1:].split(':', 1)
desc = desc.strip().split(';', 1)[0].strip()
d[int(val, 16)] = desc
LOOKUPS[name] = d
# 加载脚本指令描述 scr_desc.txt
for line in open("scr_desc.txt", 'r', encoding='utf-8-sig', errors='ignore'):
if not line.startswith('@'): continue
parts = line.split(';', 1)[0].strip().split()
cmd = int(parts[0][1:-1], 16)
arg_types = parts[2:] + ['Null'] * (3 - len(parts[2:]))
DESCS[cmd] = (parts[1], arg_types[:3])
def load_mkf_chunk(mkf_path, chunk_index):
with open(mkf_path, 'rb') as f:
f.seek(0)
first_offset_data = f.read(4)
if not first_offset_data: return None
first_offset = struct.unpack('<I', first_offset_data)[0]
count = first_offset // 4
assert chunk_index < count - 1
f.seek(chunk_index * 4)
begin = struct.unpack('<I', f.read(4))[0]
end = struct.unpack('<I', f.read(4))[0]
length = end - begin
assert length > 0
f.seek(begin)
return f.read(length)
def read_msg(f_idx, f_dat, index):
begin = struct.unpack('<I', f_idx[index * 4: index * 4 + 4])[0]
end = struct.unpack('<I', f_idx[index * 4 + 4: index * 4 + 8])[0]
length = end - begin
assert length >= 0
buf = f_dat[begin: begin + length]
return buf.decode('gbk', errors='replace').rstrip('\x00')
def read_word(f_word, index):
data = f_word[index * 10: index * 10 + 10]
if not data: return f"{index:04x}"
return data.decode('gbk', errors='replace').rstrip('\x00')
def format_arg(val, type_name, ctx):
if type_name == "Null": return ""
if type_name == "WordID":
if val == 0: return "Null"
if val == 1: return "Empty"
val_str = read_word(ctx['f_word'], val)
return val_str.strip() if val_str else "Unknown"
if type_name == "MessageID":
msg = read_msg(ctx['f_msg_idx'], ctx['f_msg'], val)
return f"`{msg}`"
if type_name == "ScriptID":
return f"{val:04x}"
if type_name == "UInt16": return f"{val}"
if type_name == "Int16": return f"{val if val <= 32767 else val - 65536}"
if type_name in ("UInt16H", "Int"): return f"{val:04x}"
if type_name == "Binary": return f"{val:b}"
if type_name == "Boolean": return "True" if val != 0 else "False"
if type_name in LOOKUPS:
return LOOKUPS[type_name].get(val, f"{val:04x}")
return f"{val:04x}"
def process_script_block(f_scr, index, ctx):
while True:
data = f_scr[index * 8: index * 8 + 8]
if not data or len(data) != 8:
return -1
entry = struct.unpack('<4H', data)
cmd, args = entry[0], entry[1:]
raw_str = f"{index:04x}: {cmd:04x} {args[0]:04x} {args[1]:04x} {args[2]:04x} ;"
decoded_str = ""
if cmd in DESCS:
name, types = DESCS[cmd]
decoded_str = name
for a, t in zip(args, types):
s = format_arg(a, t, ctx)
if t != "Null":
decoded_str += " " + s
else:
decoded_str = f"{cmd:04x} {args[0]:04x} {args[1]:04x} {args[2]:04x}"
print(f"{raw_str}{decoded_str}")
index += 1
# 0=End, 2=Jump, 3=Call
if cmd in [0, 2, 3]:
break
return index
def main():
files = ["Sss.mkf", "M.msg", "Word.dat"]
missing = [f for f in files if not os.path.exists(f)]
if missing:
print(f"Error: Missing files: {', '.join(missing)}")
return
load_resources()
f_msg_idx = load_mkf_chunk("Sss.mkf", 3)
f_scr = load_mkf_chunk("Sss.mkf", 4)
f_word = open("Word.dat", 'rb').read()
f_msg = open("M.msg", 'rb').read()
ctx = {'f_word': f_word, 'f_msg_idx': f_msg_idx, 'f_msg': f_msg}
idx = 0
while True:
res = process_script_block(f_scr, idx, ctx)
if res == -1: break
idx = res
print()
if __name__ == "__main__":
main()
sudo apt install libpulse-dev libegl-dev libxkbcommon-dev libwayland-dev
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment