Skip to content

Instantly share code, notes, and snippets.

@dmitmel
Created February 11, 2022 14:11
Show Gist options
  • Save dmitmel/6e3d4062fb7bdff6c986e8e8775df494 to your computer and use it in GitHub Desktop.
Save dmitmel/6e3d4062fb7bdff6c986e8e8775df494 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# Font patcher for this game: https://store.steampowered.com/app/1056180/Cathedral/
import gzip
import io
import json
import struct
import sys
from typing import Any, Dict, List, TypedDict, cast
import PIL.Image
FONT_RESOURCE_IDS = {
"default": "RESOURCES\\FONT\\DEFAULT",
"small-gui": "RESOURCES\\FONT\\SMALL-GUI",
}
SUPPLEMENTARY_FONT_PATHS = {
"default": "default-ru.png",
"small-gui": "small-gui-ru.png",
}
PATCHED_CHARACTERS = "абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"
class MetadataAnimationTrack(TypedDict):
start: int
end: int
fps: int
class MetadataAnimation(TypedDict):
frameWidth: int
frameHeight: int
tracks: Dict[str, MetadataAnimationTrack]
class MetadataResource(TypedDict):
key: str
value: str
class MetadataLayout(TypedDict):
x: int
y: int
width: int
height: int
class Metadata(TypedDict):
animations: Dict[str, MetadataAnimation]
properties: Dict[str, Dict[str, object]]
resources: List[MetadataResource]
MetadataAtlasLayout = Dict[str, MetadataLayout]
def main(argv: List[str]) -> int:
pak_file_path = sys.argv[1]
with gzip.GzipFile(pak_file_path, "rb") as pak_file:
data_len: int
(data_len,) = struct.unpack("<i", pak_file.read(4))
metadata: Metadata = json.loads(pak_file.read(data_len).decode("utf8"))
(data_len,) = struct.unpack("<i", pak_file.read(4))
atlas_layout: MetadataAtlasLayout = json.loads(pak_file.read(data_len).decode("utf8"))
atlas_data = pak_file.read()
with PIL.Image.open(io.BytesIO(atlas_data), formats=["PNG"]) as orig_atlas_img:
new_atlas_img = orig_atlas_img
resources_to_files_mapping = {res["key"]: res["value"] for res in metadata["resources"]}
additional_height = 0
for font_name, resource_id in FONT_RESOURCE_IDS.items():
font_sprite = atlas_layout[resources_to_files_mapping[resource_id]]
additional_height = max(additional_height, font_sprite["height"])
new_atlas_img = PIL.Image.new(
cast(Any, orig_atlas_img.mode),
(orig_atlas_img.width, orig_atlas_img.height + additional_height)
)
new_atlas_img.paste(orig_atlas_img, (0, 0))
font_pos_x = 0
font_pos_y = orig_atlas_img.height
for font_name, resource_id in FONT_RESOURCE_IDS.items():
font_sprite = atlas_layout[resources_to_files_mapping[resource_id]]
font_animation = metadata["animations"][resource_id]
with PIL.Image.open(SUPPLEMENTARY_FONT_PATHS[font_name], formats=["PNG"]) as suppl_img:
expected_suppl_width = font_animation["frameWidth"] * len(PATCHED_CHARACTERS)
expected_suppl_height = font_animation["frameHeight"]
if suppl_img.width != expected_suppl_width or suppl_img.height != expected_suppl_height:
raise Exception(
f"Invalid dimensions of the supplementary font: {suppl_img.width}x{suppl_img.height}, must be: {expected_suppl_width}x{expected_suppl_height}"
)
orig_font_img = orig_atlas_img.crop((
font_sprite["x"],
font_sprite["y"],
font_sprite["x"] + font_sprite["width"],
font_sprite["y"] + font_sprite["height"],
))
font_sprite["x"] = font_pos_x
font_sprite["y"] = font_pos_y
font_sprite["width"] = orig_font_img.width + suppl_img.width
font_sprite["height"] = max(orig_font_img.height, suppl_img.height)
font_pos_x += font_sprite["width"]
new_atlas_img.paste(orig_font_img, (font_sprite["x"], font_sprite["y"]))
new_atlas_img.paste(
suppl_img, (font_sprite["x"] + orig_font_img.width, font_sprite["y"])
)
start_idx = orig_font_img.width // font_animation["frameWidth"]
for char in PATCHED_CHARACTERS:
font_animation["tracks"]["_0x" + char.encode("utf8").hex()] = (
MetadataAnimationTrack(start=start_idx, end=start_idx, fps=1)
)
start_idx += 1
if new_atlas_img is orig_atlas_img:
new_atlas_img = orig_atlas_img.copy()
with gzip.GzipFile(pak_file_path, "wb") as pak_file:
data: bytes
data = json.dumps(metadata).encode("utf8")
pak_file.write(struct.pack("<i", len(data)))
pak_file.write(data)
data = json.dumps(atlas_layout).encode("utf8")
pak_file.write(struct.pack("<i", len(data)))
pak_file.write(data)
data_io = io.BytesIO()
new_atlas_img.save(data_io, format="PNG")
pak_file.write(data_io.getvalue())
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment