2023-05-19 07:30:53 -05:00
|
|
|
|
import io
|
2022-01-23 08:25:40 -05:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import re
|
2023-05-19 07:30:53 -05:00
|
|
|
|
import shutil
|
|
|
|
|
|
import struct
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
from itertools import tee
|
2022-01-23 08:25:40 -05:00
|
|
|
|
from pathlib import Path
|
2023-05-19 07:30:53 -05:00
|
|
|
|
|
|
|
|
|
|
import lxml.etree as etree
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
from tqdm import tqdm
|
2023-05-19 08:07:41 -05:00
|
|
|
|
from pythonlib.formats.scpk import Scpk
|
2023-05-19 07:30:53 -05:00
|
|
|
|
|
2023-05-19 08:07:41 -05:00
|
|
|
|
import pythonlib.utils.comptolib as comptolib
|
|
|
|
|
|
import pythonlib.formats.pak2 as pak2lib
|
|
|
|
|
|
from pythonlib.formats.theirsce import Theirsce
|
|
|
|
|
|
from pythonlib.formats.theirsce_instructions import (AluOperation, InstructionType,
|
2023-05-19 07:30:53 -05:00
|
|
|
|
TheirsceBaseInstruction)
|
|
|
|
|
|
from .ToolsTales import ToolsTales
|
|
|
|
|
|
|
2023-01-06 22:45:40 -05:00
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class LineEntry:
|
|
|
|
|
|
names: list[str]
|
|
|
|
|
|
text: str
|
|
|
|
|
|
offset: int
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class NameEntry:
|
|
|
|
|
|
index: int
|
|
|
|
|
|
offsets: list[int]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VARIABLE_NAME = "[VARIABLE]"
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
|
|
|
|
|
class ToolsTOR(ToolsTales):
|
|
|
|
|
|
|
|
|
|
|
|
POINTERS_BEGIN = 0xD76B0 # Offset to DAT.BIN pointer list start in SLPS_254.50 file
|
|
|
|
|
|
POINTERS_END = 0xE60C8 # Offset to DAT.BIN pointer list end in SLPS_254.50 file
|
|
|
|
|
|
HIGH_BITS = 0xFFFFFFC0
|
|
|
|
|
|
LOW_BITS = 0x3F
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
|
|
|
|
|
#Path to used
|
2023-05-19 08:07:41 -05:00
|
|
|
|
# fmt: off
|
2023-05-19 07:30:53 -05:00
|
|
|
|
dat_bin_original = '../Data/Tales-Of-Rebirth/Disc/Original/DAT.BIN'
|
|
|
|
|
|
dat_bin_new = '../Data/Tales-Of-Rebirth/Disc/New/DAT.BIN'
|
|
|
|
|
|
elf_original = '../Data/Tales-Of-Rebirth/Disc/Original/SLPS_254.50'
|
|
|
|
|
|
elf_new = '../Data/Tales-Of-Rebirth/Disc/New/SLPS_254.50'
|
|
|
|
|
|
story_XML_new = '../Tales-Of-Rebirth/Data/TOR/Story/' #Story XML files will be extracted here
|
|
|
|
|
|
story_XML_patch = '../Data/Tales-Of-Rebirth/Story/' #Story XML files will be extracted here
|
|
|
|
|
|
skit_XML_patch = '../Data/Tales-Of-Rebirth/Skits/' #Skits XML files will be extracted here
|
|
|
|
|
|
skit_XML_new = '../Tales-Of-Rebirth/Data/TOR/Skits/'
|
|
|
|
|
|
dat_archive_extract = '../Data/Tales-Of-Rebirth/DAT/'
|
2023-05-19 08:07:41 -05:00
|
|
|
|
# fmt: on
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, tbl):
|
|
|
|
|
|
|
2022-10-02 08:18:37 -04:00
|
|
|
|
super().__init__("TOR", tbl, "Tales-Of-Rebirth")
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
2022-09-02 20:56:48 -04:00
|
|
|
|
with open("../{}/Data/{}/Misc/{}".format(self.repo_name, self.gameName, self.tblFile), encoding="utf-8") as f:
|
2023-05-16 00:05:37 -05:00
|
|
|
|
jsonRaw = json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
for k, v in jsonRaw.items():
|
|
|
|
|
|
self.jsonTblTags[k] = {int(k2, 16): v2 for k2, v2 in v.items()}
|
2022-07-16 15:11:15 -04:00
|
|
|
|
|
2023-05-16 00:05:37 -05:00
|
|
|
|
for k, v in self.jsonTblTags.items():
|
|
|
|
|
|
self.ijsonTblTags[k] = {v2: k2 for k2, v2 in v.items()}
|
2022-07-16 15:11:15 -04:00
|
|
|
|
self.id = 1
|
2023-05-19 07:30:53 -05:00
|
|
|
|
# byteCode
|
2022-02-18 15:42:52 -05:00
|
|
|
|
self.story_byte_code = b"\xF8"
|
2023-01-06 22:39:17 -05:00
|
|
|
|
self.string_opcode = InstructionType.STRING
|
2023-05-14 16:50:41 -04:00
|
|
|
|
self.list_status_insertion = ['Done', 'Proofreading', 'Editing']
|
2022-02-10 20:13:15 -05:00
|
|
|
|
|
2022-09-18 18:22:59 -04:00
|
|
|
|
self.mkdir('../Data/{}/DAT'.format(self.repo_name))
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
2023-01-06 22:16:30 -05:00
|
|
|
|
# Replace n occurences of a string starting from the right
|
|
|
|
|
|
def rreplace(self, s, old, new, occurrence):
|
|
|
|
|
|
li = s.rsplit(old, occurrence)
|
|
|
|
|
|
return new.join(li)
|
|
|
|
|
|
|
|
|
|
|
|
def add_line_break(self, text):
|
2023-05-19 07:30:53 -05:00
|
|
|
|
temp = ""
|
|
|
|
|
|
currentLineSize = 0
|
2023-01-06 22:16:30 -05:00
|
|
|
|
|
|
|
|
|
|
text_size = len(text)
|
|
|
|
|
|
max_size = 32
|
|
|
|
|
|
split_space = text.split(" ")
|
|
|
|
|
|
|
|
|
|
|
|
for word in split_space:
|
|
|
|
|
|
currentLineSize += (len(word) + 1)
|
|
|
|
|
|
|
|
|
|
|
|
if currentLineSize <= max_size:
|
|
|
|
|
|
temp = temp + word + ' '
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
temp = temp + '\n' + word + ' '
|
|
|
|
|
|
currentLineSize = 0
|
|
|
|
|
|
|
|
|
|
|
|
temp = temp.replace(" \n", "\n")
|
|
|
|
|
|
temp = self.rreplace(temp, " ", "", 1)
|
|
|
|
|
|
|
|
|
|
|
|
return temp
|
|
|
|
|
|
def clean_text(self, text):
|
|
|
|
|
|
text = re.sub(r"\n ", "\n", text)
|
|
|
|
|
|
text = re.sub(r"\n", "", text)
|
|
|
|
|
|
text = re.sub(r"(<\w+:?\w+>)", "", text)
|
|
|
|
|
|
text = re.sub(r"\[\w+=*\w+\]", "", text)
|
|
|
|
|
|
text = re.sub(r" ", "", text)
|
|
|
|
|
|
text = re.sub(u'\u3000', '', text)
|
|
|
|
|
|
text = re.sub(r" ", "", text)
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
# Extract/Transform Lauren translation
|
|
|
|
|
|
def extract_Lauren_Translation(self):
|
|
|
|
|
|
|
|
|
|
|
|
# Load Lauren's googlesheet data inside a dataframe
|
|
|
|
|
|
df = self.extract_Google_Sheets("1-XwzS7F0SaLlXwv1KS6RcTEYYORH2DDb1bMRy5VM5oo", "Story")
|
|
|
|
|
|
|
|
|
|
|
|
# 1) Make some renaming and transformations
|
|
|
|
|
|
df = df.rename(columns={"KEY": "File", "Japanese": "JapaneseText", "Lauren's Script": "EnglishText"})
|
|
|
|
|
|
|
|
|
|
|
|
# 2) Filter only relevant rows and columns from the googlesheet
|
|
|
|
|
|
df = df.loc[(df['EnglishText'] != "") & (df['JapaneseText'] != ""), :]
|
|
|
|
|
|
df = df[['File', 'JapaneseText', 'EnglishText']]
|
|
|
|
|
|
|
|
|
|
|
|
# 3) Make some transformations to the JapaneseText so we can better match with XML
|
|
|
|
|
|
df['File'] = df['File'].apply(lambda x: x.split("_")[0] + ".xml")
|
|
|
|
|
|
df['JapaneseText'] = df['JapaneseText'].apply(lambda x: self.clean_text(x))
|
|
|
|
|
|
return df
|
|
|
|
|
|
|
|
|
|
|
|
# Transfer Lauren translation
|
|
|
|
|
|
def transfer_Lauren_Translation(self):
|
|
|
|
|
|
|
|
|
|
|
|
df_lauren = self.extract_Lauren_Translation()
|
|
|
|
|
|
|
|
|
|
|
|
# Distinct list of XMLs file
|
|
|
|
|
|
xml_files = list(set(df_lauren['File'].tolist()))
|
|
|
|
|
|
|
|
|
|
|
|
for file in xml_files:
|
|
|
|
|
|
cond = df_lauren['File'] == file
|
|
|
|
|
|
lauren_translations = dict(df_lauren[cond][['JapaneseText', 'EnglishText']].values)
|
|
|
|
|
|
file_path = self.story_XML_new + 'XML/' + file
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(file_path):
|
|
|
|
|
|
tree = etree.parse(file_path)
|
|
|
|
|
|
root = tree.getroot()
|
|
|
|
|
|
need_save = False
|
|
|
|
|
|
|
|
|
|
|
|
for key, item in lauren_translations.items():
|
|
|
|
|
|
|
|
|
|
|
|
for entry_node in root.iter("Entry"):
|
|
|
|
|
|
xml_jap = entry_node.find("JapaneseText").text or ''
|
|
|
|
|
|
xml_eng = entry_node.find("EnglishText").text or ''
|
|
|
|
|
|
xml_jap_cleaned = self.clean_text(xml_jap)
|
|
|
|
|
|
|
|
|
|
|
|
if key == xml_jap_cleaned:
|
|
|
|
|
|
item = self.add_line_break(item)
|
|
|
|
|
|
|
|
|
|
|
|
if xml_eng != item:
|
|
|
|
|
|
entry_node.find("EnglishText").text = item
|
|
|
|
|
|
need_save = True
|
|
|
|
|
|
|
|
|
|
|
|
if entry_node.find("Status").text == "To Do":
|
|
|
|
|
|
entry_node.find("Status").text = "Editing"
|
|
|
|
|
|
|
|
|
|
|
|
# else:
|
|
|
|
|
|
# print("File: {} - {}".format(file, key))
|
|
|
|
|
|
|
|
|
|
|
|
if need_save:
|
|
|
|
|
|
txt = etree.tostring(root, encoding="UTF-8", pretty_print=True, xml_declaration=False)
|
|
|
|
|
|
|
|
|
|
|
|
with open(file_path, 'wb') as xml_file:
|
|
|
|
|
|
xml_file.write(txt)
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("File {} skipped because file is not found".format(file))
|
|
|
|
|
|
|
2022-01-23 08:25:40 -05:00
|
|
|
|
# Extract the story files
|
2023-05-19 07:55:15 -05:00
|
|
|
|
def extract_all_story(self, replace=False) -> None:
|
|
|
|
|
|
print("Extracting Story files...")
|
2022-12-21 19:10:18 -05:00
|
|
|
|
|
2023-05-19 07:55:15 -05:00
|
|
|
|
# TODO: use pathlib for everything
|
|
|
|
|
|
folder_path = Path(self.story_XML_patch) / "XML"
|
|
|
|
|
|
scpk_path = Path(self.dat_archive_extract) / "SCPK"
|
2023-05-19 10:14:55 -05:00
|
|
|
|
|
|
|
|
|
|
for file in tqdm(list(scpk_path.glob("*.scpk"))):
|
2023-05-19 07:55:15 -05:00
|
|
|
|
theirsce = Theirsce(Scpk(file).rsce)
|
|
|
|
|
|
xml_text = self.get_xml_from_theirsce(theirsce, "Story")
|
2022-07-04 16:10:43 -04:00
|
|
|
|
self.id = 1
|
2023-05-19 07:55:15 -05:00
|
|
|
|
|
|
|
|
|
|
with open(folder_path / file.with_suffix(".xml").name, "wb") as xml:
|
|
|
|
|
|
xml.write(xml_text)
|
|
|
|
|
|
|
2022-08-13 00:13:24 -04:00
|
|
|
|
|
|
|
|
|
|
# Extract all the skits files
|
2023-05-19 08:23:47 -05:00
|
|
|
|
def extract_all_skits(self, replace=False) -> None:
|
2023-05-19 07:55:15 -05:00
|
|
|
|
print("Extracting Skit files...")
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: use pathlib for everything
|
|
|
|
|
|
folder_path = Path(self.skit_XML_patch) / "XML"
|
|
|
|
|
|
pak2_path = Path(self.dat_archive_extract) / "PAK2"
|
|
|
|
|
|
|
2023-05-19 10:14:55 -05:00
|
|
|
|
for file in tqdm(list(pak2_path.glob("*.pak2"))):
|
2023-05-19 07:55:15 -05:00
|
|
|
|
with open(file, "rb") as pak:
|
|
|
|
|
|
theirsce = pak2lib.get_theirsce_from_pak2(pak.read())
|
|
|
|
|
|
|
|
|
|
|
|
xml_text = self.get_xml_from_theirsce(Theirsce(theirsce), "Skits")
|
|
|
|
|
|
|
|
|
|
|
|
with open(folder_path / file.with_suffix(".xml").name, "wb") as xml:
|
|
|
|
|
|
xml.write(xml_text)
|
2023-05-14 16:50:41 -04:00
|
|
|
|
|
|
|
|
|
|
|
2022-01-23 08:25:40 -05:00
|
|
|
|
# Extract THEIRSCE to XML
|
2023-05-19 07:55:15 -05:00
|
|
|
|
def get_xml_from_theirsce(self, rsce: Theirsce, section: str) -> bytes:
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
|
|
|
|
|
#Create the XML file
|
2023-01-06 22:45:40 -05:00
|
|
|
|
# root = etree.Element('SceneText')
|
|
|
|
|
|
# etree.SubElement(root, "OriginalName").text = file_name
|
2023-01-06 22:39:17 -05:00
|
|
|
|
|
2023-01-06 22:45:40 -05:00
|
|
|
|
#pointers_offset, texts_offset = self.extract_Story_Pointers(rsce)
|
|
|
|
|
|
names, lines = self.extract_lines_with_speaker(rsce)
|
|
|
|
|
|
|
|
|
|
|
|
for i, (k, v) in enumerate(names.items(), -1):
|
|
|
|
|
|
names[k] = NameEntry(i, v)
|
2022-02-11 21:16:19 -05:00
|
|
|
|
|
|
|
|
|
|
#Remove duplicates
|
2022-07-04 16:10:43 -04:00
|
|
|
|
#list_informations = self.remove_duplicates(["Story"] * len(pointers_offset), pointers_offset, text_list)
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
2023-01-06 22:45:40 -05:00
|
|
|
|
# list_lines = ( ['Story', line.offset, line.text] for line in lines)
|
|
|
|
|
|
# list_names = ( ['Story', line.offset, line.text] for i, (k, v) in enumerate(found_names.items()))
|
2022-08-12 21:49:23 -04:00
|
|
|
|
#Build the XML Structure with the information
|
2023-01-06 22:45:40 -05:00
|
|
|
|
|
|
|
|
|
|
root = etree.Element("SceneText")
|
|
|
|
|
|
speakers_node = etree.SubElement(root, 'Speakers')
|
2023-01-07 22:00:46 -05:00
|
|
|
|
etree.SubElement(speakers_node, 'Section').text = "Speaker"
|
2023-01-06 22:45:40 -05:00
|
|
|
|
strings_node = etree.SubElement(root, 'Strings')
|
|
|
|
|
|
etree.SubElement(strings_node, 'Section').text = section
|
|
|
|
|
|
|
|
|
|
|
|
self.make_speakers_section(speakers_node, names)
|
|
|
|
|
|
self.make_strings_section(strings_node, lines, names)
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
2023-05-19 07:55:15 -05:00
|
|
|
|
# Return XML string
|
|
|
|
|
|
return etree.tostring(root, encoding="UTF-8", pretty_print=True)
|
2022-12-21 19:10:18 -05:00
|
|
|
|
|
2023-01-06 22:45:40 -05:00
|
|
|
|
|
|
|
|
|
|
def make_strings_section(self, root, lines: list[LineEntry], names: dict[str, NameEntry]):
|
|
|
|
|
|
pass
|
|
|
|
|
|
for line in lines:
|
|
|
|
|
|
entry_node = etree.SubElement(root, "Entry")
|
|
|
|
|
|
etree.SubElement(entry_node,"PointerOffset").text = str(line.offset)
|
|
|
|
|
|
text_split = list(filter(None, re.split(self.COMMON_TAG, line.text)))
|
|
|
|
|
|
|
|
|
|
|
|
if len(text_split) > 1 and text_split[0].startswith("<voice:"):
|
|
|
|
|
|
etree.SubElement(entry_node,"VoiceId").text = text_split[0][1:-1].split(":")[1]
|
|
|
|
|
|
etree.SubElement(entry_node, "JapaneseText").text = ''.join(text_split[1:])
|
|
|
|
|
|
else:
|
|
|
|
|
|
etree.SubElement(entry_node, "JapaneseText").text = line.text
|
|
|
|
|
|
|
|
|
|
|
|
etree.SubElement(entry_node,"EnglishText")
|
|
|
|
|
|
etree.SubElement(entry_node,"Notes")
|
2022-12-21 19:10:18 -05:00
|
|
|
|
|
2023-01-06 22:45:40 -05:00
|
|
|
|
if line.names:
|
|
|
|
|
|
etree.SubElement(entry_node,"SpeakerId").text = ','.join([str(names[n].index) for n in line.names])
|
|
|
|
|
|
etree.SubElement(entry_node,"Id").text = str(self.id)
|
|
|
|
|
|
|
|
|
|
|
|
self.id = self.id + 1
|
|
|
|
|
|
|
|
|
|
|
|
if line.text == '':
|
|
|
|
|
|
statusText = 'Done'
|
|
|
|
|
|
else:
|
|
|
|
|
|
statusText = 'To Do'
|
|
|
|
|
|
etree.SubElement(entry_node,"Status").text = statusText
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_speakers_section(self, root, names: dict[str, NameEntry]):
|
|
|
|
|
|
for k, v in names.items():
|
|
|
|
|
|
entry_node = etree.SubElement(root, "Entry")
|
2023-01-06 23:16:52 -05:00
|
|
|
|
if v.offsets:
|
|
|
|
|
|
etree.SubElement(entry_node,"PointerOffset").text = ",".join([str(off) for off in v.offsets])
|
|
|
|
|
|
else:
|
|
|
|
|
|
etree.SubElement(entry_node,"PointerOffset")
|
2023-01-06 22:45:40 -05:00
|
|
|
|
etree.SubElement(entry_node,"JapaneseText").text = str(k)
|
|
|
|
|
|
etree.SubElement(entry_node,"EnglishText")
|
|
|
|
|
|
etree.SubElement(entry_node,"Notes")
|
|
|
|
|
|
etree.SubElement(entry_node,"Id").text = str(v.index)
|
|
|
|
|
|
etree.SubElement(entry_node,"Status").text = "To Do"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_lines_with_speaker(self, theirsce: Theirsce):
|
|
|
|
|
|
# This will do a bit of everything thanks to the "nice"
|
|
|
|
|
|
# architecture of the Theirsce class :)
|
|
|
|
|
|
|
|
|
|
|
|
# Debug
|
|
|
|
|
|
# sections = []
|
|
|
|
|
|
# for _, section in enumerate(theirsce.sections):
|
|
|
|
|
|
# for _, sub in enumerate(section):
|
|
|
|
|
|
# sections.append(sub.off)
|
|
|
|
|
|
|
|
|
|
|
|
# Setup three-way opcode generator
|
|
|
|
|
|
d = TheirsceBaseInstruction(); d.type = InstructionType.INVALID
|
|
|
|
|
|
a,b,c = tee(theirsce.walk_code(), 3)
|
|
|
|
|
|
next(a, d)
|
|
|
|
|
|
next(b, d); next(b, d)
|
|
|
|
|
|
next(c, d); next(c, d); next(c, d)
|
|
|
|
|
|
|
|
|
|
|
|
# Helper function, in the future I'll
|
|
|
|
|
|
# just use a list of opcodes
|
|
|
|
|
|
def skip():
|
|
|
|
|
|
next(a, d); next(a, d)
|
|
|
|
|
|
next(b, d); next(b, d)
|
|
|
|
|
|
next(c, d); next(c, d)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
names = {VARIABLE_NAME: []}
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
params = []
|
|
|
|
|
|
used = False
|
|
|
|
|
|
for op1, op2, op3 in zip(a,b,c):
|
|
|
|
|
|
# Debug
|
|
|
|
|
|
# if theirsce.tell() in sections:
|
|
|
|
|
|
# print()
|
|
|
|
|
|
# print("SECTION: ")
|
|
|
|
|
|
|
|
|
|
|
|
# BREAK marks start of a local function
|
|
|
|
|
|
# so local params are no longer in scope
|
|
|
|
|
|
if op1.type is InstructionType.BREAK:
|
|
|
|
|
|
if used == False:
|
|
|
|
|
|
for param in params:
|
|
|
|
|
|
text = self.bytes_to_text(theirsce, param.offset + theirsce.strings_offset)
|
|
|
|
|
|
lines.append(LineEntry([], text, op1.position + 1))
|
|
|
|
|
|
params.clear()
|
|
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# This sequence mark the simple act of assigning
|
|
|
|
|
|
# a string to a local variable, so we can detect
|
|
|
|
|
|
# when they are used later in a function call
|
|
|
|
|
|
if (op1.type is InstructionType.REFERENCE
|
|
|
|
|
|
and op2.type is InstructionType.STRING
|
|
|
|
|
|
and op3.type is InstructionType.ALU
|
|
|
|
|
|
and op3.operation == AluOperation.ASSIGNMENT
|
|
|
|
|
|
):
|
|
|
|
|
|
params.append(op2)
|
|
|
|
|
|
skip()
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# This sequence represents the textbox call with
|
|
|
|
|
|
# the name being a variable (NPCs do this)
|
|
|
|
|
|
if (op1.type is InstructionType.REFERENCE
|
|
|
|
|
|
and op2.type is InstructionType.STRING
|
|
|
|
|
|
and op3.type is InstructionType.SYSCALL
|
|
|
|
|
|
and op3.function_index == 0x45
|
|
|
|
|
|
):
|
|
|
|
|
|
if len(params) >= 1:
|
|
|
|
|
|
name = [self.bytes_to_text(theirsce, p.offset + theirsce.strings_offset) for p in params]
|
|
|
|
|
|
[names.setdefault(n, []).append(p.position + 1) for n, p in zip(name, params)]
|
|
|
|
|
|
elif len(params) == 0:
|
|
|
|
|
|
name = []
|
|
|
|
|
|
text = self.bytes_to_text(theirsce, op2.offset + theirsce.strings_offset)
|
|
|
|
|
|
lines.append(LineEntry(name, text, op2.position + 1))
|
|
|
|
|
|
#print(f"{params}: {text}")
|
|
|
|
|
|
used = True
|
|
|
|
|
|
skip()
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# This sequence represents the textbox call with
|
|
|
|
|
|
# the text being a variable (Notice boxes do this)
|
|
|
|
|
|
if (op1.type is InstructionType.STRING
|
|
|
|
|
|
and op2.type is InstructionType.REFERENCE
|
|
|
|
|
|
and op3.type is InstructionType.SYSCALL
|
|
|
|
|
|
and op3.function_index == 0x45
|
|
|
|
|
|
):
|
|
|
|
|
|
name = [self.bytes_to_text(theirsce, op1.offset + theirsce.strings_offset)]
|
|
|
|
|
|
names.setdefault(name[0], []).append(op1.position + 1)
|
|
|
|
|
|
for param in params:
|
|
|
|
|
|
text = self.bytes_to_text(theirsce, param.offset + theirsce.strings_offset)
|
|
|
|
|
|
lines.append(LineEntry(name, text, param.position + 1))
|
|
|
|
|
|
#print(f"{text}: {name}")
|
|
|
|
|
|
used = True
|
|
|
|
|
|
params.clear()
|
|
|
|
|
|
skip()
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# This sequence represents a regular textbox call
|
|
|
|
|
|
# where both fields are an string (everything else, save for skits)
|
|
|
|
|
|
if (op1.type is InstructionType.STRING
|
|
|
|
|
|
and op2.type is InstructionType.STRING
|
|
|
|
|
|
and op3.type is InstructionType.SYSCALL
|
|
|
|
|
|
and op3.function_index == 0x45
|
|
|
|
|
|
):
|
|
|
|
|
|
name = [self.bytes_to_text(theirsce, op1.offset + theirsce.strings_offset)]
|
|
|
|
|
|
names.setdefault(name[0], []).append(op1.position + 1)
|
|
|
|
|
|
text = self.bytes_to_text(theirsce, op2.offset + theirsce.strings_offset)
|
|
|
|
|
|
lines.append(LineEntry(name, text, op2.position + 1))
|
|
|
|
|
|
#print(f"{name}: {text}")
|
|
|
|
|
|
skip()
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Any other string in assorted code calls
|
|
|
|
|
|
if op1.type is InstructionType.STRING:
|
|
|
|
|
|
#print(theirsce.read_string_at(op1.offset + theirsce.strings_offset))
|
|
|
|
|
|
text = self.bytes_to_text(theirsce, op1.offset + theirsce.strings_offset)
|
|
|
|
|
|
lines.append(LineEntry([], text, op1.position + 1))
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
return names, lines
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_story_pointers_plain(self, theirsce: Theirsce):
|
2023-01-06 22:39:17 -05:00
|
|
|
|
pointers_offset = []; texts_offset = []
|
|
|
|
|
|
|
|
|
|
|
|
for opcode in theirsce.walk_code():
|
2023-01-06 22:45:40 -05:00
|
|
|
|
if opcode.type == self.string_opcode:
|
2023-01-06 22:39:17 -05:00
|
|
|
|
pointers_offset.append(theirsce.tell() - 2) # Maybe check this later
|
|
|
|
|
|
texts_offset.append(opcode.offset + theirsce.strings_offset)
|
|
|
|
|
|
|
|
|
|
|
|
return pointers_offset, texts_offset
|
|
|
|
|
|
|
2022-07-16 15:11:15 -04:00
|
|
|
|
#Convert a bytes object to text using TAGS and TBL in the json file
|
2023-01-06 22:45:40 -05:00
|
|
|
|
def bytes_to_text(self, theirsce: Theirsce, offset=-1, end_strings = b"\x00"):
|
2023-05-16 00:05:37 -05:00
|
|
|
|
finalText = ""
|
|
|
|
|
|
tags = self.jsonTblTags['TAGS']
|
|
|
|
|
|
chars = self.jsonTblTags['TBL']
|
|
|
|
|
|
|
2022-07-16 15:11:15 -04:00
|
|
|
|
if (offset > 0):
|
2023-01-06 22:45:40 -05:00
|
|
|
|
theirsce.seek(offset, 0)
|
2022-07-16 15:11:15 -04:00
|
|
|
|
|
2023-05-16 00:05:37 -05:00
|
|
|
|
b = theirsce.read(1)
|
|
|
|
|
|
while True:
|
|
|
|
|
|
b = theirsce.read(1)
|
|
|
|
|
|
if b == end_strings: break
|
|
|
|
|
|
|
|
|
|
|
|
b = ord(b)
|
|
|
|
|
|
# Custom Encoded Text
|
|
|
|
|
|
if (0x99 <= b <= 0x9F) or (0xE0 <= b <= 0xEB):
|
|
|
|
|
|
c = (b << 8) | theirsce.read_uint8()
|
|
|
|
|
|
finalText += chars.get(c, "{%02X}{%02X}" % (c >> 8, c & 0xFF))
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if b == 0x1:
|
|
|
|
|
|
finalText += ("\n")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# ASCII text
|
|
|
|
|
|
if chr(b) in self.PRINTABLE_CHARS:
|
|
|
|
|
|
finalText += chr(b)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# cp932 text
|
|
|
|
|
|
if 0xA0 < b < 0xE0:
|
|
|
|
|
|
finalText += struct.pack("B", b).decode("cp932")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if b == 0x81:
|
2023-01-06 22:45:40 -05:00
|
|
|
|
next_b = theirsce.read(1)
|
2022-07-16 15:11:15 -04:00
|
|
|
|
if next_b == b"\x40":
|
|
|
|
|
|
finalText += " "
|
|
|
|
|
|
else:
|
|
|
|
|
|
finalText += "{%02X}" % b
|
|
|
|
|
|
finalText += "{%02X}" % ord(next_b)
|
2023-05-16 00:05:37 -05:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Simple Tags
|
|
|
|
|
|
if 0x3 <= b <= 0xF:
|
|
|
|
|
|
parameter = theirsce.read_uint32()
|
|
|
|
|
|
|
|
|
|
|
|
tag_name = tags.get(b, f"{b:02X}")
|
|
|
|
|
|
tag_param = self.jsonTblTags.get(tag_name.upper(), {}).get(parameter, None)
|
|
|
|
|
|
|
|
|
|
|
|
if tag_param is not None:
|
|
|
|
|
|
finalText += tag_param
|
|
|
|
|
|
else:
|
|
|
|
|
|
finalText += f"<{tag_name}:{self.hex2(parameter)}>"
|
|
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Variable tags (same as above but using rsce bytecode as parameter)
|
|
|
|
|
|
if 0x13 <= b <= 0x1A:
|
|
|
|
|
|
tag_name = f"unk{b:02X}"
|
|
|
|
|
|
parameter = "".join([f"{c:02X}" for c in theirsce.read_tag_bytes()])
|
|
|
|
|
|
|
|
|
|
|
|
finalText += f"<{tag_name}:{parameter}>"
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# None of the above
|
|
|
|
|
|
finalText += "{%02X}" % b
|
2022-07-16 15:11:15 -04:00
|
|
|
|
|
2023-01-06 22:45:40 -05:00
|
|
|
|
return finalText
|
2022-07-16 15:11:15 -04:00
|
|
|
|
|
2022-06-28 21:41:25 -04:00
|
|
|
|
def get_Node_Bytes(self, entry_node):
|
2022-06-28 21:39:01 -04:00
|
|
|
|
|
|
|
|
|
|
#Grab the fields from the Entry in the XML
|
|
|
|
|
|
status = entry_node.find("Status").text
|
|
|
|
|
|
japanese_text = entry_node.find("JapaneseText").text
|
|
|
|
|
|
english_text = entry_node.find("EnglishText").text
|
|
|
|
|
|
|
|
|
|
|
|
#Use the values only for Status = Done and use English if non empty
|
|
|
|
|
|
final_text = ''
|
2022-06-28 21:41:25 -04:00
|
|
|
|
if (status in self.list_status_insertion):
|
2022-06-28 21:39:01 -04:00
|
|
|
|
final_text = english_text or japanese_text or ''
|
|
|
|
|
|
else:
|
|
|
|
|
|
final_text = japanese_text or ''
|
2022-10-22 21:18:01 -04:00
|
|
|
|
|
|
|
|
|
|
voiceId_node = entry_node.find("VoiceId")
|
|
|
|
|
|
if (voiceId_node != None):
|
2023-05-14 16:50:41 -04:00
|
|
|
|
final_text = '<voice:{}>'.format(voiceId_node.text) + final_text
|
2022-08-15 12:56:58 -04:00
|
|
|
|
|
2022-06-28 21:39:01 -04:00
|
|
|
|
#Convert the text values to bytes using TBL, TAGS, COLORS, ...
|
|
|
|
|
|
bytes_entry = self.text_to_bytes(final_text)
|
|
|
|
|
|
|
|
|
|
|
|
return bytes_entry
|
|
|
|
|
|
|
2022-08-15 12:56:58 -04:00
|
|
|
|
def get_New_Theirsce(self, theirsce, scpk_file_name, destination):
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
|
|
|
|
|
#To store the new text_offset and pointers to update
|
|
|
|
|
|
new_text_offsets = dict()
|
|
|
|
|
|
|
|
|
|
|
|
#Grab strings_offset for pointers
|
|
|
|
|
|
theirsce.read(12)
|
|
|
|
|
|
strings_offset = struct.unpack("<L", theirsce.read(4))[0]
|
|
|
|
|
|
|
|
|
|
|
|
#Read the XML for the corresponding THEIRSCE
|
2022-08-15 12:56:58 -04:00
|
|
|
|
file = destination +"XML/"+ self.get_file_name(scpk_file_name)+'.xml'
|
2022-07-04 16:10:43 -04:00
|
|
|
|
#print("XML : {}".format(self.get_file_name(scpk_file_name)+'.xml'))
|
|
|
|
|
|
|
2022-01-30 19:19:43 -05:00
|
|
|
|
tree = etree.parse(file)
|
|
|
|
|
|
root = tree.getroot()
|
2023-05-14 16:50:41 -04:00
|
|
|
|
|
2022-06-28 21:47:33 -04:00
|
|
|
|
#Go at the start of the dialog
|
|
|
|
|
|
#Loop on every Entry and reinsert
|
2022-01-30 19:19:43 -05:00
|
|
|
|
theirsce.seek(strings_offset+1)
|
2023-05-14 16:50:41 -04:00
|
|
|
|
nodes = [ele for ele in root.iter('Entry') if ele.find('Id').text != "-1"]
|
|
|
|
|
|
nodes = [ele for ele in nodes if ele.find('PointerOffset').text != "-1"]
|
|
|
|
|
|
|
|
|
|
|
|
for entry_node in nodes:
|
|
|
|
|
|
|
2022-01-30 19:19:43 -05:00
|
|
|
|
#Add the PointerOffset and TextOffset
|
|
|
|
|
|
new_text_offsets[entry_node.find("PointerOffset").text] = theirsce.tell()
|
2022-06-28 21:43:07 -04:00
|
|
|
|
#Use the node to get the new bytes
|
|
|
|
|
|
bytes_entry = self.get_Node_Bytes(entry_node)
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
|
|
|
|
|
#Write to the file
|
2022-06-28 21:43:07 -04:00
|
|
|
|
theirsce.write(bytes_entry + b'\x00')
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-06-28 21:47:33 -04:00
|
|
|
|
#Update the pointers based on the new text_offset of the entries
|
2022-01-30 19:19:43 -05:00
|
|
|
|
for pointer_offset, text_offset in new_text_offsets.items():
|
|
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
pointers_list = pointer_offset.split(",")
|
|
|
|
|
|
new_value = text_offset - strings_offset
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for pointer in pointers_list:
|
|
|
|
|
|
|
|
|
|
|
|
theirsce.seek(int(pointer))
|
2022-06-28 21:31:01 -04:00
|
|
|
|
theirsce.write( struct.pack("<H", new_value))
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
|
|
|
|
|
return theirsce
|
|
|
|
|
|
|
|
|
|
|
|
#Repack SCPK files for Story
|
2022-06-28 21:51:09 -04:00
|
|
|
|
def pack_Story_File(self, scpk_file_name):
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
|
|
|
|
|
#Copy the original SCPK file to the folder used for the new version
|
2022-06-28 21:51:09 -04:00
|
|
|
|
shutil.copy( self.dat_archive_extract + "SCPK/" + scpk_file_name, self.story_XML_patch + "New/" + scpk_file_name)
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
#Open the original SCPK
|
2022-06-28 21:51:09 -04:00
|
|
|
|
with open( self.dat_archive_extract + "SCPK/" + scpk_file_name, 'r+b') as scpk:
|
2022-01-30 19:19:43 -05:00
|
|
|
|
#Get nb_files and files_size
|
|
|
|
|
|
scpk.read(4)
|
|
|
|
|
|
scpk.read(4)
|
|
|
|
|
|
nb_files = struct.unpack("<L", scpk.read(4))[0]
|
|
|
|
|
|
scpk.read(4)
|
|
|
|
|
|
file_size_dict = dict()
|
|
|
|
|
|
for i in range(nb_files):
|
|
|
|
|
|
pointer_offset = scpk.tell()
|
2022-02-18 15:42:52 -05:00
|
|
|
|
file_size = struct.unpack("<L", scpk.read(4))[0]
|
|
|
|
|
|
file_size_dict[pointer_offset] = file_size
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
|
|
|
|
|
#Extract each files and append to the final data_final
|
2022-02-18 15:42:52 -05:00
|
|
|
|
dataFinal = bytearray()
|
|
|
|
|
|
sizes = []
|
|
|
|
|
|
o = io.BytesIO()
|
|
|
|
|
|
|
|
|
|
|
|
i=0
|
|
|
|
|
|
for pointer_offset, fsize in file_size_dict.items():
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
data_compressed = scpk.read(fsize)
|
2023-05-19 08:07:41 -05:00
|
|
|
|
if comptolib.is_compressed(data_compressed):
|
2022-02-18 15:42:52 -05:00
|
|
|
|
c_type = struct.unpack("<b", data_compressed[:1])[0]
|
|
|
|
|
|
#print("File {} size: {} ctype: {}".format(i, fsize,c_type))
|
|
|
|
|
|
data_uncompressed = comptolib.decompress_data(data_compressed)
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
|
|
|
|
|
if data_uncompressed[:8] == b"THEIRSCE":
|
|
|
|
|
|
|
|
|
|
|
|
#Only for debug to have the original THEIRSCE
|
|
|
|
|
|
#with open("test_original_comp.theirsce", "wb") as f:
|
|
|
|
|
|
# print("Size original: {}".format(len(data_uncompressed)))
|
|
|
|
|
|
# f.write(data)
|
|
|
|
|
|
#with open("test_original.theirsce", "wb") as f:
|
|
|
|
|
|
# f.write(data_uncompressed)
|
|
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
#Update THEIRSCE uncompressed file
|
2023-05-14 16:50:41 -04:00
|
|
|
|
theirsce = self.get_New_Theirsce(io.BytesIO(data_uncompressed), scpk_file_name, self.story_XML_new)
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
2022-01-30 19:19:43 -05:00
|
|
|
|
theirsce.seek(0)
|
|
|
|
|
|
data_new_uncompressed = theirsce.read()
|
|
|
|
|
|
data_compressed = comptolib.compress_data(data_new_uncompressed, version=c_type)
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
data_compressed = comptolib.compress_data(data_uncompressed, version=c_type)
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
#Updating the header of the SCPK file to adjust the size
|
|
|
|
|
|
new_size = len(data_compressed)
|
|
|
|
|
|
#print("File recomp {} size: {} ctype: {}".format(i, new_size,c_type))
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
|
|
|
|
|
dataFinal += data_compressed
|
2022-02-18 15:42:52 -05:00
|
|
|
|
sizes.append(new_size)
|
|
|
|
|
|
i=i+1
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
|
|
|
|
|
#Write down the new SCPK from scratch
|
|
|
|
|
|
o.write(b"\x53\x43\x50\x4B\x01\x00\x0F\x00")
|
|
|
|
|
|
o.write(struct.pack("<L", len(sizes)))
|
|
|
|
|
|
o.write(b"\x00" * 4)
|
2023-05-14 16:50:41 -04:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
for i in range(len(sizes)):
|
|
|
|
|
|
o.write(struct.pack("<L", sizes[i]))
|
|
|
|
|
|
|
|
|
|
|
|
o.write(dataFinal)
|
|
|
|
|
|
|
2023-05-14 16:50:41 -04:00
|
|
|
|
with open(self.story_XML_patch + "New/" + scpk_file_name, "wb") as f:
|
|
|
|
|
|
f.write(o.getvalue())
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
|
|
|
|
|
return o.getvalue()
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-08-15 12:55:30 -04:00
|
|
|
|
def pack_Skit_File(self, pak2_file):
|
2023-05-14 16:50:41 -04:00
|
|
|
|
|
|
|
|
|
|
# Copy the original PAK2 file to the folder used for the new version
|
|
|
|
|
|
shutil.copy(self.dat_archive_extract + "PAK2/" + pak2_file, self.skit_XML_patch + "New/" + pak2_file)
|
|
|
|
|
|
|
2022-08-15 12:55:30 -04:00
|
|
|
|
pak2_file_path = os.path.join(self.dat_archive_extract, "PAK2", pak2_file)
|
|
|
|
|
|
with open(pak2_file_path,"rb") as f_pak2:
|
|
|
|
|
|
pak2_data = f_pak2.read()
|
|
|
|
|
|
|
|
|
|
|
|
#Create the pak2 object
|
|
|
|
|
|
pak2_obj = pak2lib.get_data(pak2_data)
|
|
|
|
|
|
|
|
|
|
|
|
#Generate the new Theirsce based on the XML and replace the original one
|
2023-05-14 16:50:41 -04:00
|
|
|
|
theirsce_io = self.get_New_Theirsce(io.BytesIO(pak2_obj.chunks.theirsce), os.path.basename(pak2_file_path).split(".")[0], self.skit_XML_new)
|
2022-08-15 12:55:30 -04:00
|
|
|
|
theirsce_io.seek(0)
|
|
|
|
|
|
new_data = theirsce_io.read()
|
|
|
|
|
|
pak2_obj.chunks.theirsce = new_data
|
|
|
|
|
|
|
|
|
|
|
|
self.mkdir(self.skit_XML_patch+ "New")
|
2023-05-14 16:50:41 -04:00
|
|
|
|
with open(self.skit_XML_patch+ "New/" + pak2_file, "wb") as f2:
|
|
|
|
|
|
f2.write(pak2lib.create_pak2(pak2_obj))
|
2022-08-15 12:55:30 -04:00
|
|
|
|
|
2023-05-19 08:07:41 -05:00
|
|
|
|
return
|
2022-12-21 19:10:18 -05:00
|
|
|
|
|
|
|
|
|
|
def pack_All_Skits(self):
|
|
|
|
|
|
|
|
|
|
|
|
print("Recreating Skits files")
|
|
|
|
|
|
listFiles = [ele for ele in os.listdir(self.skit_XML_patch + "New/")]
|
|
|
|
|
|
for pak2_file in listFiles:
|
|
|
|
|
|
self.pack_Skit_File(pak2_file)
|
|
|
|
|
|
print("Writing file {} ...".format(pak2_file))
|
|
|
|
|
|
|
|
|
|
|
|
def debug_Story_Skits(self, section, file_name, text=False):
|
|
|
|
|
|
|
|
|
|
|
|
if section == "Story":
|
|
|
|
|
|
theirsce = self.get_theirsce_from_scpk(self.dat_archive_extract + 'SCPK/' + self.get_file_name(file_name) + '.scpk')
|
|
|
|
|
|
else:
|
|
|
|
|
|
with open(self.dat_archive_extract + "PAK2/" + file_name.split(".")[0] + '.3.pak2', "rb") as pak:
|
|
|
|
|
|
data = pak.read()
|
|
|
|
|
|
theirsce = io.BytesIO(pak2lib.get_theirsce_from_pak2(data))
|
|
|
|
|
|
|
2023-01-07 22:00:46 -05:00
|
|
|
|
rsce = Theirsce(path=theirsce)
|
|
|
|
|
|
# pointers_offset, texts_offset = self.extract_Story_Pointers(rsce)
|
|
|
|
|
|
names, lines = self.extract_lines_with_speaker(rsce)
|
|
|
|
|
|
|
|
|
|
|
|
for i, (k, v) in enumerate(names.items(), -1):
|
|
|
|
|
|
names[k] = NameEntry(i, v)
|
|
|
|
|
|
|
2022-12-21 19:10:18 -05:00
|
|
|
|
with open('../{}.theirsce'.format(file_name), 'wb') as f:
|
|
|
|
|
|
f.write(theirsce.getvalue())
|
|
|
|
|
|
|
|
|
|
|
|
text_list = []
|
|
|
|
|
|
if text:
|
2023-01-07 22:00:46 -05:00
|
|
|
|
text_list = [line.text for line in lines]
|
2022-12-21 19:10:18 -05:00
|
|
|
|
|
2023-01-07 22:00:46 -05:00
|
|
|
|
df = pd.DataFrame({"Jap_Text": text_list})
|
2022-12-21 19:10:18 -05:00
|
|
|
|
df['Text_Offset'] = df['Text_Offset'].apply(lambda x: hex(x)[2:])
|
|
|
|
|
|
df['Pointers_Offset'] = df['Pointers_Offset'].apply(lambda x: hex(x)[2:])
|
|
|
|
|
|
df.to_excel('../{}.xlsx'.format(self.get_file_name(file_name)), index=False)
|
|
|
|
|
|
|
2023-05-19 07:45:20 -05:00
|
|
|
|
|
2023-05-19 07:48:01 -05:00
|
|
|
|
def get_datbin_file_data(self) -> dict[int, int]:
|
2023-05-19 07:45:20 -05:00
|
|
|
|
|
|
|
|
|
|
with open(self.elf_original , "rb") as elf:
|
|
|
|
|
|
elf.seek(self.POINTERS_BEGIN, 0)
|
|
|
|
|
|
blob = elf.read(self.POINTERS_END-self.POINTERS_BEGIN)
|
|
|
|
|
|
|
2023-05-19 07:48:01 -05:00
|
|
|
|
pointers = struct.unpack(f"<{len(blob)//4}L", blob)
|
|
|
|
|
|
file_data: dict[int, int] = {}
|
|
|
|
|
|
for c, n in zip(pointers, pointers[1:]):
|
|
|
|
|
|
remainder = c & self.LOW_BITS
|
|
|
|
|
|
start = c & self.HIGH_BITS
|
|
|
|
|
|
end = (n & self.HIGH_BITS) - remainder
|
2023-05-19 10:15:42 -05:00
|
|
|
|
file_data[start] = end - start
|
2022-01-23 08:25:40 -05:00
|
|
|
|
|
2023-05-19 07:48:01 -05:00
|
|
|
|
return file_data
|
|
|
|
|
|
|
|
|
|
|
|
# Extract the file DAT.BIN to the different directorties
|
|
|
|
|
|
def extract_main_archive(self) -> None:
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
2023-05-19 10:15:42 -05:00
|
|
|
|
print("Cleaning extract folder...")
|
|
|
|
|
|
for path in Path(self.dat_archive_extract).glob("**/*"):
|
|
|
|
|
|
if path.is_file():
|
|
|
|
|
|
path.unlink()
|
|
|
|
|
|
elif path.is_dir():
|
|
|
|
|
|
shutil.rmtree(path)
|
|
|
|
|
|
|
|
|
|
|
|
print("Extracting DAT.BIN files...")
|
2023-05-19 07:48:01 -05:00
|
|
|
|
with open( self.dat_bin_original, "rb") as f:
|
|
|
|
|
|
for i, (offset, size) in enumerate(tqdm(self.get_datbin_file_data().items(), desc="Extracting files", unit="file")):
|
|
|
|
|
|
|
2022-01-23 08:25:40 -05:00
|
|
|
|
# Ignore 0 byte files
|
2023-05-19 07:48:01 -05:00
|
|
|
|
if size == 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
f.seek(offset, 0)
|
|
|
|
|
|
data = f.read(size)
|
|
|
|
|
|
|
|
|
|
|
|
if comptolib.is_compressed(data):
|
|
|
|
|
|
c_type = struct.unpack("<b", data[:1])[0]
|
|
|
|
|
|
data = comptolib.decompress_data(data)
|
|
|
|
|
|
extension = self.get_extension(data)
|
|
|
|
|
|
fname = f"{i:05d}.{c_type}.{extension}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
extension = self.get_extension(data)
|
|
|
|
|
|
fname = f"{i:05d}.{extension}"
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: use pathlib for everything
|
|
|
|
|
|
final_path = Path(self.dat_archive_extract) / extension.upper()
|
|
|
|
|
|
final_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
with open(final_path / fname, "wb") as output:
|
|
|
|
|
|
output.write(data)
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-03-20 10:10:58 -04:00
|
|
|
|
|
2023-05-19 08:23:47 -05:00
|
|
|
|
def pack_main_archive(self):
|
2022-01-30 19:19:43 -05:00
|
|
|
|
sectors = [0]
|
|
|
|
|
|
remainders = []
|
|
|
|
|
|
buffer = 0
|
2023-05-14 16:50:41 -04:00
|
|
|
|
|
|
|
|
|
|
# Copy the original SLPS to Disc/New
|
|
|
|
|
|
shutil.copy(self.elf_original, self.elf_new)
|
2022-07-04 16:10:43 -04:00
|
|
|
|
|
2022-06-28 21:49:19 -04:00
|
|
|
|
output_dat_path = self.dat_bin_new
|
2022-02-18 15:42:52 -05:00
|
|
|
|
with open(output_dat_path, "wb") as output_dat:
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
print("Packing files into %s..." % os.path.basename(output_dat_path))
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
#Make a list with all the files of DAT.bin
|
|
|
|
|
|
file_list = []
|
2022-06-27 19:25:41 -04:00
|
|
|
|
for path, subdir, filenames in os.walk(self.dat_archive_extract):
|
2022-02-18 15:42:52 -05:00
|
|
|
|
if len(filenames) > 0:
|
|
|
|
|
|
file_list.extend( [os.path.join(path,file) for file in filenames])
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
|
|
|
|
|
list_test = [os.path.splitext(os.path.basename(ele))[0] for ele in file_list]
|
|
|
|
|
|
previous = -1
|
|
|
|
|
|
dummies = 0
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
2023-05-19 08:07:41 -05:00
|
|
|
|
for file in tqdm(sorted(file_list, key=self.get_file_name)):
|
2022-07-03 19:40:42 -04:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
size = 0
|
2022-01-30 19:19:43 -05:00
|
|
|
|
remainder = 0
|
2022-02-18 15:42:52 -05:00
|
|
|
|
current = int(re.search(self.VALID_FILE_NAME, file).group(1))
|
2022-07-04 16:10:43 -04:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
if current != previous + 1:
|
|
|
|
|
|
while previous < current - 1:
|
|
|
|
|
|
remainders.append(remainder)
|
|
|
|
|
|
buffer += size + remainder
|
|
|
|
|
|
sectors.append(buffer)
|
|
|
|
|
|
previous += 1
|
|
|
|
|
|
dummies += 1
|
|
|
|
|
|
file_name = self.get_file_name(file)
|
2022-07-03 19:40:42 -04:00
|
|
|
|
|
|
|
|
|
|
if ".scpk" in file:
|
2023-05-14 16:50:41 -04:00
|
|
|
|
path = os.path.join(self.story_XML_patch, 'New', '{}.scpk'.format(file_name))
|
|
|
|
|
|
print(path)
|
|
|
|
|
|
|
|
|
|
|
|
elif ".pak2" in file:
|
|
|
|
|
|
path = os.path.join(self.skit_XML_patch, 'New', '{}.pak2'.format(file_name))
|
|
|
|
|
|
print(path)
|
2022-02-18 15:42:52 -05:00
|
|
|
|
else:
|
2023-05-14 16:50:41 -04:00
|
|
|
|
path = file
|
|
|
|
|
|
|
|
|
|
|
|
with open(path, "rb") as f2:
|
|
|
|
|
|
data = f2.read()
|
2022-02-18 15:42:52 -05:00
|
|
|
|
#data = f2.read()
|
|
|
|
|
|
|
|
|
|
|
|
comp_type = re.search(self.VALID_FILE_NAME, file).group(2)
|
|
|
|
|
|
if comp_type != None:
|
|
|
|
|
|
data = comptolib.compress_data(data, version=int(comp_type))
|
|
|
|
|
|
|
|
|
|
|
|
output_dat.write(data)
|
|
|
|
|
|
size = len(data)
|
|
|
|
|
|
#print("file: {} size: {}".format(file, size))
|
|
|
|
|
|
remainder = 0x40 - (size % 0x40)
|
|
|
|
|
|
if remainder == 0x40:
|
|
|
|
|
|
remainder = 0
|
|
|
|
|
|
output_dat.write(b"\x00" * remainder)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
remainders.append(remainder)
|
|
|
|
|
|
buffer += size + remainder
|
|
|
|
|
|
sectors.append(buffer)
|
|
|
|
|
|
previous += 1
|
|
|
|
|
|
|
2022-08-05 15:22:45 -04:00
|
|
|
|
#Use the new SLPS updated and update the pointers for the SCPK
|
2022-09-24 07:33:03 -04:00
|
|
|
|
with open("../Data/{}/Disc/New/SLPS_254.50".format(self.repo_name), "r+b") as output_elf:
|
2022-02-18 15:42:52 -05:00
|
|
|
|
output_elf.seek(self.POINTERS_BEGIN)
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(len(sectors) - 1):
|
|
|
|
|
|
output_elf.write(struct.pack("<L", sectors[i] + remainders[i]))
|
2022-01-30 19:19:43 -05:00
|
|
|
|
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
2022-12-21 19:10:18 -05:00
|
|
|
|
def pack_All_Story(self):
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
|
|
|
|
|
print("Recreating Story files")
|
2022-06-27 19:55:39 -04:00
|
|
|
|
listFiles = [ele for ele in os.listdir( self.story_XML_patch + "New/")]
|
2022-06-28 21:51:09 -04:00
|
|
|
|
for scpk_file in listFiles:
|
|
|
|
|
|
self.pack_Story_File(scpk_file)
|
|
|
|
|
|
print("Writing file {} ...".format(scpk_file))
|
2022-02-18 15:42:52 -05:00
|
|
|
|
|
|
|
|
|
|
def insert_All(self):
|
|
|
|
|
|
|
|
|
|
|
|
#Updates SCPK based on XMLs data
|
|
|
|
|
|
|
2023-05-19 08:23:47 -05:00
|
|
|
|
self.pack_main_archive()
|