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