You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

410 lines
16 KiB
Python

#!/usr/bin/python
"""
homm3lodextract - extract data from heroes of might and magic 3 lod
archives and convert pcx images and def animations to png
images
copyright 2008 - Johannes 'josch' Schauer <j.schauer@email.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import zlib, os
import struct
import sys
import Image, ImageDraw
def read_frame_3(width, height, data):
length = height*width/32 #length of scanline
#UNSIGNED short here!!
offsets = struct.unpack("%dH"%length, data[:length*2])
offsets += (len(data),)
raw = ""
for i in xrange(len(offsets)-1):
line = data[offsets[i]:offsets[i+1]]
pos = 0
while pos < len(line):
count = ord(line[pos])
if 0x00 <= count <= 0x1F: #empty
raw += '\x00'*(count+1)
pos +=1
elif 0x20 <= count <= 0x3F: #light shadow
raw += '\x01'*(count-31)
pos +=1
elif 0x40 <= count <= 0x5F: #only used in Tshre.def and AvGnoll.def
raw += '\x02'*(count-63)
pos +=1
elif 0x60 <= count <= 0x7F: #only used in Tshre.def
raw += '\x03'*(count-95)
pos +=1
elif 0x80 <= count <= 0x9F: #strong shadow
raw += '\x04'*(count-127)
pos +=1
elif 0xA0 <= count <= 0xBF: #replaced by player color
raw += '\x05'*(count-159)
pos +=1
elif 0xE0 <= count <= 0xFF: #normal colors
raw += line[pos+1:pos+count-222]
pos += count-222
else:
raise Exception("%02X"%count)
return raw
def read_frame_1(width, height, data):
try:
offsets = struct.unpack("%di"%height, data[:height*4])
except:
#this failes for SGTWMTB.def
print "cannot read scanline offses"
return None
offsets += (len(data),)
raw = ""
for i in xrange(len(offsets)-1):
line = data[offsets[i]:offsets[i+1]]
pos = 0
while pos < len(line):
try:
count = ord(line[pos+1])+1
except IndexError:
#this failes for SGTWMTA.def, SGTWMTB.def
print "cannot read scanline"
return None
if ord(line[pos]) is 0xFF:
raw+=line[pos+2:pos+2+count]
pos+=2+count
else:
raw+=line[pos]*count
pos+=2
return raw
def save_frame(path, num, data, palette, width, height, fname):
(data_size, frame_type, fwidth, fheight, img_width, img_height, offset_x,
offset_y) = struct.unpack("8i", data[0:32])
#this only does not match in OVSLOT.def
if width != fwidth:
print "frame width %d does not match def width %d"%(fwidth, width)
if height != fheight:
print "frame height %d does not match def height %d"%(fheight, height)
data = data[32:32+data_size]
if frame_type is 0:
#type 0 is raw indexed image data
buffer = data
elif frame_type is 1:
buffer = read_frame_1(img_width, img_height, data)
elif frame_type is 2:
#this is seldomly used and seems to decode fine using type3
buffer = read_frame_3(img_width, img_height, data)
elif frame_type is 3:
buffer = read_frame_3(img_width, img_height, data)
else:
raise Exception("frame type %d not supported"%frame_type)
if buffer is not None:
im = Image.new("P", (fwidth, fheight), 0x00)
im.paste(
Image.frombuffer(
'RGB',
(img_width, img_height),
buffer,
'raw', 'P', 0, 1),
(offset_x, offset_y))
if fname == "clrrvr.def":
path = os.path.join(path, "%d"%num)
if not os.path.exists(path):
os.makedirs(path)
for i in xrange(12):
im.putpalette(palette[:189*3]
+palette[201*3-i*3:201*3]
+palette[189*3:201*3-i*3]
+palette[201*3:])
im2 = im.convert("RGBA")
data = im2.load()
for x in xrange(im.size[0]):
for y in xrange(im.size[1]):
if data[x, y] == (0, 255, 255, 255):
data[x, y] = (0, 0, 0, 0)
if os.path.exists(os.path.join(path, "%d.png"%i)):
print "overwriting %s" % os.path.join(path, "%d.png"%i)
im2.save(os.path.join(path, "%d.png"%i), "PNG")
elif fname == "lavrvr.def":
path = os.path.join(path, "%d"%num)
if not os.path.exists(path):
os.makedirs(path)
for i in xrange(9):
im.putpalette(palette[:240*3]
+palette[249*3-i*3:249*3]
+palette[240*3:249*3-i*3]
+palette[249*3:])
im2 = im.convert("RGBA")
data = im2.load()
for x in xrange(im.size[0]):
for y in xrange(im.size[1]):
if data[x, y] == (0, 255, 255, 255):
data[x, y] = (0, 0, 0, 0)
if os.path.exists(os.path.join(path, "%d.png"%i)):
print "overwriting %s" % os.path.join(path, "%d.png"%i)
im2.save(os.path.join(path, "%d.png"%i), "PNG")
elif fname == "mudrvr.def":
path = os.path.join(path, "%d"%num)
if not os.path.exists(path):
os.makedirs(path)
for i in xrange(12):
im.putpalette(palette[:228*3]
+palette[240*3-i*3:240*3]
+palette[228*3:240*3-i*3]
+palette[240*3:])
im2 = im.convert("RGBA")
data = im2.load()
for x in xrange(im.size[0]):
for y in xrange(im.size[1]):
if data[x, y] == (0, 255, 255, 255):
data[x, y] = (0, 0, 0, 0)
if os.path.exists(os.path.join(path, "%d.png"%i)):
print "overwriting %s" % os.path.join(path, "%d.png"%i)
im2.save(os.path.join(path, "%d.png"%i), "PNG")
elif fname == "watrtl.def":
path = os.path.join(path, "%d"%num)
if not os.path.exists(path):
os.makedirs(path)
for i in xrange(12):
im.putpalette(palette[:229*3]
+palette[241*3-i*3:241*3]
+palette[229*3:241*3-i*3]
+palette[241*3:242*3]
+palette[254*3-i*3:254*3]
+palette[242*3:254*3-i*3]
+palette[254*3:])
if os.path.exists(os.path.join(path, "%d.png"%i)):
print "overwriting %s" % os.path.join(path, "%d.png"%i)
im.convert("RGBA").save(os.path.join(path, "%d.png"%i), "PNG")
else:
im.putpalette(palette)
im = im.convert("RGBA")
data = im.load()
for x in xrange(im.size[0]):
for y in xrange(im.size[1]):
if data[x, y] == (0, 255, 255, 255):
data[x, y] = (0, 0, 0, 0)
elif data[x, y] == (255, 150, 255, 255):
data[x, y] = (0, 0, 0, 64)
elif data[x, y] == (255, 151, 255, 255):
data[x, y] = (0, 0, 0, 64)
elif data[x, y] == (255, 0, 255, 255):
data[x, y] = (0, 0, 0, 128)
if os.path.exists(os.path.join(path, "%d.png"%num)):
print "overwriting %s" % os.path.join(path, "%d.png"%num)
im.save(os.path.join(path, "%d.png"%num), "PNG")
else:
print "invalid frame"
def save_file(fid, name, ftype, data):
if not os.path.exists(os.path.join("data", ftype)):
os.makedirs(os.path.join("data", ftype))
if os.path.exists(os.path.join("data", ftype, name)):
print "overwriting %s" % os.path.join("data", ftype, name)
file = open(os.path.join("data", ftype, name), "w")
file.write(data)
file.close()
def lod_extract(filename):
lod = open(filename)
if lod.read(4) != "LOD\x00":
raise Exception("not an LOD archive")
(version, files_count) = struct.unpack("2i", lod.read(8))
print "Version: %d, File Count: %d"%(version, files_count) #RoE: 200, AB: 500
for fid in xrange(files_count):
lod.seek(92+(fid*32))
name = lod.read(16).split('\x00')[0].lower()
(offset, size_orig, ftype, size_comp) = \
struct.unpack("4i", lod.read(16))
lod.seek(offset)
if size_comp:
pcx = zlib.decompress(lod.read(size_comp))
else:
pcx = lod.read(size_orig)
if ftype == 1: #h3c file
print fid, name, ftype
save_file(fid, name, "campaigns", pcx)
elif ftype == 2: #txt file
print fid, name, ftype
save_file(fid, name, "txt", pcx)
elif ftype == 16: #pcx palette image
(size, width, height) = struct.unpack("3i", pcx[:12])
print fid, name, width, height
if not os.path.exists(os.path.join("data", "pcx_palette")):
os.makedirs(os.path.join("data", "pcx_palette"))
im = Image.frombuffer('RGB', (width, height), pcx[12:size+12],
'raw', 'P', 0, 1)
im.putpalette(pcx[12+size:12+size+3*256])
if os.path.exists(os.path.join("data", "pcx_palette", name)):
print "overwriting %s" % os.path.join("data", "pcx_palette", name)
im.save(os.path.join("data", "pcx_palette", name), "PNG")
elif ftype == 17: #pcx rgb image
(size, width, height) = struct.unpack("3i", pcx[:12])
print fid, name, width, height
if not os.path.exists(os.path.join("data", "pcx_rgb")):
os.makedirs(os.path.join("data", "pcx_rgb"))
im = Image.frombuffer('RGB', (width, height), pcx[12:size+12],
'raw', 'RGB', 0, 1)
if os.path.exists(os.path.join("data", "pcx_rgb", name)):
print "overwriting %s" % os.path.join("data", "pcx_rgb", name)
im.save(os.path.join("data", "pcx_rgb", name), "PNG")
elif ftype in (64, 65, 66, 67, 68, 69, 70, 71, 73,): #def animation
#in 0.2% of all def files the internal def type does not match the
#type given in the lod archive header but tests show that in this
#case the type given in the def file is more important
(ftype, width, height, sequences_count) = \
struct.unpack("4i", pcx[0:16])
palette = pcx[16:784]
print fid, name, ftype
#all def sequences are thrown into the same folder
#there are no 65 def types - only 65 file types
if ftype == 64:
parent = os.path.join("data", "combat_spells", name)
elif ftype == 66:
parent = os.path.join("data", "combat_creatures", name)
elif ftype == 67:
parent = os.path.join("data", "advmap_objects", name)
elif ftype == 68:
parent = os.path.join("data", "advmap_heroes", name)
elif ftype == 69:
parent = os.path.join("data", "advmap_tiles", name)
elif ftype == 70:
parent = os.path.join("data", "cursors", name)
elif ftype == 71:
parent = os.path.join("data", "interface", name)
elif ftype == 73:
parent = os.path.join("data", "combat_heroes", name)
else:
raise NotImplementedError
if not os.path.exists(parent):
os.makedirs(parent)
pos = 784
for i in xrange(sequences_count):
(index, length, unkown1, unkown2) = \
struct.unpack("4i", pcx[pos:pos+16])
pos+=16
folder = parent
#create subfolder for defs with more than one sequence
if sequences_count > 1:
folder = os.path.join(parent, str(i))
if not os.path.exists(folder):
os.makedirs(folder)
pos+=13*length #read filenames
offsets = struct.unpack("%di"%length, pcx[pos:pos+4*length])
pos+=4*length
lengths = []
for j in xrange(length):
lengths.append(struct.unpack("i", pcx[offsets[j]:offsets[j]+4])[0])
for j in xrange(length):
data = pcx[offsets[j]:offsets[j]+32+lengths[j]]
save_frame(folder, j, data, palette, width, height, name)
elif ftype == 79: #msk file
print fid, name, ftype
save_file(fid, name, "msk", pcx)
elif ftype == 80: #fnt file
print fid, name, ftype
save_file(fid, name, "fonts", pcx)
elif ftype == 96: #pal file
print fid, name, ftype
save_file(fid, name, "palettes", pcx)
else:
raise Exception("type of %s not supported: %d"%(name, ftype))
lod.close()
def snd_extract(filename):
lod = open(filename)
(files_count,) = struct.unpack("i", lod.read(4))
print files_count
if not os.path.exists(os.path.join("data", "sounds")):
os.makedirs(os.path.join("data", "sounds"))
for fid in xrange(files_count):
lod.seek(4+(fid*48))
filename = '.'.join([lod.read(40).split('\x00', 1)[0], "wav"])
(offset, size) = struct.unpack("ii", lod.read(8))
lod.seek(offset)
wav = open(os.path.join("data", "sounds", filename), "w")
wav.write(lod.read(size))
wav.close()
lod.close()
def vid_extract(filename):
lod = open(filename)
(files_count,) = struct.unpack("i", lod.read(4))
print files_count
offsets = []
files = []
for fid in xrange(files_count):
lod.seek(4+(fid*44))
files.append(lod.read(40).rstrip('\x00'))
offsets.append(struct.unpack("i", lod.read(4))[0])
offsets.append(os.stat(filename)[6])
if not os.path.exists(os.path.join("data", "videos")):
os.makedirs(os.path.join("data", "videos"))
for i in xrange(len(offsets)-1):
print files[i]
lod.seek(offsets[i])
vid = open(os.path.join("data", "videos", files[i]), "w")
vid.write(lod.read(offsets[i+1]-offsets[i]))
vid.close()
lod.close()
def main(args):
path = "."
if len(args) > 1:
path = args[1]
lod_extract(os.path.join(path, "H3bitmap.lod"))
lod_extract(os.path.join(path, "H3ab_bmp.lod"))
lod_extract(os.path.join(path, "H3sprite.lod"))
lod_extract(os.path.join(path, "H3ab_spr.lod"))
snd_extract(os.path.join(path, "Heroes3.snd"))
snd_extract(os.path.join(path, "H3ab_ahd.snd"))
vid_extract(os.path.join(path, "VIDEO.VID"))
vid_extract(os.path.join(path, "H3ab_ahd.vid"))
if __name__ == '__main__':
sys.exit(main(sys.argv))