mirror of
https://github.com/ACreTeam/ac-decomp
synced 2026-07-07 12:03:27 -04:00
Add tools to interface with .rarc files, texture files, and message files
This commit is contained in:
@@ -0,0 +1,766 @@
|
||||
import argparse
|
||||
import struct
|
||||
import os
|
||||
import re
|
||||
|
||||
CHAR_MAP = [
|
||||
"¡",
|
||||
"¿",
|
||||
"Ä",
|
||||
"À",
|
||||
"Á",
|
||||
"Â",
|
||||
"Ã",
|
||||
"Å",
|
||||
"Ç",
|
||||
"È",
|
||||
"É",
|
||||
"Ê",
|
||||
"Ë",
|
||||
"Ì",
|
||||
"Í",
|
||||
"Î",
|
||||
"Ï",
|
||||
"Ð",
|
||||
"Ñ",
|
||||
"Ò",
|
||||
"Ó",
|
||||
"Ô",
|
||||
"Õ",
|
||||
"Ö",
|
||||
"Ø",
|
||||
"Ù",
|
||||
"Ú",
|
||||
"Û",
|
||||
"Ü",
|
||||
"ß",
|
||||
"Þ",
|
||||
"à",
|
||||
" ",
|
||||
"!",
|
||||
'"',
|
||||
"á",
|
||||
"â",
|
||||
"%",
|
||||
"&",
|
||||
"'",
|
||||
"(",
|
||||
")",
|
||||
"~",
|
||||
"♥",
|
||||
",",
|
||||
"-",
|
||||
".",
|
||||
"♪",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
":",
|
||||
"🌢",
|
||||
"<",
|
||||
"=",
|
||||
">",
|
||||
"?",
|
||||
"@",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"ã",
|
||||
"💢",
|
||||
"ä",
|
||||
"å",
|
||||
"_",
|
||||
"ç",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"è",
|
||||
"é",
|
||||
"ê",
|
||||
"ë",
|
||||
"\u007f",
|
||||
"�",
|
||||
"ì",
|
||||
"í",
|
||||
"î",
|
||||
"ï",
|
||||
"•",
|
||||
"ð",
|
||||
"ñ",
|
||||
"ò",
|
||||
"ó",
|
||||
"ô",
|
||||
"õ",
|
||||
"ö",
|
||||
"⁰",
|
||||
"ù",
|
||||
"ú",
|
||||
"ー",
|
||||
"û",
|
||||
"ü",
|
||||
"ý",
|
||||
"ÿ",
|
||||
"þ",
|
||||
"Ý",
|
||||
"¦",
|
||||
"§",
|
||||
"ḏ",
|
||||
"ṉ",
|
||||
"‖",
|
||||
"µ",
|
||||
"³",
|
||||
"²",
|
||||
"¹", # note that a̱ and o̱ had to be changed because they're actually two characters in unicode. a̱ -> ḏ | o̱ -> ṉ
|
||||
"¯",
|
||||
"¬",
|
||||
"Æ",
|
||||
"æ",
|
||||
"„",
|
||||
"»",
|
||||
"«",
|
||||
"☀",
|
||||
"☁",
|
||||
"☂",
|
||||
"🌬",
|
||||
"☃",
|
||||
"∋",
|
||||
"∈",
|
||||
"/",
|
||||
"∞",
|
||||
"○",
|
||||
"🗙",
|
||||
"□",
|
||||
"△",
|
||||
"+",
|
||||
"⚡",
|
||||
"♂",
|
||||
"♀",
|
||||
"🍀",
|
||||
"★",
|
||||
"💀",
|
||||
"😮",
|
||||
"😄",
|
||||
"😣",
|
||||
"😠",
|
||||
"😃",
|
||||
"×",
|
||||
"➗",
|
||||
"🔨",
|
||||
"🎀",
|
||||
"✉",
|
||||
"💰",
|
||||
"🐾",
|
||||
"🐶",
|
||||
"🐱",
|
||||
"🐰",
|
||||
"🐦",
|
||||
"🐮",
|
||||
"🐷",
|
||||
"\n",
|
||||
"🐟",
|
||||
"🐞",
|
||||
";",
|
||||
"#",
|
||||
"\u00d2",
|
||||
"\u00d3",
|
||||
"⚷",
|
||||
"\u00d5",
|
||||
"\u00d6",
|
||||
"\u00d7",
|
||||
"\u00d8",
|
||||
"\u00d9",
|
||||
"\u00da",
|
||||
"\u00db",
|
||||
"\u00dc",
|
||||
"Ỳ",
|
||||
"ꟓ",
|
||||
"\u00df",
|
||||
"\u00e0",
|
||||
"\u00e1",
|
||||
"\u00e2",
|
||||
"\u00e3",
|
||||
"\u00e4",
|
||||
"\u00e5",
|
||||
"\u00e6",
|
||||
"\u00e7",
|
||||
"\u00e8",
|
||||
"\u00e9",
|
||||
"\u00ea",
|
||||
"\u00eb",
|
||||
"\u00ec",
|
||||
"\u00ed",
|
||||
"\u00ee",
|
||||
"\u00ef",
|
||||
"\u00f0",
|
||||
"\u00f1",
|
||||
"\u00f2",
|
||||
"\u00f3",
|
||||
"\u00f4",
|
||||
"\u00f5",
|
||||
"\u00f6",
|
||||
"÷",
|
||||
"\u00f8",
|
||||
"\u00f9",
|
||||
"\u00fa",
|
||||
"\u00fb",
|
||||
"\u00fc",
|
||||
"\u00fd",
|
||||
"\u00fe",
|
||||
"\u00ff",
|
||||
]
|
||||
|
||||
CONT_SIZES = [
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
2,
|
||||
5,
|
||||
2,
|
||||
2,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
6,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
6,
|
||||
3,
|
||||
3,
|
||||
4,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
6,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
12,
|
||||
14,
|
||||
]
|
||||
|
||||
COMMANDS = [
|
||||
"MSGEND",
|
||||
"MSGCONTINUE",
|
||||
"MSGCLEAR",
|
||||
"PAUSE",
|
||||
"BTN",
|
||||
"TEXTCOLOR",
|
||||
"ABLECANCEL",
|
||||
"UNABLECANCEL",
|
||||
"DEMOPLR",
|
||||
"DEMONPC0",
|
||||
"DEMONPC1",
|
||||
"DEMONPC2",
|
||||
"DEMONPCQST",
|
||||
"OPENCHOICE",
|
||||
"SETFORCEMSG",
|
||||
"SETNEXTMSG0",
|
||||
"SETNEXTMSG1",
|
||||
"SETNEXTMSG2",
|
||||
"SETNEXTMSG3",
|
||||
"SETNEXTMSGRND2",
|
||||
"SETNEXTMSGRND3",
|
||||
"SETNEXTMSGRND4",
|
||||
"SETSELSTR2",
|
||||
"SETSELSTR3",
|
||||
"SETSELSTR4",
|
||||
"FORCENEXT",
|
||||
"STR_PLAYERNAME",
|
||||
"STR_TALKNAME",
|
||||
"STR_TAIL",
|
||||
"STR_YEAR",
|
||||
"STR_MONTH",
|
||||
"STR_WEEK",
|
||||
"STR_DAY",
|
||||
"STR_HOUR",
|
||||
"STR_MIN",
|
||||
"STR_SEC",
|
||||
"STR_FREE0",
|
||||
"STR_FREE1",
|
||||
"STR_FREE2",
|
||||
"STR_FREE3",
|
||||
"STR_FREE4",
|
||||
"STR_FREE5",
|
||||
"STR_FREE6",
|
||||
"STR_FREE7",
|
||||
"STR_FREE8",
|
||||
"STR_FREE9",
|
||||
"STR_DETERMINATION",
|
||||
"STR_COUNTRYNAME",
|
||||
"STR_RNDNUM",
|
||||
"STR_ITEM0",
|
||||
"STR_ITEM1",
|
||||
"STR_ITEM2",
|
||||
"STR_ITEM3",
|
||||
"STR_ITEM4",
|
||||
"STR_FREE10",
|
||||
"STR_FREE11",
|
||||
"STR_FREE12",
|
||||
"STR_FREE13",
|
||||
"STR_FREE14",
|
||||
"STR_FREE15",
|
||||
"STR_FREE16",
|
||||
"STR_FREE17",
|
||||
"STR_FREE18",
|
||||
"STR_FREE19",
|
||||
"STR_MAIL",
|
||||
"LUCK_NEUTRAL",
|
||||
"LUCK_RELATIONSHIP",
|
||||
"LUCK_UNPOPULAR",
|
||||
"LUCK_BAD",
|
||||
"LUCK_MONEY",
|
||||
"LUCK_GOODS",
|
||||
"LUCK_6",
|
||||
"LUCK_7",
|
||||
"LUCK_8",
|
||||
"LUCK_9",
|
||||
"MSGCONTENTS_NORMAL",
|
||||
"MSGCONTENTS_ANGRY",
|
||||
"MSGCONTENTS_SAD",
|
||||
"MSGCONTENTS_FUN",
|
||||
"MSGCONTENTS_SLEEPY",
|
||||
"COLORCHARS",
|
||||
"SNDCUT",
|
||||
"LINEOFS",
|
||||
"LINETYPE",
|
||||
"CHARSCALE",
|
||||
"BTN2",
|
||||
"BGMMAKE",
|
||||
"BGMDELETE",
|
||||
"MSGTIMEEND",
|
||||
"SNDTRGSYS",
|
||||
"LINESCALE",
|
||||
"SNDNOPAGE",
|
||||
"VOICETRUE",
|
||||
"VOICEFALSE",
|
||||
"SELNOB",
|
||||
"GIVEOPEN",
|
||||
"GIVECLOSE",
|
||||
"MSGCONTENTS_GLOOMY",
|
||||
"SELNOBCLOSE",
|
||||
"SETNEXTMSGRNDSECTION",
|
||||
"AGBDUMMY0",
|
||||
"AGBDUMMY1",
|
||||
"AGBDUMMY2",
|
||||
"SPACE",
|
||||
"AGBDUMMY3",
|
||||
"AGBDUMMY4",
|
||||
"MALEFEMALECHK",
|
||||
"AGBDUMMY5",
|
||||
"AGBDUMMY6",
|
||||
"AGBDUMMY7",
|
||||
"AGBDUMMY8",
|
||||
"AGBDUMMY9",
|
||||
"AGBDUMMY10",
|
||||
"STR_ISLANDNAME",
|
||||
"SETCURSORJUST",
|
||||
"CLRCUSRORJUST",
|
||||
"CUTARTICLE",
|
||||
"CAPTIALIZE",
|
||||
"STR_AMPM",
|
||||
"SETNEXTMSG4",
|
||||
"SETNEXTMSG5",
|
||||
"SETSELSTR5",
|
||||
"SETSELSTR6",
|
||||
]
|
||||
|
||||
|
||||
def decode_control_code(ba: bytearray, idx: int):
|
||||
if ba[idx] != 0x7F:
|
||||
raise ValueError("First character must be 0x7F")
|
||||
cont_type = ba[idx + 1]
|
||||
if cont_type >= len(CONT_SIZES):
|
||||
raise ValueError(f"Invalid control code id {cont_type:02X}")
|
||||
cont_size = CONT_SIZES[cont_type]
|
||||
if len(ba) < idx + cont_size:
|
||||
raise ValueError(
|
||||
f"Bytearray is not large enough for control code {cont_type:02X}"
|
||||
)
|
||||
|
||||
cmd = COMMANDS[cont_type]
|
||||
if cmd is not None:
|
||||
if cont_size > 2:
|
||||
hex_values = " ".join(
|
||||
"{:02X}".format(b)
|
||||
for b in ba[(idx + 2) : (idx + CONT_SIZES[cont_type])]
|
||||
)
|
||||
return f"<<{cmd} [{hex_values}]>>", cont_size
|
||||
else:
|
||||
return f"<<{cmd}>>", cont_size
|
||||
else:
|
||||
hex_values = " ".join(
|
||||
"{:02X}".format(b) for b in ba[(idx + 2) : (idx + CONT_SIZES[cont_type])]
|
||||
)
|
||||
return f"<<Control Code [{cont_type:02X}] [{hex_values}]>>", cont_size
|
||||
|
||||
|
||||
def decode_entry(ba: bytearray, start: int, end: int, idx: int):
|
||||
parts = [f"[[ENTRY {idx} START]]\n"] # Use a list to collect string parts
|
||||
i = start
|
||||
while i < end:
|
||||
char = ba[i]
|
||||
if char == 0x7F:
|
||||
cont_str, cont_size = decode_control_code(ba, i)
|
||||
parts.append(cont_str)
|
||||
i += cont_size
|
||||
else:
|
||||
parts.append(CHAR_MAP[ba[i]])
|
||||
i += 1
|
||||
parts.append("\n\n")
|
||||
return "".join(parts) # Join the parts into a final string at the end
|
||||
|
||||
|
||||
def decode_file(data_path: str, table_path: str, out_path: str):
|
||||
idx = 0
|
||||
last_end = 0
|
||||
output_buffer = []
|
||||
|
||||
with open(data_path, "rb") as df, open(table_path, "rb") as tf, open(
|
||||
out_path, "w"
|
||||
) as of:
|
||||
while True:
|
||||
bytes = tf.read(4)
|
||||
if not bytes:
|
||||
break
|
||||
end = struct.unpack(">I", bytes)[0]
|
||||
if end != 0:
|
||||
size = end - last_end
|
||||
last_end = end
|
||||
data = bytearray(df.read(size))
|
||||
decoded_str = decode_entry(data, 0, size, idx)
|
||||
output_buffer.append(decoded_str)
|
||||
|
||||
idx += 1
|
||||
# Write buffer content to file to reduce write calls
|
||||
if len(output_buffer) >= 8192:
|
||||
of.write("".join(output_buffer))
|
||||
output_buffer.clear()
|
||||
|
||||
# Write remaining buffer content to file
|
||||
if output_buffer:
|
||||
of.write("".join(output_buffer))
|
||||
|
||||
|
||||
# Function to convert a hex string to a list of integers
|
||||
def convert_hex_string_to_ints(hex_str):
|
||||
# Remove spaces and convert to upper case
|
||||
clean_str = hex_str.replace(" ", "").upper()
|
||||
# Convert every two characters to an integer
|
||||
return [int(clean_str[i : i + 2], 16) for i in range(0, len(clean_str), 2)]
|
||||
|
||||
|
||||
# pre-compiled regex patterns
|
||||
cmd_pattern = re.compile(r"^([\w\s]+)")
|
||||
arg_pattern = re.compile(r"\[([0-9A-Fa-f\s]*)\]")
|
||||
|
||||
|
||||
def encode_control_code(cont_code_str: str, start_idx: int = 0, end_idx: int = None):
|
||||
sliced_str = cont_code_str[start_idx:end_idx] # bad but necessary in python
|
||||
cmd_match = cmd_pattern.match(sliced_str)
|
||||
if not cmd_match:
|
||||
raise ValueError("Missing command in control code!")
|
||||
|
||||
cmd = cmd_match.group(1).strip()
|
||||
args = arg_pattern.findall(sliced_str)
|
||||
|
||||
cmd_idx = COMMANDS.index(cmd)
|
||||
arg_list = [
|
||||
byte for hex_str in args for byte in convert_hex_string_to_ints(hex_str)
|
||||
]
|
||||
return cmd_idx, arg_list
|
||||
|
||||
|
||||
def encode_entry(entry: str):
|
||||
ba = bytearray()
|
||||
i = 0
|
||||
max = len(entry)
|
||||
while i < max:
|
||||
char = entry[i]
|
||||
if char == "<" and i < max - 1 and entry[i + 1] == "<":
|
||||
start = i + 2
|
||||
end = start
|
||||
found = False
|
||||
while end < max:
|
||||
if entry[end] == ">" and end < max - 1 and entry[end + 1] == ">":
|
||||
found = True
|
||||
break
|
||||
end += 1
|
||||
if found:
|
||||
cmd_idx, arg_list = encode_control_code(entry, start, end)
|
||||
cmd_size = CONT_SIZES[cmd_idx]
|
||||
if len(arg_list) != cmd_size - 2:
|
||||
raise ValueError(
|
||||
f"Expected args of length {cmd_size - 2} for command {COMMANDS[cmd_idx]}, but got {len(arg_list)}"
|
||||
)
|
||||
ba.append(0x7F)
|
||||
ba.append(cmd_idx)
|
||||
ba.extend(arg_list)
|
||||
i = end + 2
|
||||
continue
|
||||
|
||||
ba.append(CHAR_MAP.index(char))
|
||||
i += 1
|
||||
return ba
|
||||
|
||||
|
||||
def encode_file(
|
||||
file_path: str,
|
||||
data_path: str,
|
||||
table_path: str,
|
||||
data_size: int = -1,
|
||||
table_size: int = -1,
|
||||
):
|
||||
entries = {}
|
||||
current_entry = None
|
||||
recording = False
|
||||
|
||||
with open(file_path, "r") as tf, open(data_path, "wb") as df, open(
|
||||
table_path, "wb"
|
||||
) as tabf:
|
||||
for line in tf:
|
||||
stripped_line = line.strip()
|
||||
|
||||
# Check for entry start
|
||||
if stripped_line.startswith("[[ENTRY") and stripped_line.endswith("START]]"):
|
||||
entry_index = stripped_line.split()[1] # Assuming the format [[ENTRY X START]]
|
||||
current_entry = entry_index
|
||||
entries[current_entry] = []
|
||||
recording = True
|
||||
continue
|
||||
|
||||
# Check for entry end
|
||||
if (
|
||||
line.find("<<MSGEND>>") != -1
|
||||
or line.find("<<MSGTIMEEND")
|
||||
or line.find("<<MSGCONTINUE>>")
|
||||
):
|
||||
entries[current_entry].append(line)
|
||||
recording = False
|
||||
continue
|
||||
|
||||
# Record lines if within an entry and not empty
|
||||
if recording and line:
|
||||
entries[current_entry].append(line)
|
||||
|
||||
# end_ofs = 0
|
||||
for entry in entries:
|
||||
entries[entry] = encode_entry("".join(entries[entry]).rstrip())
|
||||
df.write(entries[entry])
|
||||
tabf.write(struct.pack(">I", df.tell()))
|
||||
|
||||
if data_size > 0:
|
||||
data_remain = data_size - df.tell()
|
||||
if data_remain > 0:
|
||||
df.write(b"\x00" * data_remain)
|
||||
|
||||
if table_size > 0:
|
||||
table_remain = table_size - tabf.tell()
|
||||
if table_remain > 0:
|
||||
tabf.write(b"\x00" * table_remain)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pack or dump Animal Crossing text files."
|
||||
)
|
||||
parser.add_argument("-m", help="The mode to run. Valid arguments are un[pack].")
|
||||
parser.add_argument("path", help="The path of the source file.")
|
||||
parser.add_argument("out", help="The path of the destination file.")
|
||||
parser.add_argument(
|
||||
"--data_size",
|
||||
help="Optional hexadecimal padded size for the data file.",
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--table_size",
|
||||
help="Optional hexadecimal padded size for the table file.",
|
||||
required=False,
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.m.lower() == "pack":
|
||||
# Create *_table.bin path
|
||||
dir_name, file_name = os.path.split(args.out)
|
||||
name, ext = os.path.splitext(file_name)
|
||||
new_file_name = f"{name}_table{ext}"
|
||||
|
||||
# encode
|
||||
encode_file(
|
||||
args.path,
|
||||
args.out,
|
||||
os.path.join(dir_name, new_file_name),
|
||||
int(args.data_size, 16) if args.data_size is not None else -1,
|
||||
int(args.table_size, 16) if args.table_size is not None else -1,
|
||||
)
|
||||
elif args.m.lower() == "unpack":
|
||||
# Search for *_table.bin
|
||||
dir_name, file_name = os.path.split(args.path)
|
||||
name, ext = os.path.splitext(file_name)
|
||||
new_file_name = f"{name}_table{ext}"
|
||||
table_path = os.path.join(dir_name, new_file_name)
|
||||
|
||||
if not os.path.exists(table_path):
|
||||
raise Exception(
|
||||
f"Couldn't find a valid table path. Please ensure {new_file_name} exists!"
|
||||
)
|
||||
|
||||
# decode
|
||||
decode_file(args.path, table_path, args.out)
|
||||
else:
|
||||
raise Exception("Invalid mode! Please use -m un[pack]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user