Last active
December 25, 2025 09:25
-
-
Save scturtle/3ac8798853566c4954c7350d49fb0359 to your computer and use it in GitHub Desktop.
pal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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))}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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