Skip to content

Instantly share code, notes, and snippets.

@infval
Last active December 23, 2024 14:32
Show Gist options
  • Save infval/3d12781f57e891d2905212f5aaebc6c5 to your computer and use it in GitHub Desktop.
Save infval/3d12781f57e891d2905212f5aaebc6c5 to your computer and use it in GitHub Desktop.
[Wii] Coraline | dxt Converter / Unpacker / Packer / Extractor | Magic: 30 81 80 00 / \x30\x81\x80\x00 / 30818000 / 308180
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
[Wii] Coraline
.dxt <-> PNG
Requirements:
pip install -U pillow
Usage:
# ./*.dxt -> ./*.dxt.png
script.py
# filename.dxt -> filename.dxt.png
script.py filename.dxt
# filename.dxt.png + filename.dxt (header) -> filename.dxt
script.py filename.dxt.png
Useful Tool:
Color quantizer - http://x128.ho.ua/color-quantizer.html
"""
__version__ = "1.1"
__author__ = "infval"
import sys
from pathlib import Path
from struct import unpack
from collections import OrderedDict
from PIL import Image
HEADER_SIZE = 0x80
class Texture:
"""
[Wii] Color Index 8-bits (C8) + Palette RGBA8
"""
def __init__(self, path, offset, w, h, pal_offset):
self.path = path
self.offset = offset
self.w = w
self.h = h
self.pal_offset = pal_offset
def to_png(self, png_path):
im = Image.new("RGBA", (self.w, self.h))
pixels = im.load()
with open(self.path, "rb") as f:
f.seek(self.offset)
b = f.read(self.w * self.h)
f.seek(self.pal_offset)
b_pal = f.read(256 * 4)
pal = []
for i in range(256):
index = i * 2
pal.append((b_pal[index+1], # R
b_pal[index+0], # G
b_pal[index+1+0x200], # B
b_pal[index+0+0x200])) # A
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
x = 0
y = 0
for i in range(self.w * self.h):
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
c = b[index]
pixels[x, y] = pal[c]
x += 1
if x == self.w:
x = 0
y += 1
im.save(png_path, "PNG")
print("Unpacked: {} -> {}".format(self.path, png_path))
def from_png(self, bin_path, base_path=None):
im = Image.open(self.path).convert("RGBA")
pixels = im.load()
self.w, self.h = im.size
if not base_path:
# Raw
self.offset = 0
self.pal_offset = self.w * self.h
b = bytearray(self.w * self.h + 256 * 4)
else:
with open(base_path, "rb") as f:
b = bytearray(f.read())
pal = OrderedDict()
pal_i = 0
b_img = bytearray(self.w * self.h)
tw = 8
th = 4
bytes_in_tile = tw * th
bytes_in_row = self.w * th
i = 0
for y in range(self.h):
for x in range(self.w):
c = pixels[x, y]
if c not in pal:
if len(pal) >= 256:
print("Too many colors! Max: 256.", self.path)
sys.exit(1)
else:
pal[c] = pal_i
pal_i += 1
index = i // bytes_in_row * bytes_in_row
index += x // tw * bytes_in_tile
index += (y % th) * tw + x % tw
b_img[index] = pal[c]
i += 1
b[self.offset: self.offset + self.w * self.h] = b_img
pal = list(pal.keys())
pal += [(0, 0, 0, 0)] * (256 - len(pal)) # Unused colors
for i in range(256):
index1 = self.pal_offset + i * 2
index2 = self.pal_offset + i * 2 + 0x200
cR, cG, cB, cA = pal[i]
b[index2+0] = cA
b[index2+1] = cB
b[index1+0] = cG
b[index1+1] = cR
with open(bin_path, "wb") as f:
f.write(b)
print("Packed: {} -> {}".format(self.path, bin_path))
def dxt_to_png(path):
with open(path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
png_path = path.with_name(path.name + ".png")
Texture(path, HEADER_SIZE, w, h, HEADER_SIZE + w * h).to_png(png_path)
def png_to_dxt(path):
bin_path = path.with_suffix('')
with open(bin_path, "rb") as f:
f.seek(0x0A)
w, h = unpack("<BB", f.read(2))
w = 1 << w
h = 1 << h
Texture(path, HEADER_SIZE, None, None, HEADER_SIZE + w * h).from_png(bin_path, bin_path)
if __name__ == '__main__':
if len(sys.argv) == 1:
path = Path(".")
for p in path.glob("*.dxt"):
dxt_to_png(p)
else:
p = Path(sys.argv[1])
if p.suffix == ".png":
png_to_dxt(p)
else:
dxt_to_png(p)
@Rypie109
Copy link

Rypie109 commented Jul 7, 2024

wait, I just realised dxt is txd backwards. Same file structure? Idk

@romhacking
Copy link

romhacking commented Jul 8, 2024

How does this work?

I don't have access to the "infval" account right now. For example, open the global.wii file in a HEX editor, find bytes 30 81 80 00, for example at position 0x700, + 0xA (position 0x70A): width, + 0xB (position 0x70B): height, texture size = 2^width * 2^height + 0x480, here it is 2^8 * 2^9 + 0x480 = 0x20480 bytes, copy the bytes from position 0x700 to 0x20B7F into a separate file like 1.dxt. Run on the command line:
dxt_to_png.py 1.dxt
File 1.dxt.png will appear, edit it. Run:
dxt_to_png.py 1.dxt.png
The 1.dxt file will be modified with the new image.
The Pillow library is required, run on the command line (I run as administrator):
pip install -U pillow
It's better for you to write a script that will extract and insert textures back, the size does not change after modification. Textures are also stored in gtex_s.wii, legal.wii, l_cred_s.wii, l_cred_t.wii, g_d1nw.wii, l_d1nw_s.wii, etc. (you can use 010 Editor to search through all files).
Use http://x128.ho.ua/color-quantizer.html to reduce colors to 256 with alpha (transparency), otherwise the script will not work. You can use TruePNG (http://x128.ho.ua/pngutils.html). Pillow seems to be able to do it too, but I didn’t know and the results are different.

wait, I just realised dxt is txd backwards. Same file structure? Idk

I received the files from the game translator, I don’t know why the file has such an extension. 😄

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

OMG thank you! I could not for the life of me figure this out! I am also not really acquinted to gist as I am to regular github, so if there were instructions, I could not find them.

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

Is there a way to batch process these files when converting dxt to png?

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

I am getting an error: "line 50
with open(self.path, "rb") as f:
^
SyntaxError: invalid syntax"

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

Also, 1 more question, what I am getting from this is to copy from each 30 81 80 00 to the next, I am still somewhat a beginner when it comes to hex editing, but I do have a decent enough understanding.

@romhacking
Copy link

romhacking commented Jul 8, 2024

Is there a way to batch process these files when converting dxt to png?

Execute the script without arguments to convert all .dxt files from the script folder to .png. You can write your own .bat file. To get all the .dxt from .wii files you need to write code.

I am getting an error: "line 50

The script may have been copied incorrectly, try using Download ZIP. This is a Python 3 script.

what I am getting from this is to copy from each 30 81 80 00 to the next

This is necessary to obtain the .dxt file, my script will not open the .wii file.

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

I downloaded the script, but I do not think I am using python 3. I will try to load the right python version somehow.

@romhacking
Copy link

I will try to load the right python version somehow

I think another reason. I checked now on version 3.12.4 (https://www.python.org), there is no error.

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

What are you running the script through? Cmd? Python from cmd? Shell?

@romhacking
Copy link

What are you running the script through?

cmd. PowerShell works too.

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

Got it, now I just have to figure out how to switch my py version from 3.11 to 3.12 in cmd. Thanks for answering all my questions, I really appreciate this, and the script!

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

I figured it out!

@Rypie109
Copy link

Rypie109 commented Jul 8, 2024

I am a bit confused on what I should be doing on color quantizer, as when I select 256 from the dropdown menu, and press, ok, it switches to 146. Also, you mention that the script wont work if I dont reduce it to 256, I am assuming you are referring to your python script, but that doesnt seem to have any issues, maybe im understanding it wrong.

@romhacking
Copy link

what I should be doing on color quantizer, as when I select 256 from the dropdown menu, and press, ok, it switches to 146

"Color quantizer" is needed if you want to convert the modified PNG back to a .dxt file and the PNG will have more than 256 colors (my script will display the message "Too many colors! Max: 256."). You have fewer than 256 colors, so the program is not needed in this case.

@Rypie109
Copy link

Rypie109 commented Jul 9, 2024

Ok, I have no need to recompress the textures. I have figured this all out, and now I need to find out how to make a script to automaticly split at certain points. Any ideas where to start/where to ask?

@romhacking
Copy link

I need to find out how to make a script to automaticly split at certain points

Extracting .dxt files from .wii is easy. Here is the script, name it wii_to_dxt.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
[Wii] Coraline
  .wii -> .dxt

Usage:
# ./*.wii -> ./*.dxt
  script.py
# filename.wii -> filename_*.dxt
  script.py filename.wii
"""
__version__ = "1.0"
__author__  = "infval"

import sys
from pathlib import Path

def wii_to_dxt(path):
    print(f"# {path}")
    data = path.read_bytes()
    ind = -1
    while True:
        ind = data.find(bytes.fromhex("30 81 80 00"), ind + 1)
        if ind == -1:
            break
        w, h = data[ind + 0xA], data[ind + 0xB]
        size = (1 << w) * (1 << h) + 0x480
        p = path.with_name(path.stem + f"_{ind:06X}.dxt")
        p.write_bytes(data[ind : ind + size])
        print(f"> {p}")

if __name__ == '__main__':
    if len(sys.argv) == 1:
        path = Path(".")
        for p in path.glob("*.wii"):
            wii_to_dxt(p)
    else:
        p = Path(sys.argv[1])
        wii_to_dxt(p)

To extract all .wii in the script folder, run the script:
wii_to_dxt.py
To unpack a single .wii, pass the file path as a command line argument:
wii_to_dxt.py global.wii

@Rypie109
Copy link

...YOU MADLAD, thank you so much!

@Rypie109
Copy link

You have been monumental in this project, and I made sure to tell people that this stuff was by you. The last thing for me to do to technically finish this 235 day project is to get the rigs, but I do not expect you to do that at all, as I don't want to waste your time, and you have already done so much for me already.

@Rypie109
Copy link

@romhacking I have some questions. What made you create this script? Any motive? Are you also interested in datamining this game?

@romhacking
Copy link

@Rypie109 The script dxt_to_png.py was made for the Russian translation of the game Coraline, that's all. I mostly do small hacks for NES games.

@Rypie109
Copy link

@romhacking That is very interesting! I am assuming this was a fan translation?

@romhacking
Copy link

@Rypie109 Of course it's an unofficial translation. I only made this script.

@Rypie109
Copy link

Hi, it's me again. Is there a way to make the dxt_to_png script work with a game that has the exact same form
l_ben1_t_2F1100
at, yet has a slightly different formatting? Here is a file I copied out, same header same everthing, and when converted it looks like this. Heres a sample dxt file, just rename it from .svg to .dxt
l_ben1_t_2F1100 dxt

@Rypie109
Copy link

The image I attached is not properly converted, open it in a new tab or save it to see

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment