Last active
June 3, 2024 18:21
-
-
Save tehbeard/dfe087536d3783f64bf1312340dc87e7 to your computer and use it in GitHub Desktop.
Gamemaker 7.1/8 GEX decompiler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>GameMaker 7/8.1 GEX decompiler</title> | |
</head> | |
<body> | |
<h1>GameMaker 7/8.1 GEX decompiler</h1> | |
<p> | |
<strong>What the hell is this?</strong> | |
<br/> | |
I found a GameMaker 8.1 project of mine, and wanted to open it in Game Maker Studio 2.0.<br/> | |
You can't natively import <code>.gmk</code> files into GMS2. <br/> | |
I used <a href="http://lateralgm.org/">LateralGM</a> to get the project into a GMS1 compatible format. | |
<br/><br/> | |
Unfortunately this doesn't seem to handle extension packages. (A mechanism for reusable code)<br/> | |
And GMS1/2 doesn't handle <code>.gex</code> files.<br/> | |
So, I wrote this decompiler to get the code out of a <code>.gex</code> file so I could get the project to compile in GMS2. | |
</p> | |
<p> | |
<strong>References I used in building this.</strong><br/> | |
<a href="https://enigma-dev.org/docs/Wiki/GMKrypt">https://enigma-dev.org/docs/Wiki/GMKrypt</a><br/> | |
<a href="http://lateralgm.org/formats/gmkrypt1.html">http://lateralgm.org/formats/gmkrypt1.html</a><br/> | |
<a href="https://github.com/IsmAvatar/LateralGM">https://github.com/IsmAvatar/LateralGM</a><br/> | |
<a href="https://enigma-dev.org/docs/Wiki/GEX_format">https://enigma-dev.org/docs/Wiki/GEX_format</a><br/> | |
<a href="https://enigma-dev.org/docs/Wiki/GED_format">https://enigma-dev.org/docs/Wiki/GED_format</a><br/> | |
</p> | |
<p> | |
This madness is all <a href="https://develop.games/" target="_blank">Thor's fault.</a> | |
</p> | |
<hr/> | |
<input type="file" id="gex" accept=".gex" /> | |
<script type="module"> | |
// This code only exists because Thor is a meance of postivity towards making games. | |
// GMKrypyt code | |
// https://enigma-dev.org/docs/Wiki/GMKrypt | |
// http://lateralgm.org/formats/gmkrypt1.html | |
function generateSwapTable(seed) { | |
const table = [[], []]; // int[][] table = new int[2][256]; | |
const a = 6 + (seed % 250); | |
const b = Math.floor(seed / 250); | |
for (let i = 0; i < 256; i++) { | |
table[0][i] = i; | |
} | |
for (let i = 1; i < 10001; i++) { | |
const j = 1 + ((i * a + b) % 254); | |
const t = table[0][j & 0xff]; | |
table[0][j] = table[0][j + 1] & 0xff; | |
table[0][j + 1] = t & 0xff; | |
} | |
table[1][0] = 0; //this operation is optional, as 0 is default | |
for (let i = 1; i < 256; i++) { | |
table[1][table[0][i]] = i & 0xff; | |
} | |
return table; | |
} | |
export function decryptGMKrypt(seed, data) { | |
const lookup = generateSwapTable(seed); | |
return data.map((byte) => lookup[1][byte] & 0xff); | |
} | |
// GEX File format decompiler | |
// https://github.com/IsmAvatar/LateralGM | |
// https://enigma-dev.org/docs/Wiki/GED_format#cite_note-extdef-1 | |
export async function decompile(data) { | |
const view = new DataView(data.buffer); | |
const formatId = view.getInt32(0, true); | |
const version = view.getInt32(4, true); | |
// const seedPre = view.getInt32(8, true); | |
// const seedPost = view.getInt32(12, true); | |
const seed = view.getInt32(8, true); | |
const plaintext = decryptGMKrypt(seed, data.slice(12)); | |
// Parse the GED | |
const txtDec = new TextDecoder(); | |
const plainView = new DataView(plaintext.buffer); | |
function readPrefixedString() { | |
const size = plainView.getUint32(gedOffset, true); | |
const value = txtDec.decode( | |
plaintext.subarray(gedOffset + 4, gedOffset + 4 + size) | |
); | |
gedOffset += 4 + size; | |
return value; | |
} | |
function readUInt32() { | |
const value = plainView.getUint32(gedOffset, true); | |
gedOffset += 4; | |
return value; | |
} | |
let gedOffset = 0; | |
const gedVersion = readUInt32(); | |
const editable = readUInt32(); | |
const labels = [ | |
"Name", | |
"Folder", | |
"Version", | |
"Author", | |
"Date", | |
"License", | |
"Description", | |
"Help File", | |
]; | |
const metadata = {}; | |
for (const label of labels) { | |
const value = readPrefixedString(); | |
metadata[label] = value; | |
} | |
const hidden = readUInt32(); // Skip hidden | |
// Uses | |
const usesCount = readUInt32(); | |
const uses = []; | |
for (let i = 0; i < usesCount; i++) { | |
uses.push(readPrefixedString()); | |
} | |
// Files | |
const filesCount = readUInt32(); | |
const files = []; | |
const fileTypes = ["N/A", "DLL", "gml", "Action Library", "Other"]; | |
const callCon = { 2: "GML", 11: "stdCall", 12: "cdecl" }; | |
for (let i = 0; i < filesCount; i++) { | |
const version = readUInt32(); | |
const fileName = readPrefixedString(); | |
const originalName = readPrefixedString(); | |
const kind = fileTypes[readUInt32()]; | |
const initialization = readPrefixedString(); | |
const finalization = readPrefixedString(); | |
const functionCount = readUInt32(); | |
const functions = []; | |
for (let i = 0; i < functionCount; i++) { | |
const version = readUInt32(); | |
const name = readPrefixedString(); | |
const ext_name = readPrefixedString(); | |
const call_con = callCon[readUInt32()]; | |
const helpLine = readPrefixedString(); | |
const hidden = readUInt32(); | |
const argCount = readUInt32(); | |
gedOffset += 4 * 18; | |
functions.push({ | |
version, | |
name, | |
ext_name, | |
call_con, | |
helpLine, | |
hidden, | |
argCount, | |
}); | |
} | |
const constantCount = readUInt32(); | |
const constants = []; | |
for (let i = 0; i < constantCount; i++) { | |
const version = readUInt32(); | |
const name = readPrefixedString(); | |
const value = readPrefixedString(); | |
const hidden = readUInt32(); | |
constants.push({ | |
version, | |
name, | |
value, | |
hidden, | |
}); | |
} | |
files.push({ | |
version, | |
fileName, | |
originalName, | |
kind, | |
initialization, | |
finalization, | |
functions, | |
constants, | |
}); | |
} | |
async function readCompressedDat() | |
{ | |
const datSize = readUInt32(); | |
const datCompressed = plaintext.slice(gedOffset, gedOffset + datSize); | |
gedOffset += datSize; | |
const unzip = new DecompressionStream("deflate"); | |
const writer = unzip.writable.getWriter(); | |
writer.write(datCompressed); | |
writer.releaseLock(); | |
const reader = unzip.readable.getReader(); | |
const read = await reader.read(); | |
return read.value; | |
} | |
for(let file of files) | |
{ | |
file.data = await readCompressedDat(); | |
file.dataText = txtDec.decode(file.data); | |
} | |
console.log({ | |
gedVersion, | |
editable, | |
hidden, | |
metadata, | |
files | |
}); | |
// const datSeed = readUInt32(); | |
// console.log("datSeed:", datSeed); | |
// console.log( gedOffset + datSize , plaintext.length); | |
// console.log(unzip.length); | |
return { | |
gedVersion, | |
editable, | |
metadata, | |
files | |
}; | |
} | |
// UI CODE | |
document.querySelector("#gex").addEventListener("change", async (ev) => { | |
if (ev.target.files.length === 1) { | |
const fileContents = new Uint8Array( | |
await ev.target.files[0].arrayBuffer() | |
); | |
const gex = await decompile(fileContents); | |
console.log({ gex }); | |
document.querySelector("#metadata").innerText = JSON.stringify( | |
gex, | |
(k, v) => { | |
if(k === "dataText") | |
{ | |
return undefined; | |
} | |
return (v instanceof Uint8Array) ? "Uint8Array(" + v.length + ")" : v | |
}, | |
2 | |
); | |
document.querySelector("#files").innerHTML = ""; | |
for(const file of gex.files) | |
{ | |
const node = document.querySelector("#dat-file").content.cloneNode(true) | |
node.querySelector("h4").innerText = file.fileName; | |
node.querySelector("textarea").value = file.dataText; | |
node.querySelector("a").href = URL.createObjectURL( new Blob([file.data])); | |
node.querySelector("a").innerText = "Download " + file.fileName; | |
document.querySelector("#files").appendChild(node); | |
} | |
} | |
}); | |
</script> | |
<template id="dat-file"> | |
<div> | |
<h4></h4> | |
<a href="#" download>Download File</a><br/> | |
<textarea rows="40" cols="80"></textarea> | |
</div> | |
</template> | |
<h2>Metadata</h2> | |
<pre id="metadata"></pre> | |
<h3>DAT file contents</h3> | |
<div id="files"> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment