gamemaker_enum_reflection_gen/main.odin
2026-05-28 11:01:19 +02:00

193 lines
6.1 KiB
Odin

package main
import "core:fmt"
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: <program> <project dir> <output gml>")
}
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)
}
gml := strings.builder_make()
gml_write_enums(&gml, enums)
os.write_entire_file(output_gml_path, strings.to_string(gml)) or_return
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(gml: ^strings.Builder, enums: [dynamic]GmlEnum) {
fmt.sbprint(gml, "/*\n" + TAB + "This file was generated using enum_reflection tool\n*/\n", sep = "")
fmt.sbprint(gml, "enum " + ENUM_ENUM + " {\n", sep = "")
for e in enums {
fmt.sbprint(gml, TAB, e.name, ",\n", sep = "")
}
fmt.sbprint(gml, TAB + "_Count\n}\n\n", sep = "")
fmt.sbprint(gml, ENUM_NAMES_GLOBAL, " = [\n", sep = "")
for e in enums {
fmt.sbprint(gml, TAB, "\"", e.name, "\",\n", sep = "")
}
fmt.sbprint(gml, "];\n\n", sep = "")
fmt.sbprint(gml, ENUM_MEMBER_NAMES_GLOBAL, " = [\n", sep = "")
for e in enums {
fmt.sbprint(gml, TAB, "[", sep = "")
for entry in e.members {
fmt.sbprint(gml, "\"", entry, "\", ", sep = "")
}
fmt.sbprint(gml, "],\n", sep = "")
}
fmt.sbprint(gml, "];\n\n", sep = "")
fmt.sbprint(gml, "function ", ENUM_GET_NAME_FUNCTION, "(index) {return ", ENUM_NAMES_GLOBAL, "[index];}\n", sep = "")
fmt.sbprint(gml, "function ", ENUM_GET_MEMBER_NAME_FUNCTION, "(index, member) {return ", ENUM_MEMBER_NAMES_GLOBAL, "[index][member];}\n", sep = "")
fmt.sbprint(gml, "function ", ENUM_GET_MEMBER_NAMES_FUNCTION, "(index) {return ", ENUM_MEMBER_NAMES_GLOBAL, "[index];}", sep = "")
fmt.sbprint(gml, "function ", ENUM_GET_MEMBER_NAME_FULL_FUNCTION, "(index, member) {\n", sep = "")
fmt.sbprint(gml, TAB, "return $\"{", ENUM_NAMES_GLOBAL, "[index]}.{" + ENUM_MEMBER_NAMES_GLOBAL, "[index][member]}\";\n", sep = "")
fmt.sbprint(gml, "}\n\n", sep = "")
}