Skip to content

Instantly share code, notes, and snippets.

@windwakr
Last active July 24, 2023 12:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save windwakr/b7279f80a846dec97e11d7f966834acb to your computer and use it in GitHub Desktop.
Save windwakr/b7279f80a846dec97e11d7f966834acb to your computer and use it in GitHub Desktop.
Game Maker 4.1/4.2/4.3 decompiler
#Game Maker 4.X decompiler
#for python 2.7 :^)
#Only tested on ~20 files
#
#As far as I can tell, there are no tools out there that actually support GM4(even if they claim to).
#Unlike later versions, image data is stored unencrypted. So we need to partially parse the GMD.
import struct
import io
import os
import sys
import string
GMDSTART = 0
KEYADDR = 0
VERSION = 0
def detectVersion(fh):
global GMDSTART, KEYADDR, VERSION
tmp = fh.read()
if tmp.find('Game_Maker 4.1') != -1:
KEYADDR = 0x10C8E0
GMDSTART = 0x10C8E4
VERSION = 410
elif tmp.find('Game_Maker 4.2') != -1:
KEYADDR = 0x10C8E0
GMDSTART = 0x10C8E4
VERSION = 420
elif tmp.find('Game_Maker 4.3') != -1:
KEYADDR = 0x124F80
GMDSTART = 0x124F84
VERSION = 430
else:
print 'Not a Game Maker 4.1/4.2/4.3 file'
raise SystemExit()
def readDword(fh):
return struct.unpack('<I', fh.read(4))[0]
def writeDword(fh, val):
fh.write(struct.pack('<I', val))
def readDataWithLen(fh):
length = readDword(fh)
return fh.read(length)
def transferImageData(inf, outf): #transfer unencrypted image data from inf to outf
outfoffset = outf.tell()
inf.seek(GMDSTART + outfoffset)
tmp = io.BytesIO()
type = readDword(inf)
writeDword(tmp, type)
if type == 0: #plain BMP? Can anything else be here?
tmp.write(inf.read(2)) #'BM'
length = readDword(inf) #length of whole BMP
writeDword(tmp, length)
tmp.write(inf.read(length-6))
elif type == 1:
width = readDword(inf)
writeDword(tmp, width)
height = readDword(inf)
writeDword(tmp, height)
for _ in xrange(height):
length = readDword(inf)
writeDword(tmp, length)
tmp.write(inf.read(length))
tmp.write(inf.read(length * 3))
elif type == 2:
length = readDword(inf)
writeDword(tmp, length)
tmp.write(inf.read(length * 3))
width = readDword(inf)
writeDword(tmp, width)
height = readDword(inf)
writeDword(tmp, height)
for _ in xrange(height):
length = readDword(inf)
writeDword(tmp, length)
tmp.write(inf.read(length * 2))
elif type == 0xFFFFFFFF: #empty
pass #nothing else to do here
else:
print 'Invalid image type?'
raise SystemExit()
outf.write(tmp.getvalue())
#This function based off code at http://ismavatar.com/lgm/formats/gmkrypt1.html
def generateSwapTable(seed):
table = (range(0, 256), range(0, 256))
for i in range(1, 10001):
j = 1 + ((i * seed) % 254)
table[0][j], table[0][j + 1] = table[0][j + 1], table[0][j]
for i in range(1, 256):
table[1][table[0][i]] = i
return table
def usage():
print 'Usage:\n\t%s filetodecompile.exe' % (os.path.basename(sys.argv[0]))
raise SystemExit()
if len(sys.argv) < 2:
usage()
if os.path.exists(sys.argv[1]) == False:
usage()
out = io.BytesIO()
with open(sys.argv[1], 'rb') as f:
detectVersion(f)
f.seek(KEYADDR)
key = readDword(f)
table = generateSwapTable(key)[1]
#decrypt the gmd
#in GM4 the encryption is just a simple substitution cipher, so we'll use python's string translation functions to speed it up
table = string.maketrans(''.join([chr(x) for x in range(0,256)]), ''.join([chr(x) for x in table]))
out.write(f.read().translate(table))
#start parsing the gmd looking for image data
#unique ID, stored unencrypted
out.seek(0x10)
f.seek(GMDSTART + 0x10)
out.write(f.read(0x10))
#parse options
out.read(0x3C)
if VERSION >= 420:
out.read(0x10)
if readDword(out) > 0:
transferImageData(f, out)
out.read(0x0C)
readDataWithLen(out)
out.read(0x14)
if readDword(out) == 2:
transferImageData(f, out)
transferImageData(f, out)
if readDword(out) > 0:
transferImageData(f, out)
readDataWithLen(out)
if VERSION >= 420:
out.read(0x10)
#parse sounds, just passing through
if readDword(out) != 400:
print 'Error reading sound data'
raise SystemExit()
numsounds = readDword(out)
for _ in xrange(numsounds):
exists = readDword(out)
if exists:
readDataWithLen(out)
if readDword(out) != 400:
print 'Error reading sound data'
raise SystemExit()
sndtype = readDword(out)
readDataWithLen(out)
if sndtype != 0xFFFFFFFF:
readDataWithLen(out)
out.read(0x0C)
#parse sprites
if readDword(out) != 400:
print 'Error reading sprite data'
raise SystemExit()
numsprites = readDword(out)
for _ in xrange(numsprites):
exists = readDword(out)
if exists:
readDataWithLen(out)
if readDword(out) != 400:
print 'Error reading sprite data'
raise SystemExit()
out.read(0x34)
numframes = readDword(out)
for _ in xrange(numframes):
transferImageData(f, out)
#parse backgrounds
if readDword(out) != 400:
print 'Error reading background data'
raise SystemExit()
numbackgrounds = readDword(out)
for _ in xrange(numbackgrounds):
exists = readDword(out)
if exists:
readDataWithLen(out)
if readDword(out) != 400:
print 'Error reading background data'
raise SystemExit()
out.read(0x14)
imgexists = readDword(out)
if imgexists:
transferImageData(f, out)
#the rest of the file should all be encrypted, so we can stop parsing here
with open('%s_dec.gmd' % (os.path.splitext(os.path.basename(sys.argv[1]))[0]), 'wb') as f:
f.write(out.getvalue())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment