commit 6c2ddef1bb4940a6d32e01665d4cd7cb8f4489c6 Author: Synthasmagoria Date: Fri Apr 3 21:56:27 2026 +0200 Initial commit diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..0ee418f --- /dev/null +++ b/main.odin @@ -0,0 +1,314 @@ +package regenerate_extension + +import "core:os" +import "core:fmt" +import "core:encoding/json" +import "core:odin/parser" +import "core:odin/ast" +import "core:strings" +import "core:mem/virtual" +import "core:mem" +import "core:time" + +arena: virtual.Arena + +main :: proc() { + arena_err := virtual.arena_init_growing(&arena) + if (arena_err != nil) { + fmt.println(arena_err) + } + context.allocator = virtual.arena_allocator(&arena) + + time_start := time.now() + + switch len(os.args) { + case 0, 1, 2: + fmt.println("--- Regenerate GameMaker Extension Bindings ---") + fmt.println("Usage: ") + fmt.println("Note: Link to existing extension in order to update its contents") + case 3: + err := program(module_path = os.args[1], gamemaker_extension_path = os.args[2]) + if (err != nil) { + fmt.println("Error: Finished running with the following error:", err) + return + } + case: + fmt.println("Error: Too many arguments") + } + + time_total := time.to_unix_nanoseconds(time.now()) - time.to_unix_nanoseconds(time_start) + fmt.println("Finished in", time_total / 1000, "ms") + kb_used := arena.total_used / mem.Kilobyte + kb_reserved := arena.total_reserved / mem.Kilobyte + fmt.println("Used: ", kb_used, "kb / ", kb_reserved, "kb of memory (growing)", sep = "") +} + +GAMEMAKER_EXTENSION_RESOURCE_VERSION :: "2.0" +GAMEMAKER_RESOURCE_JSON5_MARSHAL_OPTIONS :: json.Marshal_Options { + spec = .JSON5, + pretty = true, + sort_maps_by_key = true, + indentation = 0, + spaces = 4, + use_spaces = true, +} + +@rodata string_type_string := "string" +@rodata double_type_string := "double" +@rodata empty_type_string := "" +@rodata empty_string := "" +@rodata gmextension_function_resource_type := "GMExtensionFunction" +@rodata gmextension_function_resource_version := "2.0" + +Proc :: struct { + name: string, + params: [dynamic]Param, + result_type_name: string, + result: GameMakerParamType +} + +GameMakerParamType :: enum i64 { + None = 0, + String = 1, + Double = 2, +} + +Param :: struct { + name: string, + type_name: string, + type: GameMakerParamType +} + +GameMakerResourceError :: enum { + InvalidVersion, +} + +Error :: union { + os.Error, + json.Error, + json.Marshal_Error, + GameMakerResourceError, +} + +program :: proc(module_path, gamemaker_extension_path: string) -> Error { + resource := gamemaker_resource_read(gamemaker_extension_path) or_return + + resource_version := gamemaker_resource_get_version(resource); + if (resource_version != GAMEMAKER_EXTENSION_RESOURCE_VERSION) { + fmt.println("Error: Found extension resource version ", resource_version, " expected ", GAMEMAKER_EXTENSION_RESOURCE_VERSION) + return GameMakerResourceError.InvalidVersion + } + + procs := make([dynamic]Proc) + walker := os.walker_create(module_path) + for file, ok := os.walker_walk(&walker); ok; file, ok = os.walker_walk(&walker) { + if file.type == .Directory { + walker.skip_dir = true + continue + } + if file.type != .Regular { + continue + } + if _, ext := os.split_filename(file.name); ext != "odin" { + continue + } + list := odin_file_get_exported_procs(file.fullpath) or_return + for item in list { + append(&procs, item) + } + } + + funcs := make(json.Array) + for procedure in procs { + append(&funcs, gamemaker_extension_function_create(procedure)) + } + + files := resource["files"].(json.Array) + for file in files { + object := file.(json.Object) + filename := object["filename"].(json.String) + _, filename_ext := os.split_filename(filename) + switch { + case filename_ext == "ext" || filename_ext == "so" || filename_ext == "dll": + case: + fmt.println("Invalid filename extension '", filename, "' skipping") + continue + } + object["functions"] = funcs + break + } + + gamemaker_resource_write(gamemaker_extension_path, resource) or_return + return nil +} + +string_builder_jsdoc_write_param :: proc(sb: ^strings.Builder, type, name: string) { + strings.write_string(sb, "///@param {") + strings.write_string(sb, type) + strings.write_string(sb, "} ") + strings.write_string(sb, name) + strings.write_string(sb, "\n") +} + +string_builder_jsdoc_write_return :: proc(sb: ^strings.Builder, type: string) { + strings.write_string(sb, "///@returns {") + strings.write_string(sb, type) + strings.write_string(sb, "}\n") +} + +gamemaker_extension_function_create :: proc(procedure: Proc) -> json.Object { + sb := strings.builder_make() + for param in procedure.params { + switch param.type { + case .None: panic("Invalid parameter type") + case .String: string_builder_jsdoc_write_param(&sb, "pointer", param.name) + case .Double: string_builder_jsdoc_write_param(&sb, "real", param.name) + } + } + switch procedure.result { + case .None: + case .String: string_builder_jsdoc_write_return(&sb, "pointer") + case .Double: string_builder_jsdoc_write_return(&sb, "real") + } + jsdoc := strings.string_from_ptr(raw_data(sb.buf), len(sb.buf)) + + function := make(json.Object) + function["$GMExtensionConstant"] = empty_string + function["%Name"] = procedure.name + function["argCount"] = json.Integer(0) + args := make(json.Array) + for param in procedure.params { + append(&args, json.Integer(param.type)) + } + function["args"] = args + function["documentation"] = jsdoc + function["externalName"] = procedure.name + function["help"] = empty_string + function["hidden"] = false + function["kind"] = json.Integer(1) + function["name"] = procedure.name + function["resourceType"] = gmextension_function_resource_type + function["resourceVersion"] = gmextension_function_resource_version + function["returnType"] = json.Integer(procedure.result) + return function +} + +gamemaker_resource_read :: proc(path: string) -> (object: json.Object, err: Error) { + data := os.read_entire_file(path, context.allocator) or_return + resource := json.parse(data, .JSON5, true) or_return + object = resource.(json.Object) + return +} + +gamemaker_resource_write :: proc(path: string, data: json.Value) -> Error { + str := json.marshal(data, GAMEMAKER_RESOURCE_JSON5_MARSHAL_OPTIONS) or_return + os.write_entire_file(path, str) or_return + return nil +} + +gamemaker_resource_get_version :: proc(resource: json.Object) -> string { + return resource["resourceVersion"].(json.String) +} + +odin_file_get_exported_procs :: proc(odin_file_path: string) -> (list: [dynamic]Proc, err: Error) { +@static _odin_write_file_exports_procs: [dynamic]Proc + _odin_write_file_exports_procs = make([dynamic]Proc) + + source_code := os.read_entire_file(odin_file_path, context.allocator) or_return + file: ast.File + file.src = string(source_code) + file.fullpath = odin_file_path + file.derived = &file + p: parser.Parser + parser.parse_file(&p, &file) + + visitor := ast.Visitor{ + visit = proc(v: ^ast.Visitor, node: ^ast.Node) -> ^ast.Visitor { + if node == nil do return v + #partial switch derived in node.derived { + case ^ast.Value_Decl: + if !odin_ast_node_has_attribute(derived.attributes, "export") { + return v + } + if len(derived.values) != 1 { + return v + } + lit, is_proc_lit := derived.values[0].derived.(^ast.Proc_Lit) + if !is_proc_lit { + return v + } + proc_name := derived.names[0].derived.(^ast.Ident).name + proc_params := odin_ast_node_proc_lit_get_params(lit) + proc_result, proc_result_name := odin_ast_node_proc_lit_get_results(lit) + append( + &_odin_write_file_exports_procs, + Proc{name = proc_name, params = proc_params, result = proc_result, result_type_name = proc_result_name}) + } + return v + } + } + ast.walk(&visitor, &file) + + list = _odin_write_file_exports_procs + _odin_write_file_exports_procs = [dynamic]Proc{} + return +} + +odin_ast_node_proc_lit_get_params :: proc(lit: ^ast.Proc_Lit) -> [dynamic]Param { + params := make([dynamic]Param) + for param in lit.type.params.list { + type_name: string + type: GameMakerParamType + #partial switch val in param.type.derived { + case ^ast.Ident: + type = .Double + type_name = double_type_string + case ^ast.Multi_Pointer_Type, ^ast.Pointer_Type: + type = .String + type_name = string_type_string + } + for name in param.names { + append(¶ms, Param{ + name = name.derived.(^ast.Ident).name, + type_name = type_name, + type = type}) + } + } + return params +} + +odin_ast_node_proc_lit_get_results :: proc(lit: ^ast.Proc_Lit) -> (GameMakerParamType, string) { + if lit.type.results == nil { + return .None, empty_type_string + } + results := lit.type.results.list + switch len(results) { + case 0: + return .Double, double_type_string + case 1: + type := results[0].derived.(^ast.Field).type + #partial switch _ in type.derived { + case ^ast.Pointer_Type, ^ast.Multi_Pointer_Type: + return .String, string_type_string + case ^ast.Ident: + return .Double, double_type_string + case: + panic("Invalid field") + } + case: + panic("fuck fuck fuck fuck fukc") + } + unreachable() +} + +odin_ast_node_has_attribute :: proc(attributes: [dynamic]^ast.Attribute, name: string) -> bool { + for attribute in attributes { + for element in attribute.elems { + ident, is_ident := element.derived.(^ast.Ident) + if is_ident && ident.name == name { + return true + } + } + } + return false +}