Last active
December 8, 2022 19:39
-
-
Save nommiin/2fac97812dd20b70fa343b9d4bb6b846 to your computer and use it in GitHub Desktop.
A terribly hacky script that somehow makes a weird like builder pattern class thing for Imguigml
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
| /* | |
| This is too much work: | |
| - Here's a link to the generated script using default settings, targeting extImGuiGML v1.100.4 (GM Beta v2022.1100.0.259) | |
| https://gist.github.com/nommiin/bce79d83e6e2d135df11328808105e82 | |
| How to use: | |
| 1. Create a main.js in your project root (near .yyp) and copy all this into it | |
| 1a.) Feel free to modify any of the consts at the top, there's a few settings | |
| 2. Run "node main.js" | |
| 3. Open newly created "Imgui.gml" and copy into GameMaker | |
| 4. Create an instance of the Imgui class and have at it! | |
| Imgui Class: | |
| - Constructor takes "cache", which should be a struct containing variables & defaults | |
| - The .End() method returns the cached variables struct (rather than self), so it can't be chained and is expected to be used last | |
| - You can call this once you're done rendering your GUI, then access the struct to get the user's inputs | |
| - Functions with single return values are pushed onto the stack | |
| - Use .Discard to get rid of the return value | |
| - Use .Bind(name) to assign the returned value to "name" | |
| - Use .If(callback) to check if the returned value is true | |
| - Functions that return an array containing a changed variable now take a variable name instead of value | |
| - For example: `.InputText("Name", "my_name", 32)` will use & update the "my_name" variable | |
| - NOTE: These functions do not push to the stack, so do not use the .Bind method | |
| - WARNING: Passing unset variables won't crash, but lead to undefined behaviour; so be sure to set variables beforehand in the constructor! | |
| - .Bind(name) can be used to assign the popped stack value to "name" | |
| - .Get(?name) can be used to retrieved variables from the cache (or the cache struct itself if no argument is given), returns the value of "name" | |
| - .Set(name, val) can be used to assign a given value to a variable in the cache, returns self | |
| - .If(callback) can be used to run the callback if the popped stack value is true | |
| - .If(bool, callback) can be used to run the callback if "bool" is true | |
| - .If(name, callback) can be used to run the callback if the variable "name" is true | |
| - NOTE: If USE_PASCAL is false, this function will be named "_if" to avoid keyword conflicts | |
| - .For(arr, callback(element, index)) can be used to loop through an array and run a callback for each item | |
| - NOTE: If USE_PASCAL is false, this function will be named "_for" to avoid keyword conflicts | |
| Example: | |
| ---------------------------------------- | |
| // Create Event | |
| gui = new Imgui({my_name: "Nomm", my_age: 21, is_hungry: true, close: false}); | |
| // Step Event | |
| var data = gui | |
| .Begin("Questionnaire") | |
| .InputText("Name", "my_name", 16) | |
| .InputInt("Age", "my_age") | |
| .Checkbox("Hungry", "is_hungry") | |
| .Button("Close Game").Bind(close) | |
| .End(); | |
| if (data.close) { | |
| show_message(string("Goodbye, {0}!", gui.Get("my_name"))); | |
| game_end(); | |
| } | |
| ---------------------------------------- | |
| Updates: | |
| - 12/8/2022: Added .For method, .Get now returns __Cache if no name is given, updated INCLUDE_JSDOC to work for non-generated methods | |
| - 12/8/2022.2: Added USE_PASCAL const, fixed a typo lol | |
| - 12/8/2022.3: Added link to generated script | |
| - 12/8/2022.4: Cleaned up standard function generation, added .If method. .For and .If now scope to the calling class | |
| - 12/8/2022.5: Reworked how .Bind works, got rid of __Binds & CHECK_BINDS; now everything goes into the stack! Reread the Imgui Class section for more info | |
| ** by Nommiin, 2022 :3 ** | |
| */ | |
| const fs = require("node:fs"), path = require("node:path"), Tab = " "; | |
| // The name of the generated class, also used for output filename (CLASS_NAME.gml) | |
| const CLASS_NAME = "Imgui"; | |
| // If enabled, generated methods will have JSDoc tags | |
| const INCLUDE_JSDOC = true; | |
| // Character used for tabs in generated script, Tag.Build method uses TAB_CHAR.repeat | |
| const TAB_CHAR = " "; | |
| // If enabled, method names will be formatted in PascalCase | |
| // If disabled, the base function name is used (ie: .get_cursor_pos instead of .GetCursorPos) | |
| const USE_PASCAL = true; | |
| class Tag { | |
| /** | |
| * @param {string} line | |
| */ | |
| constructor(line) { | |
| // Type | |
| const ampersand = line.indexOf("@"); | |
| if (ampersand === -1) { | |
| throw `Could not find start of tag, expected "@" (ampersand)`; | |
| } | |
| const space = line.indexOf(" ", ampersand); | |
| if (space === -1) { | |
| throw `Could not find tag type definition, expected " " (space)`; | |
| } | |
| this.Type = line.slice(ampersand + 1, space); | |
| if (this.Type === "return") this.Type = "returns"; | |
| // Datatype | |
| let brace = line.indexOf("{", space), ending = space; | |
| if (brace > -1) { | |
| const close = line.indexOf("}", brace); | |
| if (close === -1) { | |
| if (this.Type !== "returns") { | |
| throw `Could not find closing brace for datatype`; | |
| } | |
| this.Datatype = line.slice(brace + 1).trim(); | |
| console.log(`[WARNING] Malformed "returns" tag met, salvaging type: "${this.Datatype}"`); | |
| } else { | |
| this.Datatype = line.slice(brace + 1, close).trim(); | |
| ending = close; | |
| } | |
| } | |
| // Value | |
| switch (this.Type) { | |
| case "function": { | |
| const start = line.indexOf("(", space); | |
| if (start === -1) { | |
| throw `Could not find start of function signature, expected "(" (left-parentheses)`; | |
| } | |
| this.Name = line.slice(space + 1, start); | |
| break; | |
| } | |
| case "desc": { | |
| this.Value = line.slice(space).trim(); | |
| break; | |
| } | |
| case "param": { | |
| let val = line.slice(ending + 1).trim(), ind = val.indexOf(" "), name = undefined; | |
| if (ind > -1) { | |
| name = val.slice(0, ind); | |
| this.Description = val.slice(ind).trim(); | |
| } else { | |
| name = val; | |
| } | |
| if (!name.startsWith("[")) { | |
| this.Name = name; | |
| this.Optional = false; | |
| } else { | |
| const end = name.indexOf("]"); | |
| if (end > -1) { | |
| name = name.slice(1, -1); | |
| } else { | |
| name = name.slice(1); | |
| console.log(`[WARNING] Malformed "param" tag met, salvaging type: "${name}"`); | |
| } | |
| this.Optional = true; | |
| } | |
| const assign = name.indexOf("="); | |
| if (assign > -1) { | |
| this.Name = name.slice(0, assign); | |
| this.Default = name.slice(assign + 1); | |
| const size = this.Default.indexOf("sizeof"); | |
| if (size > -1) { | |
| const start = this.Default.indexOf("(", size), end = this.Default.indexOf(")", size); | |
| if (start > -1 && end > -1) { | |
| const size_type = this.Default.slice(start + 1, end); | |
| switch (size_type) { | |
| case "float": { | |
| this.Default = "4"; | |
| break; | |
| } | |
| default: { | |
| throw `Could not handle sizeof call for type: "${size_type}"`; | |
| } | |
| } | |
| } | |
| } | |
| const evil = ["f", "]", ")"]; | |
| while (evil.includes(this.Default[this.Default.length - 1])) { | |
| this.Default = this.Default.slice(0, -1); | |
| } | |
| } else { | |
| this.Name = name; | |
| } | |
| break; | |
| } | |
| case "returns": break; | |
| default: { | |
| throw `Could not handle unknown tag type: "${this.Type}"`; | |
| } | |
| } | |
| // Children | |
| this.Children = []; | |
| } | |
| get Name_Pretty() { | |
| if (USE_PASCAL) return this.Name.slice("imguigml_".length).split("_").filter(e => e.length > 0).map(e => e.charAt(0).toUpperCase() + e.slice(1)).join(""); | |
| return this.Name.slice("imguigml_".length); | |
| } | |
| Build() { | |
| // __imguigml_image & __imguigml_image_button | |
| if (this.Name.startsWith("__")) return ""; | |
| const name = this.Name_Pretty; | |
| const ret = this.Children.filter(e => e.Type === "returns").pop(); | |
| const desc = this.Children.filter(e => e.Type === "desc").pop(); | |
| const param = this.Children.filter(e => e.Type === "param"); | |
| // Binds | |
| const binds = []; | |
| if (ret && ret.Datatype) { | |
| const type = ret.Datatype; | |
| if (type.startsWith("Array:")) { | |
| let types = type.slice("Array:".length).trim(); | |
| if (types.startsWith("[")) types = types.slice(1); | |
| if (types.endsWith("]")) types = types.slice(0, -1); | |
| types = types.split(",").map(e => e.trim()); | |
| if (types[0] == "_changed") { | |
| for(let i = 0; i < param.length; i++) { | |
| const arg = param[i], ind = types.indexOf(arg.Name); | |
| if (ind > -1) { | |
| binds[i] = ind - 1; | |
| } else { | |
| binds[i] = -1; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // JSDoc | |
| let str = ""; | |
| if (INCLUDE_JSDOC) { | |
| str = TAB_CHAR.repeat(1) + `/// @function ${name}(${param.map(e => e.Name).join(", ")})\n`; | |
| if (desc) str += TAB_CHAR.repeat(1) + `/// @desc ${desc.Value}\n`; | |
| param.forEach(arg => { | |
| str += TAB_CHAR.repeat(1) + `/// @param ${(arg.Datatype ? "{" + arg.Datatype + "} " : "")}${arg.Name}${arg.Description ? " - " + arg.Description : ""}\n` | |
| }); | |
| } | |
| // Definition | |
| str += TAB_CHAR.repeat(1) + `static ${name} = function(${param.map(e => `${e.Name}${e.Default ? "=" + e.Default : ""}`).join(", ")}) {\n`; | |
| // Function Call | |
| str += TAB_CHAR.repeat(2) + `${ret ? "var _ret = " : ""}`; | |
| // Bound Arguments | |
| let has_bind = false; | |
| const args = param.map((e, ind) => { | |
| const bind = binds[ind]; | |
| if (bind > -1) { | |
| has_bind = true; | |
| return `self.__Cache[$ ${e.Name}]`; | |
| } else { | |
| return e.Name; | |
| } | |
| }).join(", "); | |
| str += `${this.Name}(${args});\n`; | |
| // Bind Caching | |
| if (has_bind) { | |
| str += TAB_CHAR.repeat(2) + `if (_ret[0]) {\n`; | |
| for(let i = 0; i < param.length; i++) { | |
| const bind = binds[i]; | |
| if (bind > -1) { | |
| str += TAB_CHAR.repeat(3) + `self.__Cache[$ ${param[i].Name}] = _ret[${i}];\n`; | |
| } | |
| } | |
| str += TAB_CHAR.repeat(2) + `}\n`; | |
| } else { | |
| if (ret) { | |
| str += TAB_CHAR.repeat(2) + `array_push(self.__Stack, _ret);\n`; | |
| } | |
| } | |
| // imguigml_end bonus! | |
| if (this.Name == "imguigml_end") { | |
| // GOOFY! | |
| str += TAB_CHAR.repeat(2) + `self.__Stack = [];\n`; | |
| str += TAB_CHAR.repeat(2) + `return self.__Cache;\n${TAB_CHAR.repeat(1)}}\n`; | |
| return str; | |
| } | |
| str += TAB_CHAR.repeat(2) + `return self;\n${TAB_CHAR.repeat(1)}}\n`; | |
| return str; | |
| } | |
| } | |
| try { | |
| const begin = performance.now(); | |
| (function(files) { | |
| let str = ` | |
| function ${CLASS_NAME}(cache) constructor { | |
| self.__Cache = cache ?? {}; | |
| self.__Stack = []; | |
| ${!INCLUDE_JSDOC ? "" : ` | |
| /// @function ${USE_PASCAL ? "B" : "b"}ind([_name=undefined]) | |
| /// @desc Assigns the given variable to the latest value on the stack | |
| /// @param {string|undefined} _name - The name of the variable to assign to | |
| `.trimEnd()} | |
| static ${USE_PASCAL ? "B" : "b"}ind = function(_name) { | |
| self.__Cache[$ _name] = array_pop(self.__Stack); | |
| return self; | |
| } | |
| ${!INCLUDE_JSDOC ? "" : ` | |
| /// @function ${USE_PASCAL ? "G" : "g"}et([_name=undefined]) | |
| /// @desc Retrieves the given variable from the cache | |
| /// @param {string|undefined} _name - The name of the variable to retrieve | |
| /// @returns {any|struct} - The value of the variable, if _name is not provided this will be the .__Cache struct | |
| `.trimEnd()} | |
| static ${USE_PASCAL ? "G" : "g"}et = function(_name) { | |
| return (_name != undefined ? self.__Cache[$ _name] : self.__Cache); | |
| } | |
| ${!INCLUDE_JSDOC ? "" : ` | |
| /// @function ${USE_PASCAL ? "S" : "s"}et(_name, _val) | |
| /// @desc Assigns the given value to the variable in cache | |
| /// @param {string} _name - The name of the variable to set | |
| /// @param {any} _val - The value to assign to _name | |
| `.trimEnd()} | |
| static ${USE_PASCAL ? "S" : "s"}et = function(_name, _val) { | |
| self.__Cache[$ _name] = _val; | |
| return self; | |
| } | |
| ${!INCLUDE_JSDOC ? "" : ` | |
| /// @function ${USE_PASCAL ? "F" : "_f"}or(_arr, _callback(element, index)) | |
| /// @desc Runs the given callback for each element in the array | |
| /// @param {Array} _arr - The array to iterate over | |
| /// @param {Function} _callback - The callback to run for each element in the array | |
| `.trimEnd()} | |
| static ${USE_PASCAL ? "F" : "_f"}or = function(_arr, _callback) { | |
| var _i = array_length(_arr); | |
| if (_i > 0) { | |
| var _bind = method(self, method_get_index(_callback)); | |
| for(var i = 0; i < _i; i++) { | |
| _bind(_arr[i], _i); | |
| } | |
| } | |
| return self; | |
| } | |
| ${!INCLUDE_JSDOC ? "" : ` | |
| /// @function ${USE_PASCAL ? "I" : "_i"}f(_val, _callback()) | |
| /// @desc Runs the callback if _val is equal to true | |
| /// @param {Boolean|String} _val - The value to check, can also be a cached variable | |
| /// @param {Function} _callback - The callback to run if _val is true | |
| `.trimEnd()} | |
| static ${USE_PASCAL ? "I" : "_i"}f = function(_val, _callback) { | |
| if (typeof(_val) == "method") { | |
| var _get = array_pop(self.__Stack); | |
| if (_get) { | |
| method(self, method_get_index(_val))(); | |
| } | |
| return self; | |
| } | |
| if (!is_string(_val)) { | |
| if (_val) { | |
| method(self, method_get_index(_callback))(); | |
| } | |
| } else { | |
| var _get = self.__Cache[$ _val]; | |
| if (_get != undefined && _get) { | |
| method(self, method_get_index(_callback))(); | |
| } | |
| } | |
| return self; | |
| } | |
| ${!INCLUDE_JSDOC ? "" : ` | |
| /// @function ${USE_PASCAL ? "D" : "d"}iscard() | |
| /// @desc Pops and discards the most recent stack value | |
| `.trimEnd()} | |
| static ${USE_PASCAL ? "D" : "d"}iscard = function() { | |
| array_pop(self.__Stack); | |
| return self; | |
| } | |
| `.trimStart().split("\n").map(e => { | |
| let tabs = 0; | |
| for(let i = 0; i < e.length; i++) { | |
| if (e.charCodeAt(i) > 32) { | |
| tabs = ((i / 4) - 4) + 1; | |
| break; | |
| } | |
| } | |
| return TAB_CHAR.repeat(Math.max(tabs, 0)) + e.trimStart(); | |
| }).join("\n") + "\n"; | |
| files.forEach((file, ind) => { | |
| if (!fs.existsSync(file)) throw `Could not find file: ${file}`; | |
| console.log(`[${ind}]: ${file}`); | |
| const content = fs.readFileSync(file, {encoding: "utf-8"}).split("\n"), functions = []; | |
| for(let i = 0; i < content.length; i++) { | |
| const line = content[i].trim(); | |
| try { | |
| const tag = new Tag(line); | |
| if (tag.Type == "function") { | |
| for(let j = i + 1; j < content.length; j++) { | |
| const read = content[j].trim(); | |
| if (read.length < 4) { | |
| continue; | |
| } | |
| if (read.startsWith("///") && read.slice(3).trim().startsWith("@")) { | |
| try { | |
| const child = new Tag(read); | |
| tag.Children.push(child); | |
| } catch (e) { | |
| console.log(`[ERROR] Failed to parse line ${j + 1}: ${e}`); | |
| } | |
| continue; | |
| } | |
| i = j; | |
| break; | |
| } | |
| functions.push(tag); | |
| } | |
| } catch (e) { | |
| // do nothing | |
| } | |
| } | |
| console.log(`read ${content.length} lines, parsed ${functions.length} functions`); | |
| if (functions.length === 0) { | |
| return; | |
| } | |
| for(var i = 0; i < functions.length; i++) { | |
| const line = functions[i].Build(); | |
| if (line.length > 0) { | |
| str += line + "\n"; | |
| } | |
| } | |
| }); | |
| fs.writeFileSync(CLASS_NAME + ".gml", str + "}"); | |
| })(["scripts/imguigml_wrapper/imguigml_wrapper.gml", "scripts/imguigml_wrapper_widget/imguigml_wrapper_widget.gml"]); | |
| console.log(`elapsed time: ${Math.round(performance.now() - begin)}ms`); | |
| process.exit(0); | |
| } catch (e) { | |
| console.error(`An error has occured:\n- ${e}`); | |
| process.exit(1); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment