Jump to content

Championship Manager 17 our CM 17


Recommended Posts

Posted

Hello everyone,

I am trying to extract the game assets (textures, images, and audio) from an Android APK game called championship manager 17 (developed by Square Enix).

Inside the extracted assets folder, there are no standard .assets files. Instead, there is a large main file named packres-android_core (with no extension).

I tried renaming it to .zip / .rar and opening it with AssetStudio, but it seems to be a proprietary binary format, and it didn't work. However, I can see dialogues and readable text inside data.save and extdata.save using a hex editor, so the main archive likely holds the media.

Could anyone help me create or find a QuickBMS script to unpack this packres-android_core file?

packres-android-core 38,0 MB

download: https://www.mediafire.com/file/hkwbtat2317h6lz/packres-android_core.png/file

Thank you in advance!

Screenshot_2.png

  • Members
Posted (edited)
5 hours ago, atrius98 said:

Hello everyone,

I am trying to extract the game assets (textures, images, and audio) from an Android APK game called championship manager 17 (developed by Square Enix).

Inside the extracted assets folder, there are no standard .assets files. Instead, there is a large main file named packres-android_core (with no extension).

I tried renaming it to .zip / .rar and opening it with AssetStudio, but it seems to be a proprietary binary format, and it didn't work. However, I can see dialogues and readable text inside data.save and extdata.save using a hex editor, so the main archive likely holds the media.

Could anyone help me create or find a QuickBMS script to unpack this packres-android_core file?

packres-android-core 38,0 MB

download: https://www.mediafire.com/file/hkwbtat2317h6lz/packres-android_core.png/file

Thank you in advance!

Screenshot_2.png

Python 3.X

 

import argparse
import struct
import sys
import threading
import zlib
from dataclasses import dataclass
from pathlib import Path, PurePosixPath


MAGIC = b"D0"


@dataclass(frozen=True)
class PackEntry:
    name: str
    offset: int
    stored_size: int
    original_size: int
    stored_raw: bool


def u16(data: bytes, offset: int) -> int:
    return struct.unpack_from("<H", data, offset)[0]


def u32(data: bytes, offset: int) -> int:
    return struct.unpack_from("<I", data, offset)[0]


def parse_pack(data: bytes) -> tuple[int, list[PackEntry]]:
    if len(data) < 8:
        raise ValueError("The file is too small to contain a D0 pack header.")
    if data[:2] != MAGIC:
        raise ValueError(f"Unsupported magic {data[:2]!r}; expected {MAGIC!r}.")

    index_end = u32(data, 2)
    entry_count = u16(data, 6)
    if not (8 <= index_end <= len(data)):
        raise ValueError(f"Invalid index end offset: 0x{index_end:X}.")

    index_pos = 8
    raw_entries: list[tuple[str, int, int, bool]] = []
    for entry_index in range(entry_count):
        name_end = data.find(b"\0", index_pos, index_end)
        if name_end < 0:
            raise ValueError(f"Missing file-name terminator in index entry {entry_index}.")
        if name_end + 9 > index_end:
            raise ValueError(f"Truncated metadata in index entry {entry_index}.")

        name = data[index_pos:name_end].decode("utf-8", "replace")
        packed_size_field = u32(data, name_end + 1)
        original_size = u32(data, name_end + 5)
        stored_raw = bool(packed_size_field & 0x80000000)
        stored_size = packed_size_field & 0x7FFFFFFF
        if not name or stored_size == 0:
            raise ValueError(f"Invalid index entry {entry_index} at 0x{index_pos:X}.")

        raw_entries.append((name, stored_size, original_size, stored_raw))
        index_pos = name_end + 9

    if index_pos != index_end:
        raise ValueError(
            f"Index parsed to 0x{index_pos:X}, but header declares 0x{index_end:X}."
        )

    entries: list[PackEntry] = []
    data_pos = index_end
    for name, stored_size, original_size, stored_raw in raw_entries:
        data_end = data_pos + stored_size
        if data_end > len(data):
            raise ValueError(f"Resource {name!r} extends past the end of the container.")
        entries.append(
            PackEntry(
                name=name,
                offset=data_pos,
                stored_size=stored_size,
                original_size=original_size,
                stored_raw=stored_raw,
            )
        )
        data_pos = data_end

    if data_pos != len(data):
        raise ValueError(
            f"Resource data ends at 0x{data_pos:X}, but file ends at 0x{len(data):X}."
        )

    return index_end, entries


def safe_relative_path(name: str) -> Path:
    normalized = name.replace("\\", "/")
    parts = []
    for part in PurePosixPath(normalized).parts:
        if part in ("", ".", "/"):
            continue
        if part == "..":
            continue
        clean = part.replace(":", "_")
        parts.append(clean)
    if not parts:
        raise ValueError(f"Resource name {name!r} does not contain a safe path.")
    return Path(*parts)


def decompress_payload(payload: bytes, entry: PackEntry) -> bytes:
    if entry.stored_raw or entry.stored_size == entry.original_size:
        return payload

    errors: list[str] = []
    for window_bits in (zlib.MAX_WBITS, -zlib.MAX_WBITS):
        try:
            output = zlib.decompress(payload, window_bits)
            if entry.original_size and len(output) != entry.original_size:
                raise ValueError(
                    f"decompressed size {len(output)} does not match {entry.original_size}"
                )
            return output
        except Exception as exc:
            errors.append(str(exc))
    raise ValueError(f"Could not decompress {entry.name!r}: {'; '.join(errors)}")


def extract_pack(input_path: Path, output_dir: Path, preserve_folders: bool = True) -> list[Path]:
    data = input_path.read_bytes()
    _, entries = parse_pack(data)
    output_dir.mkdir(parents=True, exist_ok=True)

    written: list[Path] = []
    for index, entry in enumerate(entries, 1):
        payload = data[entry.offset : entry.offset + entry.stored_size]
        output = decompress_payload(payload, entry)
        relative = safe_relative_path(entry.name)
        if not preserve_folders:
            relative = Path(f"{index:04d}_{relative.name}")
        output_path = output_dir / relative
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_bytes(output)
        written.append(output_path)
    return written


def summary_text(input_path: Path, index_end: int, entries: list[PackEntry]) -> str:
    raw_count = sum(entry.stored_raw for entry in entries)
    compressed_count = len(entries) - raw_count
    lines = [
        f"Container: {input_path.name}",
        f"Magic: {MAGIC.decode('ascii')}",
        f"Index end: 0x{index_end:X}",
        f"Entries: {len(entries)}",
        f"Raw entries: {raw_count}",
        f"Compressed entries: {compressed_count}",
        "",
    ]
    for entry in entries[:80]:
        mode = "raw" if entry.stored_raw else "compressed"
        lines.append(
            f"0x{entry.offset:08X}  {entry.stored_size:9d}  {mode:10s}  {entry.name}"
        )
    if len(entries) > 80:
        lines.append(f"... {len(entries) - 80} more entries")
    return "\n".join(lines)


def run_gui() -> None:
    import tkinter as tk
    from tkinter import filedialog, messagebox, scrolledtext, ttk

    root = tk.Tk()
    root.title("D0 Packres Extractor")
    root.geometry("800x540")
    root.resizable(True, True)

    default_file = Path("packres-android_core.png")
    input_var = tk.StringVar(value=str(default_file.resolve()) if default_file.exists() else "")
    output_var = tk.StringVar(value=str((Path.cwd() / "_packres_android_core_extract").resolve()))
    folders_var = tk.BooleanVar(value=True)
    status_var = tk.StringVar(value="Ready")

    frame = ttk.Frame(root, padding=12)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(1, weight=1)
    frame.rowconfigure(5, weight=1)

    def browse_input() -> None:
        path = filedialog.askopenfilename(
            title="Choose packres container",
            filetypes=[("Packres files", "*.png *.dat *.bin"), ("All files", "*.*")],
        )
        if path:
            input_var.set(path)
            source = Path(path)
            output_var.set(str(source.with_name(source.stem + "_extract")))

    def browse_output() -> None:
        path = filedialog.askdirectory(title="Choose output folder")
        if path:
            output_var.set(path)

    def set_log(text: str) -> None:
        log.configure(state="normal")
        log.delete("1.0", "end")
        log.insert("1.0", text)
        log.configure(state="disabled")

    def scan_clicked() -> None:
        try:
            input_path = Path(input_var.get().strip())
            if not input_path.is_file():
                raise ValueError("Input file does not exist.")
            index_end, entries = parse_pack(input_path.read_bytes())
            set_log(summary_text(input_path, index_end, entries))
            status_var.set(f"Scan complete: {len(entries)} entries.")
        except Exception as exc:
            status_var.set("Scan failed")
            messagebox.showerror("Scan failed", str(exc))

    def extract_clicked() -> None:
        def worker() -> None:
            try:
                input_path = Path(input_var.get().strip())
                output_dir = Path(output_var.get().strip())
                if not input_path.is_file():
                    raise ValueError("Input file does not exist.")
                written = extract_pack(input_path, output_dir, folders_var.get())
                index_end, entries = parse_pack(input_path.read_bytes())
                text = summary_text(input_path, index_end, entries)
                text += f"\n\nWrote {len(written)} file(s) to:\n{output_dir}"
                root.after(0, lambda: set_log(text))
                root.after(0, lambda: status_var.set(f"Extracted {len(written)} file(s)."))
                root.after(
                    0,
                    lambda: messagebox.showinfo(
                        "Extract complete", f"Extracted {len(written)} file(s)."
                    ),
                )
            except Exception as exc:
                root.after(0, lambda: status_var.set("Extract failed"))
                root.after(0, lambda: messagebox.showerror("Extract failed", str(exc)))

        status_var.set("Extracting...")
        threading.Thread(target=worker, daemon=True).start()

    ttk.Label(frame, text="Input Container").grid(row=0, column=0, sticky="w", padx=6, pady=6)
    ttk.Entry(frame, textvariable=input_var).grid(row=0, column=1, sticky="ew", padx=6, pady=6)
    ttk.Button(frame, text="Browse", command=browse_input).grid(row=0, column=2, padx=6, pady=6)

    ttk.Label(frame, text="Output Folder").grid(row=1, column=0, sticky="w", padx=6, pady=6)
    ttk.Entry(frame, textvariable=output_var).grid(row=1, column=1, sticky="ew", padx=6, pady=6)
    ttk.Button(frame, text="Browse", command=browse_output).grid(row=1, column=2, padx=6, pady=6)

    ttk.Checkbutton(frame, text="Preserve folders", variable=folders_var).grid(
        row=2, column=0, columnspan=2, sticky="w", padx=6, pady=8
    )

    buttons = ttk.Frame(frame)
    buttons.grid(row=3, column=0, columnspan=3, sticky="e", padx=6, pady=8)
    ttk.Button(buttons, text="Scan", command=scan_clicked).grid(row=0, column=0, padx=6)
    ttk.Button(buttons, text="Extract", command=extract_clicked).grid(row=0, column=1, padx=6)

    ttk.Label(frame, textvariable=status_var).grid(
        row=4, column=0, columnspan=3, sticky="w", padx=6, pady=4
    )
    log = scrolledtext.ScrolledText(frame, wrap="none", state="disabled")
    log.grid(row=5, column=0, columnspan=3, sticky="nsew", padx=6, pady=6)

    root.mainloop()


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Extract D0 packres containers.")
    parser.add_argument("input", nargs="?", help="Input packres file.")
    parser.add_argument("-o", "--output", help="Output folder. Default: <input>_extract")
    parser.add_argument("--flat", action="store_true", help="Do not preserve resource folders.")
    parser.add_argument("--scan-only", action="store_true", help="Only display the index.")
    parser.add_argument("--gui", action="store_true", help="Open the graphical interface.")
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(sys.argv[1:] if argv is None else argv)
    if args.gui or not args.input:
        run_gui()
        return 0

    input_path = Path(args.input)
    output_dir = Path(args.output) if args.output else input_path.with_name(input_path.stem + "_extract")
    data = input_path.read_bytes()
    index_end, entries = parse_pack(data)
    print(summary_text(input_path, index_end, entries))
    if args.scan_only:
        return 0

    written = extract_pack(input_path, output_dir, preserve_folders=not args.flat)
    print(f"\nWrote {len(written)} file(s) to {output_dir}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

738D21D2-9223-4810-A4F3-4201ED13BE7A.thumb.png.83d9218e30a51d9b896bc3fc24a85029.png

Edited by zbirow
  • Like 1
  • Thanks 1
Posted (edited)

Any idea how I can open these files called '.cm'? I'm creating a mod, but I don't understand much about programming, only hexadecimal code and reverse engineering. I'm studying how to change the game files without breaking the game, and especially how to change the game's music. And above all, thank you for helping me, you're truly an angel.

Screenshot_1.png

Edited by atrius98
  • Members
Posted (edited)
2 hours ago, atrius98 said:

Any idea how I can open these files called '.cm'? I'm creating a mod, but I don't understand much about programming, only hexadecimal code and reverse engineering. I'm studying how to change the game files without breaking the game, and especially how to change the game's music. And above all, thank you for helping me, you're truly an angel.

Screenshot_1.png

.cm files aren't the same. They have different structures.

For example, the regens.cm file probably lists players. Below their names is one byte of metadata. I'm not familiar with the game, so I don't know if these are club tags/country or something else.
image.png.a234267d7bf8e13cb36c2e777f75c855.png

Edited by zbirow
Posted

Thank you so much! The extraction worked perfectly. Is there a way to repack the modified files back into the packres-android_core container using Python? "re-pack"

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...