diff --git a/assetcomp/assetcomp.odin b/assetcomp/assetcomp.odin new file mode 100644 index 0000000..1790c21 --- /dev/null +++ b/assetcomp/assetcomp.odin @@ -0,0 +1,352 @@ +package assetcomp + +import "common:encoding/sexp" +import "common:name" +import "core:container/intrusive/list" +import "core:fmt" +import "core:log" +import os "core:os/os2" + +_ :: sexp + +main :: proc() { + context.logger = log.create_console_logger() + file, file_err := os.read_entire_file("src_assets/defs.ass", context.temp_allocator) + + if file_err != nil { + log.fatalf("failed to read ass file {}", file_err) + } + + parser: sexp.SEXP_Parser + parser.data = transmute(string)file + expr_list, err := sexp.parse(&parser) + + if err != nil { + w := os.to_writer(os.stderr) + sexp.print_pretty_error(w, parser.data, err) + fmt.print("\n") + } + + it := sexp.iterator_list(expr_list) + + for expr in sexp.iterator_next(&it) { + sexp.print_sexp(expr.expr, os.to_writer(os.stdout)) + fmt.print("\n") + } + + asset_gather_ctx: Asset_Gather_Context + + interpreter: Lisp_Interpreter + interpreter.user_data = &asset_gather_ctx + interpreter.external_func_table[sexp.Ident(name.from_string("define-asset"))] = + lisp_define_asset + interpreter.external_func_table[sexp.Ident(name.from_string("run-proc"))] = lisp_run_proc + + lisp_interpret_expr_list(&interpreter, sexp.iterator_list(expr_list)) + + for n, local_func in interpreter.local_func_table { + log.debugf("local func {}\nargs: ", name.to_string(name.Name(n))) + sexp.print_list(local_func.args, os.to_writer(os.stderr)) + log.debugf("\nbody: ") + sexp.print_list(local_func.body, os.to_writer(os.stderr)) + log.debugf("\n") + } +} + +lisp_run_proc :: proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp { + log.debugf("called run-proc with args:") + args := args + for body in sexp.iterator_next(&args) { + sexp.print_sexp(lisp_interpret_expr(ctx, body.expr), os.to_writer(os.stdout)) + fmt.print("\n") + } + fmt.print("\n") + return nil +} + +lisp_define_asset :: proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp { + // asset_ctx := cast(^Asset_Gather_Context)ctx.user_data + + args := args + type := name.Name( + lisp_interpret_expr(ctx, sexp.iterator_next_checked(&args)).(sexp.Atom).(sexp.Tag), + ) + id := lisp_interpret_expr(ctx, sexp.iterator_next_checked(&args)).(sexp.Atom).(string) + + log.debugf("called define-asset with id {}, type {}, body:", id, name.to_string(type)) + for body in sexp.iterator_next(&args) { + sexp.print_sexp(lisp_interpret_expr(ctx, body.expr), os.to_writer(os.stdout)) + fmt.print("\n") + } + + return nil +} + +Lisp_External_Func :: #type proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp + +global_func_table: map[sexp.Ident]Lisp_External_Func + +@(init) +init_global_funcs :: proc() { + global_func_table[sexp.Ident(name.from_string("defn"))] = + proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp { + args := args + name := sexp.to_ident_checked(sexp.iterator_next_checked(&args)) + func_args := sexp.iterator_next_checked(&args).(sexp.Sexp_List) + body := args + + ctx.local_func_table[name] = Local_Func { + args = sexp.iterator_list(func_args), + body = body, + } + + return nil + } + + global_func_table[sexp.Ident(name.from_string("def"))] = + proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp { + args := args + name := sexp.to_ident_checked(sexp.iterator_next_checked(&args)) + value := sexp.iterator_next_checked(&args) + + result := lisp_interpret_expr(ctx, value) + get_scope(ctx).values[name] = result + return result + } + + global_func_table[sexp.Ident(name.from_string("let"))] = + proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp { + args := args + bindings := sexp.iterator_next_checked(&args).(sexp.Sexp_List) + + push_scope(ctx) + defer pop_scope(ctx) + scope := get_scope(ctx) + + binds_it := sexp.iterator_list(bindings) + for name_expr in sexp.iterator_next(&binds_it) { + var_name := sexp.to_ident_checked(name_expr.expr) + value := lisp_interpret_expr(ctx, sexp.iterator_next_checked(&binds_it)) + scope.values[var_name] = value + } + + body := args + + return lisp_interpret_expr_list(ctx, body) + } + + global_func_table[sexp.Ident(name.from_string("temp-dir"))] = + proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp { + args := args + pattern := lisp_interpret_expr(ctx, sexp.iterator_next_checked(&args)).(sexp.Atom).(string) + assert(len(pattern) > 0) + + temp_dir, _ := os.temp_dir(context.temp_allocator) + temp_path, _ := os.mkdir_temp(temp_dir, pattern, context.temp_allocator) + + return sexp.Atom(temp_path) + } + + global_func_table[sexp.Ident(name.from_string("if"))] = + proc(ctx: ^Lisp_Interpreter, args: sexp.List_Iterator) -> sexp.Sexp { + args := args + condition := lisp_interpret_expr(ctx, sexp.iterator_next_checked(&args)) + + true_body := sexp.iterator_next_checked(&args) + + if condition != nil { + return lisp_interpret_expr(ctx, true_body) + } else { + false_body, ok := sexp.iterator_next(&args) + + if ok { + return lisp_interpret_expr(ctx, false_body.expr) + } + } + + return nil + } +} + +Local_Func :: struct { + args: sexp.List_Iterator, + body: sexp.List_Iterator, +} + +Scope :: struct { + values: map[sexp.Ident]sexp.Sexp, +} + +MAX_SCOPE_DEPTH :: 32 + +Lisp_Interpreter :: struct { + user_data: rawptr, + external_func_table: map[sexp.Ident]Lisp_External_Func, + local_func_table: map[sexp.Ident]Local_Func, + scope_stack: [MAX_SCOPE_DEPTH]Scope, + scope_idx: i32, +} + +push_scope :: proc(ctx: ^Lisp_Interpreter) { + assert(ctx.scope_idx + 1 < len(ctx.scope_stack)) + + ctx.scope_idx += 1 +} + +pop_scope :: proc(ctx: ^Lisp_Interpreter) { + assert(ctx.scope_idx - 1 >= 0) + + delete(ctx.scope_stack[ctx.scope_idx].values) + ctx.scope_idx -= 1 +} + +get_scope :: proc(ctx: ^Lisp_Interpreter) -> ^Scope { + return &ctx.scope_stack[ctx.scope_idx] +} + +find_value_scope :: proc(ctx: ^Lisp_Interpreter, name: sexp.Ident) -> sexp.Sexp { + idx := ctx.scope_idx + for idx >= 0 { + scope := &ctx.scope_stack[idx] + + value, ok := scope.values[name] + if ok { + return value + } + idx -= 1 + } + + return nil +} + +lisp_apply :: proc( + ctx: ^Lisp_Interpreter, + func: sexp.Ident, + args: sexp.List_Iterator, +) -> ( + result: sexp.Sexp, +) { + log.debugf("apply {} {}", name.to_string(name.Name(func)), sexp.to_string_temp(args)) + { + local_func, ok := ctx.local_func_table[func] + if ok { + push_scope(ctx) + defer pop_scope(ctx) + + scope := get_scope(ctx) + + def_args_it := local_func.args + provided_args_it := args + for defined_arg in sexp.iterator_next(&def_args_it) { + var_name := sexp.to_ident_checked(defined_arg.expr) + var_value := lisp_interpret_expr( + ctx, + sexp.iterator_next_checked(&provided_args_it), + ) + + log.debugf( + "adding local var to scope {} {}", + name.to_string(name.Name(var_name)), + sexp.to_string_temp(var_value), + ) + + scope.values[var_name] = lisp_interpret_expr(ctx, var_value) + } + + body_it := local_func.body + for body in sexp.iterator_next(&body_it) { + // Last one will be the actual result + result = lisp_interpret_expr(ctx, body.expr) + } + + return + } + } + + { + external_func, ok := ctx.external_func_table[func] + if ok { + return external_func(ctx, args) + } + } + + { + global_func, ok := global_func_table[func] + if ok { + return global_func(ctx, args) + } + } + + log.fatalf("function with name {} not found", name.to_string(name.Name(func))) + return +} + +lisp_interpret_expr_list :: proc( + ctx: ^Lisp_Interpreter, + body: sexp.List_Iterator, +) -> ( + result: sexp.Sexp, +) { + body := body + for expr in sexp.iterator_next(&body) { + result = lisp_interpret_expr(ctx, expr.expr) + } + + return +} + +lisp_interpret_expr :: proc(ctx: ^Lisp_Interpreter, expr: sexp.Sexp) -> (result: sexp.Sexp) { + for { + switch &s in expr { + case sexp.Atom: + #partial switch a in s { + case sexp.Ident: + result = find_value_scope(ctx, a) + case: + result = s + } + case sexp.Sexp_List: + if list.is_empty(cast(^list.List)&s) { + return nil + } + + it := sexp.iterator_list(s) + raw_func_expr := sexp.iterator_next_checked(&it) + func_ident, ok := sexp.to_ident(raw_func_expr) + + if !ok { + func_expr := lisp_interpret_expr(ctx, raw_func_expr) + func_ident, ok = sexp.to_ident(func_expr) + } + + if !ok { + log.fatalf("expected identifier in a call expression, got {}", func_ident) + } + + result = lisp_apply(ctx, func_ident, it) + } + + if result == nil || !sexp.is_ident_expr(result) { + break + } + } + + return +} + +Asset_Model :: struct { + // Expression that defines a model + model: sexp.Sexp, +} + +Asset_Union :: union { + Asset_Model, +} + +Asset_Def :: struct { + data: Asset_Union, +} + +Asset_Gather_Context :: struct { + assets: map[string]Asset_Def, +} diff --git a/builder/builder.odin b/builder/builder.odin index 95dc23d..3e624df 100644 --- a/builder/builder.odin +++ b/builder/builder.odin @@ -14,6 +14,7 @@ Build_Variant :: enum { Hot_Reload, Desktop, Web, + Assetcomp, } Options :: struct { @@ -282,6 +283,7 @@ main :: proc() { "odin", "build", "game", + "-define:NAME_STATIC_INIT=false", "-define:RAYLIB_SHARED=true", "-define:PHYSFS_SHARED=true", "-define:DEV=true", @@ -371,5 +373,20 @@ main :: proc() { ), "./bin/web/odin.js", ) + case .Assetcomp: + remove_all("./bin/assetcomp") + mkdir_all("./bin/assetcomp") + + cmd := slice.concatenate( + [][]string { + []string{"odin", "build", "assetcomp", "-out:./bin/assetcomp/assetcomp" + EXE_EXT}, + // tracy_flag, + debug_flag, + optimize_flag, + COMMON_FLAGS, + }, + context.temp_allocator, + ) + run_cmd(cmd, "") } } diff --git a/common/assetdsl/assetdsl.odin b/common/assetdsl/assetdsl.odin new file mode 100644 index 0000000..182dd5d --- /dev/null +++ b/common/assetdsl/assetdsl.odin @@ -0,0 +1,301 @@ +package assetdsl + +import "core:container/intrusive/list" +import "core:log" +import "core:mem" +import "core:mem/virtual" +import os "core:os/os2" +import "core:slice" +import "core:strings" + +Asset_Builder :: struct { + arena: virtual.Arena, + // Allocator created from the arena above + allocator: mem.Allocator, + asset_defs: map[string]^Asset_Def, + gen_files: list.List, +} + +Source_File_Node :: struct { + using node: Builder_Node, + path: string, +} + +make_source_file_node :: proc(builder: ^Asset_Builder, path: string) -> (node: ^Source_File_Node) { + node = new(Source_File_Node, builder.allocator) + node.path = strings.clone(path, builder.allocator) + node.node.exec = + proc(ctx: ^Asset_Builder_Exec_Context, builder_node: ^Builder_Node) -> ([]byte, Error) { + source_file_node := container_of(builder_node, Source_File_Node, "node") + + contents, err := os.read_entire_file(source_file_node.path, context.allocator) + + if err != nil { + return nil, make_error(os.error_string(err)) + } + + return contents, nil + } + + return +} + +Source_File :: struct { + path: string, +} +Gen_File :: struct { + node: ^Gen_File_Node, +} + +Lazy_Path :: union #no_nil { + Source_File, + Gen_File, +} + +Asset_Builder_Exec_Context :: struct {} + +Builder_Node :: struct { + needs_list: list.List, + needs_node: list.Node, + exec: proc( + ctx: ^Asset_Builder_Exec_Context, + builder_node: ^Builder_Node, + ) -> ( + []byte, + Error, + ), +} + +Output_Node :: struct($T: typeid) { + using node: Builder_Node, + // Computed when this executes + value: Maybe(T), +} + +Gen_File_Node :: struct { + using node: Builder_Node, + // List node to iterate all gen files. List itself is stored on builder + gen_files: list.Node, + // Absolute path filled when file is generated + abs_path: string, +} + +make_output_node :: proc(builder: ^Asset_Builder, $T: typeid) -> ^Output_Node(T) { + context.allocator = builder.allocator + + out_node := new(Output_Node(T)) + + return out_node +} + +Run_Proc_Node :: struct { + using node: Builder_Node, + args: []Run_Arg, +} + +Custom_Node :: struct($T: typeid) { + using node: Builder_Node, + data: T, + callback: proc(ctx: ^Asset_Builder_Exec_Context, data: ^T) -> ([]byte, Error), +} + +make_custom_node :: proc( + builder: ^Asset_Builder, + data: string, + callback: proc(ctx: ^Asset_Builder_Exec_Context, data: ^string) -> ([]byte, Error), +) -> ^Custom_Node(string) { + node := new(Custom_Node(string), builder.allocator) + node.data = data + node.callback = callback + node.node.exec = + proc(ctx: ^Asset_Builder_Exec_Context, builder_node: ^Builder_Node) -> ([]byte, Error) { + custom_node := container_of(builder_node, Custom_Node(string), "node") + return custom_node.callback(ctx, &custom_node.data) + } + return node +} + +add_dependency :: proc(node: ^Builder_Node, dependency: ^Builder_Node) { + list.push_back(&node.needs_list, &dependency.needs_node) +} + +get_path_builder_node :: proc(path: Lazy_Path) -> (node: ^Builder_Node) { + switch p in path { + case Gen_File: + node = p.node + case Source_File: + } + + return +} + +make_run_proc_node :: proc(builder: ^Asset_Builder, args: []Run_Arg) -> ^Run_Proc_Node { + context.allocator = builder.allocator + + run_node := new(Run_Proc_Node) + run_node.args = slice.clone(args) + + // Add input dependencies + for &arg in run_node.args { + switch a in arg { + case string: + case ^Output_Node(string): + add_dependency(&run_node.node, &a.node) + case Arg_Path: + switch p in a { + case Input_Path: + if dep := get_path_builder_node(Lazy_Path(p)); dep != nil { + add_dependency(&run_node.node, dep) + } + case Output_Path: + } + } + } + + run_node.exec = proc(ctx: ^Asset_Builder_Exec_Context, node: ^Builder_Node) -> Error { + run_node := container_of(node, Run_Proc_Node, "node") + + resolved_args := make([]string, len(run_node.args)) + + for arg, i in run_node.args { + arg_str := resolve_arg(arg) + resolved_args[i] = arg_str + } + + log.infof("run: {}", resolved_args) + + return nil + } + + stdout_node := make_output_node(builder, []byte) + stderr_node := make_output_node(builder, []byte) + + add_dependency(&stdout_node.node, &run_node.node) + add_dependency(&stderr_node.node, &run_node.node) + + return run_node +} + +Lazy_Value :: union($T: typeid) #no_nil { + T, + ^Output_Node(T), +} + +Input_Path :: distinct Lazy_Path +Output_Path :: distinct Lazy_Path + +Arg_Path :: union { + Input_Path, + Output_Path, +} + +Run_Arg :: union #no_nil { + // Simple string + string, + Arg_Path, + // Will be evaluated at execution time + ^Output_Node(string), +} + +arg_in :: proc(path: Lazy_Path) -> Run_Arg { + return Arg_Path(Input_Path(path)) +} + +arg_out :: proc(path: Lazy_Path) -> Run_Arg { + return Arg_Path(Output_Path(path)) +} + +resolve_arg :: proc(arg: Run_Arg) -> string { + result: string + switch a in arg { + case string: + result = a + case Arg_Path: + result = "TODO" + case ^Output_Node(string): + result = a.value.? + } + + return result +} + +Run_Proc_Step :: struct { + stdout: ^Output_Node([]byte), + stderr: ^Output_Node([]byte), +} +Asset_Def :: struct { + id: string, + file: Gen_File, +} + +Builder_Error :: struct { + msg: string, +} + +Error :: union { + Builder_Error, +} + +builder_init :: proc(builder: ^Asset_Builder, arena: virtual.Arena) { + builder.arena = arena + builder.allocator = virtual.arena_allocator(&builder.arena) +} + +@(private = "file") +make_error :: proc(msg: string) -> Error { + return Builder_Error{msg = msg} +} + +define_asset :: proc(builder: ^Asset_Builder, id: string) -> (^Asset_Def, Error) { + context.allocator = builder.allocator + + asset_def, ok := builder.asset_defs[id] + if ok { + return nil, make_error("asset exists") + } + asset_def = new(Asset_Def) + asset_def.id = strings.clone(id) + asset_def.file = gen_file(builder) + builder.asset_defs[id] = asset_def + + return asset_def, nil +} + +source_file :: proc(builder: ^Asset_Builder, path: string) -> Lazy_Path { + return Source_File{path = strings.clone(path, builder.allocator)} +} +gen_file :: proc(builder: ^Asset_Builder) -> Gen_File { + gen_file_node := new(Gen_File_Node, builder.allocator) + list.push_back(&builder.gen_files, &gen_file_node.gen_files) + + return Gen_File{node = gen_file_node} +} +run_proc :: proc(builder: ^Asset_Builder, args: []Run_Arg) -> (Run_Proc_Step, Error) { + +} + +export_model_from_blend :: proc( + builder: ^Asset_Builder, + blend: Lazy_Path, + output: Lazy_Path, + collection: string, +) { +} + +define_assets :: proc(builder: ^Asset_Builder) { + ae86_asset := define_asset(builder, "models.ae86") + + ae86_blend := source_file(builder, "ae86.blend") + ae86_convex_blend := source_file(builder, "car_convex.blend") + + run_proc( + builder, + { + "blender", + "--python", + arg_in(source_file(builder, "export-model")), + arg_in(ae86_blend), + arg_out(ae86_asset.file), + }, + ) +} diff --git a/common/encoding/sexp/sexp.odin b/common/encoding/sexp/sexp.odin new file mode 100644 index 0000000..12e9c1e --- /dev/null +++ b/common/encoding/sexp/sexp.odin @@ -0,0 +1,451 @@ +package sexp + +import "common:name" +import "core:container/intrusive/list" +import "core:fmt" +import "core:io" +import "core:strings" +import "core:testing" +import "core:unicode" +import "core:unicode/utf8" + +SEXP_Parser :: struct { + data: string, + pos: int, + c: rune, +} + +Ident :: distinct name.Name +Tag :: distinct name.Name + +Atom :: union { + Ident, + Tag, + string, + f64, +} + +Sexp_List_Node :: struct { + expr: Sexp, + node: list.Node, +} + +Sexp_List :: distinct list.List + +Sexp :: union { + Atom, + Sexp_List, +} + +to_ident :: proc(sexp: Sexp) -> (ident: Ident, ok: bool) { + atom: Atom + atom, ok = sexp.(Atom) + if ok { + ident, ok = atom.(Ident) + } + return +} + +to_ident_checked :: proc(sexp: Sexp, expr := #caller_expression) -> Ident { + ident, ok := to_ident(sexp) + assert(ok, expr) + return ident +} + +is_ident_expr :: proc(sexp: Sexp) -> (ok: bool) { + atom: Atom + atom, ok = sexp.(Atom) + if ok { + _, ok = atom.(Ident) + } + return +} + +to_string_temp_sexp :: proc(sexp: Sexp) -> string { + builder := strings.builder_make(context.temp_allocator) + w := strings.to_writer(&builder) + print_sexp(sexp, w) + return strings.to_string(builder) +} + +to_string_temp_iter :: proc(it: List_Iterator) -> string { + builder := strings.builder_make(context.temp_allocator) + w := strings.to_writer(&builder) + print_list(it, w) + return strings.to_string(builder) +} + +to_string_temp :: proc { + to_string_temp_sexp, + to_string_temp_iter, +} + +expect :: proc(ctx: ^SEXP_Parser, expected: rune) -> Error { + c, next := peek(ctx) or_return + + if c != expected { + return make_error(ctx, fmt.tprintf("unexpected character {}, expected: {}", c, expected)) + } + ctx.pos = next + + return nil +} + +is_ident :: proc(c: rune) -> bool { + switch c { + case 'a' ..= 'z', 'A' ..= 'Z', '0' ..= '9', '-', '_', '.': + return true + case: + } + + return false +} + +parse_atom :: proc(ctx: ^SEXP_Parser) -> (atom: Atom, error: Error) { + c, next := peek(ctx) or_return + + switch c { + case 'a' ..= 'z', 'A' ..= 'Z': + start := ctx.pos + for ctx.pos < len(ctx.data) { + c, next = peek(ctx) or_return + if !is_ident(c) { + break + } + + ctx.pos = next + } + return Ident(name.from_string(ctx.data[start:ctx.pos])), nil + case ':': + // skip it + ctx.pos = next + + start := ctx.pos + for ctx.pos < len(ctx.data) { + c, next = peek(ctx) or_return + if !is_ident(c) { + break + } + + ctx.pos = next + } + return Tag(name.from_string(ctx.data[start:ctx.pos])), nil + case '"': + // skip it + ctx.pos = next + + start := ctx.pos + loop: for { + c, next = peek(ctx) or_return + + switch c { + case '\\': + _, next = peek(ctx) or_return + ctx.pos = next + case '"': + break loop + } + ctx.pos = next + } + + result := ctx.data[start:ctx.pos] + ctx.pos = next + return result, nil + + } + + return nil, make_error(ctx, fmt.tprintf("unknown atom {}", c)) +} + +parse_list :: proc(ctx: ^SEXP_Parser) -> (result: Sexp_List, error: Error) { + skip_whitespace(ctx) or_return + expect(ctx, '(') or_return + + for { + skip_whitespace(ctx) or_return + c, next := peek(ctx) or_return + + if c == ')' { + ctx.pos = next + break + } else { + sexp := parse_sexp(ctx) or_return + node := new(Sexp_List_Node) + node.expr = sexp + list.push_back(cast(^list.List)&result, &node.node) + } + } + + return +} + +parse_sexp :: proc(ctx: ^SEXP_Parser) -> (sexp: Sexp, error: Error) { + skip_whitespace(ctx) or_return + c, _ := peek(ctx) or_return + + switch c { + case '(': + return parse_list(ctx) + case: + return parse_atom(ctx) + } +} + +// Parses a top level list of s-expressions which are not enclosed by parens +// No freeing procedure is provided, instead pass in an arena allocator and call free_all on it +parse :: proc( + ctx: ^SEXP_Parser, + allocator := context.temp_allocator, +) -> ( + sexp: Sexp_List, + error: Error, +) { + context.allocator = allocator + + sexp_list: Sexp_List + + for ctx.pos < len(ctx.data) { + expr := parse_sexp(ctx) or_return + node := new(Sexp_List_Node) + node.expr = expr + list.push_back(cast(^list.List)&sexp_list, &node.node) + + skip_whitespace(ctx) + } + + return sexp_list, nil +} + +List_Iterator :: list.Iterator(Sexp_List_Node) + +iterator_list :: proc(sexp_list: Sexp_List) -> List_Iterator { + return list.iterator_head(list.List(sexp_list), Sexp_List_Node, "node") +} + +iterator_next :: proc(it: ^List_Iterator) -> (^Sexp_List_Node, bool) { + return list.iterate_next(it) +} + +iterator_next_checked :: proc(it: ^List_Iterator) -> Sexp { + node, ok := iterator_next(it) + assert(ok) + + return node.expr +} + +print_list :: proc(it: List_Iterator, w: io.Writer) -> io.Error { + it := it + io.write_byte(w, '(') or_return + first := true + for sexp_node in list.iterate_next(&it) { + if !first { + io.write_byte(w, ' ') or_return + } + print_sexp(sexp_node.expr, w) or_return + first = false + } + io.write_byte(w, ')') or_return + + return nil +} + +print_sexp :: proc(sexp: Sexp, w: io.Writer) -> io.Error { + switch s in sexp { + case Atom: + switch a in s { + case Ident: + io.write_string(w, string(name.to_string(name.Name(a)))) or_return + case Tag: + io.write_byte(w, ':') or_return + io.write_string(w, string(name.to_string(name.Name(a)))) or_return + case string: + io.write_quoted_string(w, a, '"') or_return + case f64: + io.write_f64(w, a) or_return + } + case Sexp_List: + it := list.iterator_head(list.List(s), Sexp_List_Node, "node") + print_list(it, w) or_return + } + + return nil +} + +Error_Type :: struct { + msg: string, + pos: int, +} + +Error :: union { + Error_Type, +} + +make_error :: proc(ctx: ^SEXP_Parser, err: string) -> Error { + return Error_Type{msg = err, pos = ctx.pos} +} + +get_line_column :: proc(str: string, pos: int) -> (line: int, column: int) { + line, column = 0, 0 + + p := 0 + + for p < pos { + c, num_bytes := utf8.decode_rune(str[p:]) + p += num_bytes + if c == '\n' { + line += 1 + column = 0 + } else { + column += 1 + } + } + + return line, column +} + +get_line_string :: proc(str: string, line: int) -> string { + cur_line := 0 + p := 0 + + start := -1 + for p < len(str) { + c, num_bytes := utf8.decode_rune(str[p:]) + if c == '\n' { + cur_line += 1 + } + + if cur_line == line + 1 { + break + } + + p += num_bytes + if cur_line == line && start == -1 { + start = p + } + } + + if start != -1 { + return str[start:p] + } else { + return "" + } +} + +peek :: proc(ctx: ^SEXP_Parser) -> (c: rune, next: int, err: Error) { + if ctx.pos < len(ctx.data) { + num_bytes: int + c, num_bytes = utf8.decode_rune(ctx.data[ctx.pos:]) + if c == utf8.RUNE_ERROR { + return c, ctx.pos, make_error(ctx, "invalid utf8 rune") + } + return c, ctx.pos + num_bytes, nil + } + + return 0, 0, make_error(ctx, "unexpected EOF") +} + +skip_whitespace :: proc(ctx: ^SEXP_Parser) -> Error { + if ctx.pos == len(ctx.data) { + return nil + } + + for { + c, next := peek(ctx) or_return + + if c == ';' { + skip_until_newline(ctx) or_return + } else if unicode.is_white_space(c) { + ctx.pos = next + } else { + break + } + } + + return nil +} + +skip_until_newline :: proc(ctx: ^SEXP_Parser) -> Error { + for { + c, next := peek(ctx) or_return + ctx.pos = next + if c == '\n' { + break + } + } + + return nil +} + +print_pretty_error :: proc(w: io.Writer, data: string, error: Error) -> io.Error { + if error == nil { + return nil + } + + err := error.(Error_Type) + line, column := get_line_column(data, err.pos) + line_str := get_line_string(data, line) + + io.write_byte(w, '\n') + + offset: int + io.write_int(w, line + 1, 10, &offset) or_return + io.write_string(w, " | ") or_return + io.write_string(w, line_str) or_return + io.write_byte(w, '\n') or_return + + for _ in 0 ..< (offset + 3 + column) { + io.write_byte(w, ' ') + } + io.write_string(w, "^ ") + io.write_string(w, err.msg) + + return nil +} + +@(test) +test_parse :: proc(t: ^testing.T) { + ctx: SEXP_Parser + ctx.data = "ident (sexp with \"string\") (nested (sexp))" + + sexp, err := parse(&ctx) + + testing.expect_value(t, err, nil) + + builder: strings.Builder + strings.builder_init(&builder, context.temp_allocator) + + writer := strings.to_writer(&builder) + print_sexp(sexp, writer) + + printed := strings.to_string(builder) + + testing.expect_value(t, printed, "(ident (sexp with \"string\") (nested (sexp)))") +} + +@(test) +test_error :: proc(t: ^testing.T) { + ctx: SEXP_Parser + ctx.data = ` + ident + (sexp with "string"` + + + _, err := parse(&ctx) + + testing.expect(t, err != nil) + + builder: strings.Builder + strings.builder_init(&builder, context.temp_allocator) + + writer := strings.to_writer(&builder) + print_pretty_error(writer, ctx.data, err) + printed := strings.to_string(builder) + + testing.expect_value( + t, + printed, + ` +3 | (sexp with "string" + ^ unexpected EOF`, + ) +} diff --git a/game/name/name.odin b/common/name/name.odin similarity index 77% rename from game/name/name.odin rename to common/name/name.odin index 0a4c53d..eb4be4e 100644 --- a/game/name/name.odin +++ b/common/name/name.odin @@ -6,6 +6,10 @@ import "core:mem" import "core:strings" import "core:sync" +// When enabled name globals will be initialized automatically +NAME_STATIC_INIT :: #config(NAME_STATIC_INIT, true) +MAX_STATIC_NAMES :: #config(MAX_STATIC_NAMES, 1024) + Name :: distinct u32 NONE :: Name(0) @@ -27,6 +31,7 @@ setup_global_container :: proc(cnt: ^Container) { init :: proc(cnt: ^Container) { mem.dynamic_arena_init(&cnt.names_allocator) assert(len(cnt.names_array) == 0) + append(&cnt.names_array, "None") } @@ -39,6 +44,22 @@ destroy :: proc() { global_container = nil } +when NAME_STATIC_INIT { + @(private = "file") + static_container: Container + + @(init) + init_static :: proc() { + init(&static_container) + setup_global_container(&static_container) + } + + @(fini) + fini_static :: proc() { + destroy() + } +} + from_string :: proc(str: string) -> Name { sync.atomic_rw_mutex_guard(&global_container.lock) existing, ok := global_container.names_lookup[str] diff --git a/game/assets/assets_test.odin b/game/assets/assets_test.odin index 21c5018..59a5d40 100644 --- a/game/assets/assets_test.odin +++ b/game/assets/assets_test.odin @@ -37,6 +37,6 @@ test_curve_parsing_error :: proc(t: ^testing.T) { curve, err := parse_csv_2d(transmute([]u8)test_invalid_csv, context.temp_allocator) defer free_all(context.temp_allocator) - testing.expect_value(t, err, CSV_Parse_Error.CSV_Parse_Error) + testing.expect_value(t, err, CSV_Parse_Error.ExpectedNumber) testing.expect_value(t, len(curve.points), 0) } diff --git a/game/game.odin b/game/game.odin index efff1a3..cf39c4e 100644 --- a/game/game.odin +++ b/game/game.odin @@ -15,6 +15,7 @@ package game import "assets" +import "common:name" import "core:fmt" import "core:hash" import "core:log" @@ -26,7 +27,6 @@ import "game:render" import rl "libs:raylib" import "libs:raylib/rlgl" import "libs:tracy" -import "name" import "ui" PIXEL_WINDOW_HEIGHT :: 360 diff --git a/game/physics/debug.odin b/game/physics/debug.odin index 0ee1189..e2285c7 100644 --- a/game/physics/debug.odin +++ b/game/physics/debug.odin @@ -1,6 +1,7 @@ package physics import "bvh" +import "common:name" import "core:fmt" import "core:log" import "core:math" @@ -10,7 +11,6 @@ import "core:slice" import "core:strings" import "game:debug" import he "game:halfedge" -import "game:name" import "game:ui" import rl "libs:raylib" import "libs:raylib/rlgl" diff --git a/game/physics/scene.odin b/game/physics/scene.odin index 5e11624..1a29e17 100644 --- a/game/physics/scene.odin +++ b/game/physics/scene.odin @@ -2,9 +2,9 @@ package physics import "bvh" import "collision" +import "common:name" import lg "core:math/linalg" import "game:container/spanpool" -import "game:name" import "libs:tracy" MAX_CONTACTS :: 1024 * 16 diff --git a/game/physics/simulation.odin b/game/physics/simulation.odin index 54b43d0..4c85552 100644 --- a/game/physics/simulation.odin +++ b/game/physics/simulation.odin @@ -3,6 +3,7 @@ package physics import "base:runtime" import "bvh" import "collision" +import "common:name" import "core:container/bit_array" import "core:fmt" import "core:log" @@ -12,7 +13,6 @@ import "core:math/rand" import "core:slice" import "game:debug" import he "game:halfedge" -import "game:name" import "libs:tracy" _ :: name diff --git a/sexp b/sexp new file mode 100755 index 0000000..8f87101 Binary files /dev/null and b/sexp differ diff --git a/src_assets/defs.ass b/src_assets/defs.ass new file mode 100644 index 0000000..692da15 --- /dev/null +++ b/src_assets/defs.ass @@ -0,0 +1,23 @@ +(def nil ()) + +(defn blender-script (script-file blend-file) + (run-proc "blender" "--python" script-file blend-file :out-file)) + +(defn export-model-from-collection-blend (blend-file collection) + (blender-script "export-model-from-collection.py" blend-file)) + +(path-join "path" "components" "joined" "together") + +(define-asset :model "models.ae86" + :format :glb + :make (let (tmp (temp-dir "mytemp")) + (run-proc tmp "test.blend") + (export-model-from-collection-blend "ae86.blend" "Final"))) + +(def textures-in-file (blender-script "list_textures.py" "testblend.blend")) + +; (for textures-in-file ) + +; (define-asset :car "cars/ae86.car" +; (visual-model (export-model-from-collection (load-blend "ae86.blend") "Final")) +; (collision (export-collision-from-collection (load-blend "car_convex.blend") "Collision"))) diff --git a/test.sh b/test.sh index a08c60a..0060a22 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -odin test game/assets -collection:common=./common -collection:game=./game -collection:libs=./libs -strict-style -vet -sanitize:memory +odin test common/encoding/sexp -collection:common=./common -collection:game=./game -collection:libs=./libs -strict-style -vet -sanitize:memory