Skip to content

Instantly share code, notes, and snippets.

@tehbeard
Last active June 3, 2024 18:21
Show Gist options
  • Save tehbeard/dfe087536d3783f64bf1312340dc87e7 to your computer and use it in GitHub Desktop.
Save tehbeard/dfe087536d3783f64bf1312340dc87e7 to your computer and use it in GitHub Desktop.
Gamemaker 7.1/8 GEX decompiler
<!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