Created
November 14, 2025 09:28
-
-
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.
This file contains hidden or 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
| #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