gamemaker_regenerate_extension/main.odin
2026-04-03 21:56:27 +02:00

314 lines
8.8 KiB
Odin

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: <program> <odin module path> <gamemaker extension path>")
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(&params, 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
}