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 { file := file.(json.Object) filename := file["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 } file["functions"] = funcs } 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 }