Skip to content

Instantly share code, notes, and snippets.

@BenjaminUrquhart
Created November 14, 2025 09:28
Show Gist options
  • Select an option

  • Save BenjaminUrquhart/fd3eda0037af189080c19f89aef8ea80 to your computer and use it in GitHub Desktop.

Select an option

Save BenjaminUrquhart/fd3eda0037af189080c19f89aef8ea80 to your computer and use it in GitHub Desktop.
A Gamemaker script to parse and retrieve detailed extension information from the game WAD at runtime.
#macro GMEE_PRELOAD true
// This script references UndertaleModTool for data file parsing
// https://github.com/UnderminersTeam/UndertaleModTool/blob/master/UndertaleModLib/Models/UndertaleExtension.cs
/// Parses a Gamemaker game WAD for extension information
///
/// @param [filepath] the path to the WAD. If not provided, one is guessed based on platform/command-line arguments.
///
/// @returns {Array<Struct>} an array containing extension information parsed from the WAD
function gmee_load_file(filepath = __gmee_get_wad_path()) {
static init = false
static has_version = false
static has_options = false
if !init {
// In case someone tries to use this in old versions.
// Untested.
try { has_version = is_callable(extension_get_version) } catch(e) {}
try { has_options = is_callable(extension_get_options) } catch(e) {}
init = true
}
if !file_exists(filepath) {
__gmee_throw($"File not found: {filepath}")
}
var buff = buffer_load(filepath)
try {
var header = buffer_read(buff, buffer_u32)
// "FORM" but little endian
if(header != 0x4d524f46) {
buffer_seek(buff, buffer_seek_start, 0)
__gmee_throw($"Expected FORM, got {__gmee_read_chunk_name(buff)}")
}
// +8 bytes because FORM and this size aren't counted
var size = buffer_read(buff, buffer_u32) + 8
if(size != buffer_get_size(buff)) {
__gmee_throw($"File size mismatch: file declares size {size} but is actually {buffer_get_size(buff)}")
}
while buffer_tell(buff) < size {
var chunk_name = __gmee_read_chunk_name(buff)
var len = buffer_read(buff, buffer_u32)
var pos = buffer_tell(buff)
if chunk_name == "EXTN" {
// Oh boy I hope you like nested callbacks
return __gmee_read_ptr_list(buff, method({ has_version, has_options }, function(buff) {
var ext = {}
ext.folder = __gmee_read_string_ptr(buff)
ext.name = __gmee_read_string_ptr(buff)
if has_version {
ext.version = __gmee_read_string_ptr(buff)
}
ext.class = __gmee_read_string_ptr(buff)
var files_ptr = undefined, options_ptr = undefined
if has_options {
files_ptr = buffer_read(buff, buffer_u32)
options_ptr = buffer_read(buff, buffer_u32)
buffer_seek(buff, buffer_seek_start, files_ptr)
}
ext.files = __gmee_read_ptr_list(buff, function(buff) {
var file = {}
file.filename = __gmee_read_string_ptr(buff)
file.cleanup = __gmee_read_string_ptr(buff)
file.init = __gmee_read_string_ptr(buff)
file.kind = buffer_read(buff, buffer_u32)
file.functions = __gmee_read_ptr_list(buff, function(buff) {
var func = {}
func.name = __gmee_read_string_ptr(buff)
func.id = buffer_read(buff, buffer_u32)
func.kind = buffer_read(buff, buffer_u32)
func.return_type = buffer_read(buff, buffer_u32)
func.external_name = __gmee_read_string_ptr(buff)
var len = buffer_read(buff, buffer_u32)
func.arguments = array_create(len, undefined)
for(var i = 0; i < len; i++) {
func.arguments[@ i] = buffer_read(buff, buffer_u32)
}
// Convenience field
// self is bound to the function definition so it is easily
// retrievable if this is just floating somewhere.
func.func = method(func, func.id + 500000)
return func
})
return file
})
if has_options {
buffer_seek(buff, buffer_seek_start, options_ptr)
ext.options = __gmee_read_ptr_list(buff, function(buff) {
var option = {}
option.name = __gmee_read_string_ptr(buff)
option.value = __gmee_read_string_ptr(buff)
option.kind = buffer_read(buff, buffer_u32)
return option
})
}
return ext
}))
}
buffer_seek(buff, buffer_seek_start, pos + len)
}
// Projects with no extensions still have this chunk
__gmee_throw("Failed to locate EXTN chunk - this should never happen")
}
finally {
buffer_delete(buff)
}
}
/// Returns an array containing extension information parsed from the currently-running game WAD.
///
/// @returns {Array<Struct>}
function extension_get_all() {
static info = gmee_load_file()
return info
}
/// Returns a struct containing extension information parsed from the currently-running game WAD.
///
/// @returns {Struct}
function extension_get_all_struct() {
static map = undefined
if is_undefined(map) {
map = {}
var info = extension_get_all()
var len = array_length(info)
for(var i = 0; i < len; i++) {
var ext = info[i]
map[$ ext.name] = ext
}
}
return map
}
/// Returns an array containing the names of all extensions in the currently-running game WAD.
///
/// @returns {Array<String>}
function extension_get_names() {
return struct_get_names(extension_get_all_struct())
}
/// Returns a struct containing information about the requested extension, or undefined if the extension does not exist.
///
/// @param index_or_name The name or index of the extension to get information about.
///
/// @returns {Struct}
function extension_get_info(index_or_name) {
if is_string(index_or_name) {
return extension_get_all_struct()[$ index_or_name]
}
else {
var info = extension_get_all()
var len = array_length(info)
return index_or_name < 0 || index_or_name >= len ? undefined : info[index_or_name]
}
}
/// Returns an array containing every function defined by the requested extension.
/// Function details are available by using method_get_self on the returned functions.
///
/// @param index_or_name The name or index of the extension to get functions from.
///
/// @returns {Array<Function>}
function extension_get_functions(index_or_name) {
var info = extension_get_info(index_or_name)
if !is_undefined(info) {
var out = []
var files = info.files
var len = array_length(files)
for(var i = 0; i < len; i++) {
var functions = files[i].functions
var num = array_length(functions)
for(var j = 0; j < num; j++) {
array_push(out, functions[j].func)
}
}
return out
}
}
// ! --------------------- Internals --------------------- ! //
function __gmee_find_wad() {
if os_browser != browser_not_a_browser {
__gmee_throw("Cannot find data file on a platform that doesn't use one")
}
for(var i = 0; i < parameter_count(); i++) {
if(parameter_string(i) == "-game") {
return parameter_string(i + 1)
}
}
switch os_type {
case os_windows: return "data.win"
case os_macosx:
case os_ios: return "game.ios"
case os_gxgames:
case os_linux: return "game.unx"
case os_android: return "game.droid"
// man idk
default: return "game.win"
}
}
function __gmee_get_wad_path() {
static path = __gmee_find_wad()
return path
}
function __gmee_throw(msg) {
throw {
message: string(msg),
stacktrace: debug_get_callstack()
}
}
// can be optimized but whatever
function __gmee_read_chunk_name(buff) {
var out = ""
repeat (4) {
out += chr(buffer_read(buff, buffer_u8))
}
return out
}
function __gmee_read_string_ptr(buff) {
var pointer = buffer_read(buff, buffer_u32)
return pointer ? buffer_peek(buff, pointer, buffer_string) : undefined
}
function __gmee_read_ptr_list(buff, callback) {
var count = buffer_read(buff, buffer_u32)
var out = array_create(count, undefined)
for(var i = 0; i < count; i++) {
var ret = buffer_tell(buff) + 4
buffer_seek(buff, buffer_seek_start, buffer_read(buff, buffer_u32))
out[i] = callback(buff)
buffer_seek(buff, buffer_seek_start, ret)
}
return out
}
// Preload data if wanted
if GMEE_PRELOAD {
extension_get_all()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment