Initial commit
This commit is contained in:
commit
6c2ddef1bb
1 changed files with 314 additions and 0 deletions
314
main.odin
Normal file
314
main.odin
Normal file
|
|
@ -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: <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(¶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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue