commit bdd292ce2a5c89a70ed4ed1cc089f82420ef502d Author: Synthasmagoria Date: Tue May 12 14:54:23 2026 +0200 Initial commit diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..c6694f0 --- /dev/null +++ b/main.odin @@ -0,0 +1,210 @@ +package main + +import "core:fmt" +import "core:io" +import "core:mem" +import "core:os" +import "core:strings" + +TAB :: "\t" +ENUM_TAG :: "gml_pragma(\"enum_reflection\")" +ENUM_ENUM :: "Enum" +ENUM_NAMES_GLOBAL :: "global._ENUM_NAMES" +ENUM_MEMBER_NAMES_GLOBAL :: "global._ENUM_MEMBER_NAMES" + +ENUM_GET_NAME_FUNCTION :: "enum_get_name" +ENUM_GET_MEMBER_NAME_FUNCTION :: "enum_get_member_name" +ENUM_GET_MEMBER_NAMES_FUNCTION :: "enum_get_member_names" +ENUM_GET_MEMBER_NAME_FULL_FUNCTION :: "enum_get_member_name_full" + +print_help_message :: proc() { + fmt.println("--- GameMaker enum reflection ---", flush = false) + fmt.println("Usage: ") +} + +main :: proc() { + switch len(os.args) { + case 0, 1, 2: + print_help_message() + case 3: + err := program(os.args[1], os.args[2]) + if err != nil { + fmt.println("Error: ", err) + } + case: + fmt.println("Error: Too many arguments") + print_help_message() + } +} + +Error :: union { + os.Error, + mem.Allocator_Error, + ProgramError, +} + +ProgramError :: enum { + ProjectDirNotDir, + OutputGmlNotExists, + ScriptsDirNotExists, + ObjectsDirNotExists, +} + +program :: proc(project_dir, output_gml_path: string) -> Error { + if !os.is_directory(project_dir) { + return ProgramError.ProjectDirNotDir + } + if !os.exists(output_gml_path) { + return ProgramError.OutputGmlNotExists + } + _, output_gml_name := os.split_path(output_gml_path) + + scripts_dir := os.join_path({project_dir, "scripts"}, context.allocator) or_return + if !os.exists(scripts_dir) { + return ProgramError.ScriptsDirNotExists + } + + objects_dir := os.join_path({project_dir, "objects"}, context.allocator) or_return + if !os.exists(objects_dir) { + return ProgramError.ObjectsDirNotExists + } + + enums := make([dynamic]GmlEnum) + walker := os.walker_create(scripts_dir) + for info, ok := os.walker_walk(&walker); ok; info, ok = os.walker_walk(&walker) { + if _, ext := os.split_filename(info.name); ext != "gml" { + continue + } + if info.name == output_gml_name { + continue + } + data := os.read_entire_file(info.fullpath, context.allocator) or_return + gml := string(data) + gml_extract_enums(gml, info.name, &enums) + } + + walker = os.walker_create(objects_dir) + for info, ok := os.walker_walk(&walker); ok; info, ok = os.walker_walk(&walker) { + if _, ext := os.split_filename(info.name); ext != "gml" { + continue + } + if info.name == output_gml_name { + continue + } + data := os.read_entire_file(info.fullpath, context.allocator) or_return + gml := string(data) + gml_extract_enums(gml, info.name, &enums) + } + + f := os.open(output_gml_path, {.Write, .Trunc}) or_return + writer := io.Writer(os.to_stream(f)) + gml_write_enums(writer, enums) + os.close(f) + + return nil +} + +GmlEnum :: struct { + name: string, + members: []string, +} +gml_extract_enums :: proc(gml, filename: string, enums_out: ^[dynamic]GmlEnum) { + ENUM_SEARCH :: "enum " + + pos: int + for true { + gml_prev_enum := gml[pos:] + pos_tag_start := strings.index(gml_prev_enum, ENUM_TAG) + if pos_tag_start == -1 { + break + } + pos_tag_end := pos_tag_start + len(ENUM_TAG) + pos += pos_tag_end + gml_tag_end := gml_prev_enum[pos_tag_end:] + pos_enum_start := strings.index(gml_tag_end, ENUM_SEARCH) + if pos_enum_start == -1 { + fmt.println("Found " + ENUM_TAG + " in", filename, "but no enum") + break + } + if len(strings.trim(strings.trim_space(gml_tag_end[:pos_enum_start]), ";\n")) > 0 { + fmt.println("Found", ENUM_TAG, "in", filename, "but separation between tag and enum was too great") + break + } + gml_enum_name_start := gml_tag_end[pos_enum_start + len(ENUM_SEARCH):] + pos_enum_curly_open := strings.index(gml_enum_name_start, "{") + if pos_enum_curly_open == -1 { + fmt.println("Missing enum opening bracket in", filename) + break + } + enum_name := strings.trim_space(gml_enum_name_start[:pos_enum_curly_open]) + pos_enum_curly_close := strings.index(gml_enum_name_start, "}") + if pos_enum_curly_close == -1 { + fmt.println("Missing enum closing bracket in", filename) + } + gml_enum_content := gml_enum_name_start[pos_enum_curly_open + 1:pos_enum_curly_close] + if strings.index(gml_enum_content, "/") != -1 { + fmt.println("Enum members of", enum_name, "in", filename, "contained disallowed character '/' (possibly comments). Skipping...") + continue + } + if strings.index(gml_enum_content, "=") != -1 { + fmt.println("Enum members of", enum_name, "in", filename, "contained disallowed character '='. Skipping...") + continue + } + enum_names := strings.split(gml_enum_content, ",") + for i in 0 ..< len(enum_names) { + enum_names[i] = strings.trim_space(enum_names[i]) + } + if len(enum_names[len(enum_names) - 1]) == 0 { + enum_names = enum_names[:len(enum_names) - 1] + } + append(enums_out, GmlEnum{name = enum_name, members = enum_names}) + } +} + +gml_write_enums :: proc(writer: io.Writer, enums: [dynamic]GmlEnum) { + io.write_string(writer, "/*\n" + TAB + "This file was generated using enum_reflection tool\n*/\n") + io.write_string(writer, "enum " + ENUM_ENUM + " {\n") + for e in enums { + io.write_string(writer, TAB) + io.write_string(writer, e.name) + io.write_string(writer, ",\n") + } + io.write_string(writer, TAB + "_Count\n}\n\n") + + io.write_string(writer, ENUM_NAMES_GLOBAL + " = [\n") + for e in enums { + io.write_string(writer, TAB + "\"") + io.write_string(writer, e.name) + io.write_string(writer, "\",\n") + } + io.write_string(writer, "];\n\n") + + io.write_string(writer, ENUM_MEMBER_NAMES_GLOBAL + " = [\n") + for e in enums { + io.write_string(writer, TAB + "[") + for entry in e.members { + io.write_string(writer, "\"") + io.write_string(writer, entry) + io.write_string(writer, "\", ") + } + io.write_string(writer, "],\n") + } + io.write_string(writer, "];\n\n") + + io.write_string(writer, "function " + ENUM_GET_NAME_FUNCTION + "(index) {\n") + io.write_string(writer, TAB + "return " + ENUM_NAMES_GLOBAL + "[index];\n") + io.write_string(writer, "}\n\n") + + io.write_string(writer, "function " + ENUM_GET_MEMBER_NAME_FUNCTION + "(index, member) {\n") + io.write_string(writer, TAB + "return " + ENUM_MEMBER_NAMES_GLOBAL + "[index][member];\n") + io.write_string(writer, "}\n\n") + + io.write_string(writer, "function " + ENUM_GET_MEMBER_NAMES_FUNCTION + "(index) {\n") + io.write_string(writer, TAB + "return " + ENUM_MEMBER_NAMES_GLOBAL + "[index];\n") + io.write_string(writer, "}\n\n") + + io.write_string(writer, "function " + ENUM_GET_MEMBER_NAME_FULL_FUNCTION + "(index, member) {\n") + io.write_string(writer, TAB + "return $\"{" + ENUM_NAMES_GLOBAL + "[index]}.{" + ENUM_MEMBER_NAMES_GLOBAL + "[index][member]}\";\n") + io.write_string(writer, "}\n\n") +} +