mirror of
https://github.com/pmret/papermario.git
synced 2024-11-08 12:02:30 +01:00
Merge pull request #122 from ethteck/splat-extensions
n64splat extensions
This commit is contained in:
commit
e723afa31c
@ -1 +1 @@
|
|||||||
Subproject commit 6106762b0561e40a640c11852a17bb87963c7ba8
|
Subproject commit 8e424ae728e9f8c05ea3d81af85a4f995bb29915
|
@ -6,6 +6,7 @@ options:
|
|||||||
mnemonic_ljust: 10
|
mnemonic_ljust: 10
|
||||||
ld_o_replace_extension: False
|
ld_o_replace_extension: False
|
||||||
ld_addrs_header: include/ld_addrs.h
|
ld_addrs_header: include/ld_addrs.h
|
||||||
|
extensions: splat_ext
|
||||||
segments:
|
segments:
|
||||||
- name: header
|
- name: header
|
||||||
type: header
|
type: header
|
||||||
|
67
tools/splat_ext/PaperMarioMapFS.py
Normal file
67
tools/splat_ext/PaperMarioMapFS.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import os
|
||||||
|
from segtypes.segment import N64Segment
|
||||||
|
from pathlib import Path
|
||||||
|
from util import Yay0decompress
|
||||||
|
|
||||||
|
|
||||||
|
def decode_null_terminated_ascii(data):
|
||||||
|
length = 0
|
||||||
|
for byte in data:
|
||||||
|
if byte == 0:
|
||||||
|
break
|
||||||
|
length += 1
|
||||||
|
|
||||||
|
return data[:length].decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
class N64SegPaperMarioMapFS(N64Segment):
|
||||||
|
def __init__(self, segment, next_segment, options):
|
||||||
|
super().__init__(segment, next_segment, options)
|
||||||
|
|
||||||
|
def split(self, rom_bytes, base_path):
|
||||||
|
bin_dir = self.create_split_dir(base_path, "bin/assets")
|
||||||
|
|
||||||
|
data = rom_bytes[self.rom_start: self.rom_end]
|
||||||
|
|
||||||
|
asset_idx = 0
|
||||||
|
while True:
|
||||||
|
asset_data = data[0x20 + asset_idx * 0x1C:]
|
||||||
|
|
||||||
|
name = decode_null_terminated_ascii(asset_data[0:])
|
||||||
|
offset = int.from_bytes(asset_data[0x10:0x14], byteorder="big")
|
||||||
|
size = int.from_bytes(asset_data[0x14:0x18], byteorder="big")
|
||||||
|
decompressed_size = int.from_bytes(
|
||||||
|
asset_data[0x18:0x1C], byteorder="big")
|
||||||
|
|
||||||
|
is_compressed = size != decompressed_size
|
||||||
|
|
||||||
|
if offset == 0:
|
||||||
|
path = None
|
||||||
|
else:
|
||||||
|
path = "{}.bin".format(name)
|
||||||
|
self.create_parent_dir(bin_dir, path)
|
||||||
|
|
||||||
|
if name == "end_data":
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(os.path.join(bin_dir, path), "wb") as f:
|
||||||
|
bytes = rom_bytes[self.rom_start + 0x20 +
|
||||||
|
offset: self.rom_start + 0x20 + offset + size]
|
||||||
|
|
||||||
|
if is_compressed:
|
||||||
|
self.log(f"Decompressing {name}...")
|
||||||
|
bytes = Yay0decompress.decompress_yay0(bytes)
|
||||||
|
|
||||||
|
f.write(bytes)
|
||||||
|
self.log(f"Wrote {name} to {Path(bin_dir, path)}")
|
||||||
|
|
||||||
|
asset_idx += 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_ld_files(self):
|
||||||
|
return [("bin/assets", self.name, ".data")]
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default_name(addr):
|
||||||
|
return "assets"
|
430
tools/splat_ext/PaperMarioMessages.py
Normal file
430
tools/splat_ext/PaperMarioMessages.py
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
from segtypes.segment import N64Segment
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CHARSET = {
|
||||||
|
0x00: "𝅘𝅥𝅮",
|
||||||
|
0x01: "!",
|
||||||
|
0x02: '"',
|
||||||
|
0x03: "#",
|
||||||
|
0x04: "$",
|
||||||
|
0x05: "%",
|
||||||
|
0x06: "&",
|
||||||
|
0x07: "'",
|
||||||
|
0x08: "(",
|
||||||
|
0x09: ")",
|
||||||
|
0x0A: "*",
|
||||||
|
0x0B: "+",
|
||||||
|
0x0C: ",",
|
||||||
|
0x0D: "-",
|
||||||
|
0x0E: ".",
|
||||||
|
0x0F: "/",
|
||||||
|
0x10: "0",
|
||||||
|
0x11: "1",
|
||||||
|
0x12: "2",
|
||||||
|
0x13: "3",
|
||||||
|
0x14: "4",
|
||||||
|
0x15: "5",
|
||||||
|
0x16: "6",
|
||||||
|
0x17: "7",
|
||||||
|
0x18: "8",
|
||||||
|
0x19: "9",
|
||||||
|
0x1A: ":",
|
||||||
|
0x1B: ";",
|
||||||
|
0x1C: "<",
|
||||||
|
0x1D: "=",
|
||||||
|
0x1E: ">",
|
||||||
|
0x1F: "?",
|
||||||
|
0x20: "@",
|
||||||
|
0x21: "A",
|
||||||
|
0x22: "B",
|
||||||
|
0x23: "C",
|
||||||
|
0x24: "D",
|
||||||
|
0x25: "E",
|
||||||
|
0x26: "F",
|
||||||
|
0x27: "G",
|
||||||
|
0x28: "H",
|
||||||
|
0x29: "I",
|
||||||
|
0x2A: "J",
|
||||||
|
0x2B: "K",
|
||||||
|
0x2C: "L",
|
||||||
|
0x2D: "M",
|
||||||
|
0x2E: "N",
|
||||||
|
0x2F: "O",
|
||||||
|
0x30: "P",
|
||||||
|
0x31: "Q",
|
||||||
|
0x32: "R",
|
||||||
|
0x33: "S",
|
||||||
|
0x34: "T",
|
||||||
|
0x35: "U",
|
||||||
|
0x36: "V",
|
||||||
|
0x37: "W",
|
||||||
|
0x38: "X",
|
||||||
|
0x39: "Y",
|
||||||
|
0x3A: "Z",
|
||||||
|
0x3B: "\\[",
|
||||||
|
0x3C: "¥",
|
||||||
|
0x3D: "]",
|
||||||
|
0x3E: "^",
|
||||||
|
0x3F: "_",
|
||||||
|
0x40: "`",
|
||||||
|
0x41: "a",
|
||||||
|
0x42: "b",
|
||||||
|
0x43: "c",
|
||||||
|
0x44: "d",
|
||||||
|
0x45: "e",
|
||||||
|
0x46: "f",
|
||||||
|
0x47: "g",
|
||||||
|
0x48: "h",
|
||||||
|
0x49: "i",
|
||||||
|
0x4A: "j",
|
||||||
|
0x4B: "k",
|
||||||
|
0x4C: "l",
|
||||||
|
0x4D: "m",
|
||||||
|
0x4E: "n",
|
||||||
|
0x4F: "o",
|
||||||
|
0x50: "p",
|
||||||
|
0x51: "q",
|
||||||
|
0x52: "r",
|
||||||
|
0x53: "s",
|
||||||
|
0x54: "t",
|
||||||
|
0x55: "u",
|
||||||
|
0x56: "v",
|
||||||
|
0x57: "w",
|
||||||
|
0x58: "x",
|
||||||
|
0x59: "y",
|
||||||
|
0x5A: "z",
|
||||||
|
0x5B: "{",
|
||||||
|
0x5C: "|",
|
||||||
|
0x5D: "}",
|
||||||
|
0x5E: "~",
|
||||||
|
0x5F: "°",
|
||||||
|
0x60: "À",
|
||||||
|
0x61: "Á",
|
||||||
|
0x62: "Â",
|
||||||
|
0x63: "Ä",
|
||||||
|
0x64: "Ç",
|
||||||
|
0x65: "È",
|
||||||
|
0x66: "É",
|
||||||
|
0x67: "Ê",
|
||||||
|
0x68: "Ë",
|
||||||
|
0x69: "Ì",
|
||||||
|
0x6A: "Í",
|
||||||
|
0x6B: "Î",
|
||||||
|
0x6C: "Ï",
|
||||||
|
0x6D: "Ñ",
|
||||||
|
0x6E: "Ò",
|
||||||
|
0x6F: "Ó",
|
||||||
|
0x70: "Ô",
|
||||||
|
0x71: "Ö",
|
||||||
|
0x72: "Ù",
|
||||||
|
0x73: "Ú",
|
||||||
|
0x74: "Û",
|
||||||
|
0x75: "Ü",
|
||||||
|
0x76: "ß",
|
||||||
|
0x77: "à",
|
||||||
|
0x78: "á",
|
||||||
|
0x79: "â",
|
||||||
|
0x7A: "ä",
|
||||||
|
0x7B: "ç",
|
||||||
|
0x7C: "è",
|
||||||
|
0x7D: "é",
|
||||||
|
0x7E: "ê",
|
||||||
|
0x7F: "ë",
|
||||||
|
0x80: "ì",
|
||||||
|
0x81: "í",
|
||||||
|
0x82: "î",
|
||||||
|
0x83: "ï",
|
||||||
|
0x84: "ñ",
|
||||||
|
0x85: "ò",
|
||||||
|
0x86: "ó",
|
||||||
|
0x87: "ô",
|
||||||
|
0x88: "ö",
|
||||||
|
0x89: "ù",
|
||||||
|
0x8A: "ú",
|
||||||
|
0x8B: "û",
|
||||||
|
0x8C: "ü",
|
||||||
|
0x8D: "¡",
|
||||||
|
0x8E: "¿",
|
||||||
|
0x8F: "ª",
|
||||||
|
0x90: "♥",
|
||||||
|
0x91: "★",
|
||||||
|
0x92: "↑",
|
||||||
|
0x93: "↓",
|
||||||
|
0x94: "←",
|
||||||
|
0x95: "→",
|
||||||
|
0x96: "●",
|
||||||
|
0x97: "✖",
|
||||||
|
0xA2: "“",
|
||||||
|
0xA3: "”",
|
||||||
|
0xA4: "‘",
|
||||||
|
0xA5: "’",
|
||||||
|
0xF7: " ",
|
||||||
|
0xF0: "[br]\n",
|
||||||
|
0xF1: "[prompt]",
|
||||||
|
0xF2: {None: lambda d: (f"[sleep {d[0]}]", 1)},
|
||||||
|
0xFB: "[next]\n",
|
||||||
|
0xFC: {
|
||||||
|
0x01: "[style=right]\n",
|
||||||
|
0x02: "[style=left]\n",
|
||||||
|
0x03: "[style=center]\n",
|
||||||
|
0x04: "[style=tattle]\n",
|
||||||
|
0x05: {None: lambda d: (f"[style=choice x={d[1]} y={d[3]} w={d[0]} h={d[2]}]\n", 4)},
|
||||||
|
0x06: "[style=inspect]\n",
|
||||||
|
0x07: "[style=sign]\n",
|
||||||
|
0x08: "[style=lamppost]\n",
|
||||||
|
0x09: "[style=postcard]\n",
|
||||||
|
0x0A: "[style=popup]\n",
|
||||||
|
0x0C: {None: lambda d: (f"[style=upgrade x={d[1]} y={d[3]} w={d[0]} h={d[2]}]\n", 4)},
|
||||||
|
0x0D: "[style=narrate]\n",
|
||||||
|
0x0E: "[style=epilogue]\n",
|
||||||
|
},
|
||||||
|
0xFF: {
|
||||||
|
0x00: {
|
||||||
|
0: "[font=normal]",
|
||||||
|
3: "[font=title]\n",
|
||||||
|
4: "[font=subtitle]\n",
|
||||||
|
},
|
||||||
|
0x05: {
|
||||||
|
0x0A: "[color=normal]",
|
||||||
|
0x20: "[color=red]",
|
||||||
|
0x21: "[color=pink]",
|
||||||
|
0x22: "[color=purple]",
|
||||||
|
0x23: "[color=blue]",
|
||||||
|
0x24: "[color=cyan]",
|
||||||
|
0x25: "[color=green]",
|
||||||
|
0x26: "[color=yellow]",
|
||||||
|
|
||||||
|
0x00: "[color=normal ctx=diary]",
|
||||||
|
0x07: "[color=red ctx=diary]",
|
||||||
|
|
||||||
|
0x17: "[color=dark ctx=inspect]",
|
||||||
|
|
||||||
|
0x18: "[color=normal ctx=sign]",
|
||||||
|
0x19: "[color=red ctx=sign]",
|
||||||
|
0x1A: "[color=blue ctx=sign]",
|
||||||
|
0x1B: "[color=green ctx=sign]",
|
||||||
|
|
||||||
|
0x28: "[color=red ctx=popup]",
|
||||||
|
0x29: "[color=pink ctx=popup]",
|
||||||
|
0x2A: "[color=purple ctx=popup]",
|
||||||
|
0x2B: "[color=blue ctx=popup]",
|
||||||
|
0x2C: "[color=teal ctx=popup]",
|
||||||
|
0x2D: "[color=green ctx=popup]",
|
||||||
|
0x2E: "[color=yellow ctx=popup]",
|
||||||
|
0x2F: "[color=normal ctx=popup]",
|
||||||
|
},
|
||||||
|
0x07: "[noskip]\n",
|
||||||
|
0x08: "[/noskip]\n",
|
||||||
|
0x09: "[instant]\n",
|
||||||
|
0x0A: "[/instant]\n",
|
||||||
|
0x0B: {None: lambda d: (f"[kerning={d[0]}]", 1)},
|
||||||
|
0x0C: {None: lambda d: (f"[scroll {d[0]}]", 1)},
|
||||||
|
0x0D: {None: lambda d: (f"[size x={d[0]} y={d[0]}]\n", 2)},
|
||||||
|
0x0E: "[/size]\n",
|
||||||
|
0x0F: {None: lambda d: (f"[speed delay={d[0]} chars={d[1]}]", 2)},
|
||||||
|
0x10: {None: lambda d: (f"[pos x={d[0]} y={d[1]}]", 2)},
|
||||||
|
0x11: {None: lambda d: (f"[pos y={d[0]}]", 1)},
|
||||||
|
0x12: {None: lambda d: (f"[indent {d[0]}]", 1)},
|
||||||
|
0x13: {None: lambda d: (f"[down {d[0]}]", 1)},
|
||||||
|
0x14: {None: lambda d: (f"[up {d[0]}]", 1)},
|
||||||
|
0x15: {None: lambda d: (f"[image {d[0]}]\n", 1)},
|
||||||
|
0x16: {None: lambda d: (f"[sprite {d[0]} {d[1]} {d[2]}]\n", 3)},
|
||||||
|
0x17: {None: lambda d: (f"[item {d[0]} {d[1]}]\n", 2)},
|
||||||
|
0x18: {None: lambda d: (f"[image {d[0]} {d[1]} {d[2]} {d[3]} {d[4]} {d[5]} {d[6]}]\n", 7)},
|
||||||
|
0x1E: {None: lambda d: (f"[cursor {d[0]}]", 1)},
|
||||||
|
0x1F: {None: lambda d: (f"[choicecount={d[0]}]", 1)},
|
||||||
|
0x20: {None: lambda d: (f"[cancel={d[0]}]", 1)},
|
||||||
|
0x21: {None: lambda d: (f"[option {d[0]}]", 1)},
|
||||||
|
0x24: {0xFF: {0x05: {
|
||||||
|
0x10: {0x98: {0xFF: {0x25: "Ⓐ"}}},
|
||||||
|
0x11: {0x99: {0xFF: {0x25: "Ⓑ"}}},
|
||||||
|
0x12: {0xA1: {0xFF: {0x25: "Ⓢ"}}},
|
||||||
|
0x13: {
|
||||||
|
0x9D: {0xFF: {0x25: "▲"}},
|
||||||
|
0x9E: {0xFF: {0x25: "▼"}},
|
||||||
|
0x9F: {0xFF: {0x25: "◀"}},
|
||||||
|
0xA0: {0xFF: {0x25: "▶"}},
|
||||||
|
},
|
||||||
|
0x14: {0x9C: {0xFF: {0x25: "Ⓩ"}}},
|
||||||
|
}}},
|
||||||
|
0x26: {
|
||||||
|
0x00: "[shaky]",
|
||||||
|
0x01: "[wavy]",
|
||||||
|
0x03: {None: lambda d: (f"[noise fade={d[0]}]", 1)},
|
||||||
|
0x05: {None: lambda d: (f"[faded-shaky fade={d[0]}]", 1)},
|
||||||
|
0x07: {None: lambda d: (f"[fade={d[0]}]", 1)},
|
||||||
|
0x0A: "[shout]",
|
||||||
|
0x0B: "[whisper]",
|
||||||
|
0x0C: "[scream]",
|
||||||
|
0x0D: "[chortle]",
|
||||||
|
0x0E: "[shadow]",
|
||||||
|
},
|
||||||
|
0x27: {
|
||||||
|
0x00: "[/shaky]",
|
||||||
|
0x01: "[/wavy]",
|
||||||
|
0x03: "[/noise]",
|
||||||
|
0x05: "[/faded-shaky]",
|
||||||
|
0x07: "[/fade]",
|
||||||
|
0x0A: "[/shout]",
|
||||||
|
0x0B: "[/whisper]",
|
||||||
|
0x0C: "[/scream]",
|
||||||
|
0x0D: "[/chortle]",
|
||||||
|
0x0E: "[/shadow]",
|
||||||
|
},
|
||||||
|
0x28: {None: lambda d: (f"[var {d[0]}]", 1)},
|
||||||
|
0x29: {None: lambda d: (f"[center {d[0]}]", 1)},
|
||||||
|
0x2E: {None: lambda d: (f"[volume={d[0]}]", 1)},
|
||||||
|
0x2F: {
|
||||||
|
1: "[sound=bowser]\n",
|
||||||
|
2: "[sound=spirit]\n",
|
||||||
|
None: lambda d: (f"[sound={d[0]}]\n", 1),
|
||||||
|
},
|
||||||
|
None: lambda d: (f"[func 0x{d[0]:X}]", 1),
|
||||||
|
},
|
||||||
|
None: lambda d: (f"[raw 0x{d[0]:02X}]", 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
CHARSET_CREDITS = {
|
||||||
|
**CHARSET,
|
||||||
|
0x00: "A",
|
||||||
|
0x01: "B",
|
||||||
|
0x02: "C",
|
||||||
|
0x03: "D",
|
||||||
|
0x04: "E",
|
||||||
|
0x05: "F",
|
||||||
|
0x06: "G",
|
||||||
|
0x07: "H",
|
||||||
|
0x08: "I",
|
||||||
|
0x09: "J",
|
||||||
|
0x0A: "K",
|
||||||
|
0x0B: "L",
|
||||||
|
0x0C: "M",
|
||||||
|
0x0D: "N",
|
||||||
|
0x0E: "O",
|
||||||
|
0x0F: "P",
|
||||||
|
0x10: "Q",
|
||||||
|
0x11: "R",
|
||||||
|
0x12: "S",
|
||||||
|
0x13: "T",
|
||||||
|
0x14: "U",
|
||||||
|
0x15: "V",
|
||||||
|
0x16: "W",
|
||||||
|
0x17: "X",
|
||||||
|
0x18: "Y",
|
||||||
|
0x19: "Z",
|
||||||
|
0x1A: "'",
|
||||||
|
0x1B: ".",
|
||||||
|
0x1C: ",",
|
||||||
|
0x1D: "0",
|
||||||
|
0x1E: "1",
|
||||||
|
0x1F: "2",
|
||||||
|
0x20: "3",
|
||||||
|
0x21: "4",
|
||||||
|
0x22: "5",
|
||||||
|
0x23: "6",
|
||||||
|
0x24: "7",
|
||||||
|
0x25: "8",
|
||||||
|
0x26: "9",
|
||||||
|
0x27: "©",
|
||||||
|
0x28: "&",
|
||||||
|
0xF7: " ",
|
||||||
|
}
|
||||||
|
|
||||||
|
class N64SegPaperMarioMessages(N64Segment):
|
||||||
|
def __init__(self, segment, next_segment, options):
|
||||||
|
super().__init__(segment, next_segment, options)
|
||||||
|
self.files = segment.get("files", []) if type(segment) is dict else []
|
||||||
|
|
||||||
|
def split(self, rom_bytes, base_path):
|
||||||
|
data = rom_bytes[self.rom_start: self.rom_end]
|
||||||
|
|
||||||
|
section_offsets = []
|
||||||
|
pos = 0
|
||||||
|
while True:
|
||||||
|
offset = int.from_bytes(data[pos:pos+4], byteorder="big")
|
||||||
|
|
||||||
|
if offset == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
section_offsets.append(offset)
|
||||||
|
pos += 4
|
||||||
|
|
||||||
|
for i, section_offset in enumerate(section_offsets):
|
||||||
|
name = f"{i:02X}"
|
||||||
|
if len(self.files) >= i:
|
||||||
|
name = self.files[i]
|
||||||
|
|
||||||
|
msg_offsets = []
|
||||||
|
pos = section_offset
|
||||||
|
while True:
|
||||||
|
offset = int.from_bytes(data[pos:pos+4], byteorder="big")
|
||||||
|
|
||||||
|
if offset == section_offset:
|
||||||
|
break
|
||||||
|
|
||||||
|
msg_offsets.append(offset)
|
||||||
|
pos += 4
|
||||||
|
|
||||||
|
self.log(f"Reading {len(msg_offsets)} messages in section {name} (0x{i:02X})")
|
||||||
|
|
||||||
|
path = Path(base_path, self.name, name + ".msg")
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "w") as f:
|
||||||
|
for j, msg_offset in enumerate(msg_offsets):
|
||||||
|
if j != 0:
|
||||||
|
f.write("\n")
|
||||||
|
f.write(f"[message section=0x{i:02X} index={j}]\n")
|
||||||
|
self.write_message_markup(data[msg_offset:], f)
|
||||||
|
f.write("\n[/message]\n")
|
||||||
|
|
||||||
|
def get_ld_files(self):
|
||||||
|
return [("", self.name, ".data")]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default_name(addr):
|
||||||
|
return "msg"
|
||||||
|
|
||||||
|
def write_message_markup(self, data, f):
|
||||||
|
pos = 0
|
||||||
|
font = CHARSET
|
||||||
|
|
||||||
|
while data[pos] != 0xFD:
|
||||||
|
markup, delta = self.char_to_markup(data[pos:], charset=font)
|
||||||
|
|
||||||
|
f.write(markup)
|
||||||
|
pos += delta
|
||||||
|
|
||||||
|
if markup == "[font=title]\n" or markup == "[font=subtitle]\n":
|
||||||
|
font = CHARSET_CREDITS
|
||||||
|
elif markup == "[font=normal]":
|
||||||
|
font = CHARSET
|
||||||
|
|
||||||
|
def char_to_markup(self, data, charset=CHARSET):
|
||||||
|
value = None
|
||||||
|
char = int(data[0])
|
||||||
|
|
||||||
|
if char in charset:
|
||||||
|
value = charset[char]
|
||||||
|
elif None in charset:
|
||||||
|
value = charset[None]
|
||||||
|
|
||||||
|
if type(value) is str:
|
||||||
|
return value, 1
|
||||||
|
if callable(value):
|
||||||
|
return value(data)
|
||||||
|
if type(value) is dict:
|
||||||
|
markup, delta = self.char_to_markup(data[1:], charset=value)
|
||||||
|
|
||||||
|
if markup is None:
|
||||||
|
if None in charset:
|
||||||
|
value = charset[None]
|
||||||
|
|
||||||
|
if callable(value):
|
||||||
|
return value(data)
|
||||||
|
|
||||||
|
return value, 1
|
||||||
|
else:
|
||||||
|
return markup, delta + 1
|
||||||
|
|
||||||
|
|
||||||
|
return None, 0
|
0
tools/splat_ext/__init__.py
Normal file
0
tools/splat_ext/__init__.py
Normal file
Loading…
Reference in New Issue
Block a user