Skip to content

Instantly share code, notes, and snippets.

@nommiin
Last active December 8, 2022 19:39
Show Gist options
  • Select an option

  • Save nommiin/2fac97812dd20b70fa343b9d4bb6b846 to your computer and use it in GitHub Desktop.

Select an option

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 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