diff --git a/TMPAssetConverter.py b/TMPAssetConverter.py index 6fe9bf4..b08a134 100644 --- a/TMPAssetConverter.py +++ b/TMPAssetConverter.py @@ -2,14 +2,6 @@ import sys import os import struct -if len(sys.argv) < 5: - print("Usage: " + sys.argv[0] + " newAtlas.dat newMonoBehaviour.dat originalMonoBehaviour.dat outputFolder") - exit() - -if not os.path.isdir(sys.argv[4]): - print("Output folder " + sys.argv[4] + " must be a directory!") - exit() - class DataScanner: def __init__(self, data): self.offset = 0 @@ -27,103 +19,182 @@ class DataScanner: return self.data[self.offset:(self.offset + length)] def readString(self): - length = int.from_bytes(self.peek(4), byteorder='little') + length = int.from_bytes(self.peek(4), byteorder="little") length += (4 - length) % 4 return self.read(length + 4) def rest(self): return self.data[self.offset:] -with open(sys.argv[1], "rb") as atlasFile: - atlas = DataScanner(atlasFile.read()) -with open(sys.argv[2], "rb") as behaviourFile: - behaviour = DataScanner(behaviourFile.read()) +def readString(str): + length = int.from_bytes(str[0:4], byteorder="little") + return str[4:(4+length)].decode("utf-8") -# Atlas Editing -atlasOut = b"" -atlas.advance(4 * 7) -name = atlas.readString() -atlasOut += name -name = name[4:].decode('utf-8') -atlas.advance(4 * 4) -atlasOut += atlas.rest() -with open(sys.argv[4] + "/" + name + "_Texture2D.dat", "wb") as outFile: - outFile.write(atlasOut) +class FontFile: + def __init__(self, data, filename): + stringLength = int.from_bytes(data[(4 * 15):(4 * 16)], byteorder="little") + if stringLength in range(4, 26): # Assumes filenames are between 4 and 25 chars + print(f"Detected {filename} as coming from TextMesh Pro") + self.type = "TMP" + self._fromAssetCreator(data, filename) + else: + stringLength = int.from_bytes(data[(4*12):(4*13)], byteorder="little") + if stringLength == 1: + print(f"Detected {filename} as coming from a newer Higurashi game") + self.type = "Hima" + else: + print(f"Detected {filename} as coming from an older Higurashi game") + self.type = "Oni" + self._fromGame(data, filename) + self.filename = readString(self.Filename) -# MonoBehaviour editing -behaviourOut = b"" -with open(sys.argv[3], "rb") as originalFile: - original = DataScanner(originalFile.read()) -behaviourOut += original.read(4 * 7) # Unknown -behaviour.advance(4 * 15) # Unneeded stuff -originalName = original.readString() -newName = behaviour.readString() -if originalName != newName: # If these names don't match then the atlas's won't either, and we didn't ask for the original atlas file - print("Asset names " + originalName[4:].decode("utf-8") + " and " + newName[4:].decode("utf-8") + " don't match, aborting!") + def _readArray(self, data, itemLength): + self.Array = data.read(4) + length = int.from_bytes(self.Array, byteorder="little") + atlasWidth = struct.unpack(" 8: + data.advance((itemLength - 8) * 4) + x, y = struct.unpack(" atlasWidth: + info[4:8] = self.AtlasWidth + if y > atlasHeight: + info[8:12] = self.AtlasHeight + self.Array += info + + def _fromGame(self, data, filename): + data = DataScanner(data) + self.Header = data.read(4 * 7) + self.Filename = data.readString() + if self.type == "Hima": + self.BeforeFontName = data.read(4) + else: + self.BeforeFontName = bytes() + self.FontName = data.readString() + # Font face data + self.PointSize = data.read(4) + self.Padding = data.read(4) + self.LineHeight = data.read(4) + self.Baseline = data.read(4) + self.Ascender = data.read(4) + self.Descender = data.read(4) + self.CenterLine = data.read(4) + self.SuperscriptOffset = data.read(4) + self.SubscriptOffest = data.read(4) + self.SubSize = data.read(4) + self.Underline = data.read(4) + self.UnderlineThickness = data.read(4) + self.TabWidth = data.read(4) + self.CharacterCount = data.read(4) + self.AtlasWidth = data.read(4) + self.AtlasHeight = data.read(4) + + self.AfterFontFace = data.read(4 * 8) + self._readArray(data, 8) + self.Footer = data.rest() + + def _fromAssetCreator(self, data, filename): + data = DataScanner(data) + self.Header = data.read(4 * 15) + self.Filename = data.readString() + self.BeforeFontName = data.read(4 * 7) + self.FontName = data.readString() + # Font face data + self.PointSize = data.read(4) + self.Scale = data.read(4) + self.CharacterCount = data.read(4) + self.LineHeight = data.read(4) + self.Baseline = data.read(4) + self.Ascender = data.read(4) + self.CapHeight = data.read(4) + self.Descender = data.read(4) + self.CenterLine = data.read(4) + self.SuperscriptOffset = data.read(4) + self.SubscriptOffest = data.read(4) + self.SubSize = data.read(4) + self.Underline = data.read(4) + self.UnderlineThickness = data.read(4) + self.strikethrough = data.read(4) + self.strikethroughThickness = data.read(4) + self.TabWidth = data.read(4) + self.Padding = data.read(4) + self.AtlasWidth = data.read(4) + self.AtlasHeight = data.read(4) + + self.AfterFontFace = data.read(4 * 3) + self._readArray(data, 9) + self.Footer = data.rest() + +def combineFonts(original: FontFile, new: FontFile): + out = bytes() + out += original.Header + out += original.Filename + out += original.BeforeFontName + out += new.FontName + + out += new.PointSize + out += new.Padding + out += new.LineHeight + out += new.Baseline + out += new.Ascender + out += new.Descender + out += new.CenterLine + out += new.SuperscriptOffset + out += new.SubscriptOffest + out += new.SubSize + out += new.Underline + out += new.UnderlineThickness + out += new.TabWidth + out += new.CharacterCount + out += new.AtlasWidth + out += new.AtlasHeight + + out += original.AfterFontFace + out += new.Array + out += original.Footer + + return out + +if len(sys.argv) > 4: + atlasFN = sys.argv[1] + behaviourFN = sys.argv[2] + originalFN = sys.argv[3] + outFN = sys.argv[4] +else: + if len(sys.argv) < 4: + print("Usage: " + sys.argv[0] + " [newAtlas.dat] newMonoBehaviour.dat originalMonoBehaviour.dat outputFolder") + exit() + + behaviourFN = sys.argv[1] + originalFN = sys.argv[2] + outFN = sys.argv[3] + +if not os.path.isdir(outFN): + print("Output folder " + outFN + " must be a directory!") exit() -behaviourOut += newName -behaviour.advance(4 * 6) # Unneeded stuff -original.advance(4) # Will be read from behaviour -behaviourOut += behaviour.read(4) -original.readString() # Font name, will be gotten from behaviour -behaviourOut += behaviour.readString() # Font name -original.advance(4 * 16) # Font face data, will be read and converted from behaviour -# Read font face data, is in a completely different order from the target -PointSize = behaviour.read(4) -Scale = behaviour.read(4) -CharacterCount = behaviour.read(4) -LineHeight = behaviour.read(4) -Baseline = behaviour.read(4) -Ascender = behaviour.read(4) -CapHeight = behaviour.read(4) -Descender = behaviour.read(4) -CenterLine = behaviour.read(4) -SuperscriptOffset = behaviour.read(4) -SubscriptOffest = behaviour.read(4) -SubSize = behaviour.read(4) -Underline = behaviour.read(4) -UnderlineThickness = behaviour.read(4) -strikethrough = behaviour.read(4) -strikethroughThickness = behaviour.read(4) -TabWidth = behaviour.read(4) -Padding = behaviour.read(4) -AtlasWidth = behaviour.read(4) -AtlasHeight = behaviour.read(4) -# Write font face data -behaviourOut += PointSize -behaviourOut += Padding -behaviourOut += LineHeight -behaviourOut += Baseline -behaviourOut += Ascender -behaviourOut += Descender -behaviourOut += CenterLine -behaviourOut += SuperscriptOffset -behaviourOut += SubscriptOffest -behaviourOut += SubSize -behaviourOut += Underline -behaviourOut += UnderlineThickness -behaviourOut += TabWidth -behaviourOut += CharacterCount -behaviourOut += AtlasWidth -behaviourOut += AtlasHeight -behaviour.advance(4 * 3) # Will read this from original -behaviourOut += original.read(4 * 8) # Other data -behaviourOut += behaviour.peek(4) # Char info array length -originalArrayLength = int.from_bytes(original.read(4), byteorder='little') -newArrayLength = int.from_bytes(behaviour.read(4), byteorder='little') -original.advance(originalArrayLength * 4 * 8) # We don't need this data -atlasWidth = struct.unpack(" atlasWidth: - info[4:8] = AtlasWidth - if y > atlasHeight: - info[8:12] = AtlasHeight - behaviourOut += info - behaviour.advance(4) # One field is only in the new data so we need to remove it -behaviourOut += original.rest() # Rest of file can be copied from the original -with open(sys.argv[4] + "/" + os.path.basename(sys.argv[3]), "wb") as outFile: - outFile.write(behaviourOut) - + +with open(originalFN, "rb") as originalFile: + original = FontFile(originalFile.read(), originalFN) +with open(behaviourFN, "rb") as behaviourFile: + behaviour = FontFile(behaviourFile.read(), behaviourFN) +if len(sys.argv) > 4: + with open(atlasFN, "rb") as atlasFile: + atlas = DataScanner(atlasFile.read()) + atlasOut = b"" + if int.from_bytes(atlas.peek(4), byteorder="little") not in range(8, 32): + atlas.advance(4 * 7) + atlasName = atlas.readString() + atlas.advance(4 * 4) + else: + atlasName = atlas.readString() + atlasOut += atlasName + atlasOut += atlas.rest() + +atlasName = readString(atlasName) +with open(outFN + "/" + atlasName + "_Texture2D.dat", "wb") as outFile: + outFile.write(atlasOut) +with open(outFN + "/" + original.filename + "_TextMeshProFont.dat", "wb") as outFile: + outFile.write(combineFonts(original=original, new=behaviour)) +