A bunch of prototype code for asset pipeline

This commit is contained in:
sergeypdev 2025-07-11 23:18:35 +04:00
parent a8e3bb6104
commit 5f4fa12040
13 changed files with 1171 additions and 6 deletions

352
assetcomp/assetcomp.odin Normal file
View File

@ -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,
}

View File

@ -14,6 +14,7 @@ Build_Variant :: enum {
Hot_Reload, Hot_Reload,
Desktop, Desktop,
Web, Web,
Assetcomp,
} }
Options :: struct { Options :: struct {
@ -282,6 +283,7 @@ main :: proc() {
"odin", "odin",
"build", "build",
"game", "game",
"-define:NAME_STATIC_INIT=false",
"-define:RAYLIB_SHARED=true", "-define:RAYLIB_SHARED=true",
"-define:PHYSFS_SHARED=true", "-define:PHYSFS_SHARED=true",
"-define:DEV=true", "-define:DEV=true",
@ -371,5 +373,20 @@ main :: proc() {
), ),
"./bin/web/odin.js", "./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, "")
} }
} }

View File

@ -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),
},
)
}

View File

@ -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`,
)
}

View File

@ -6,6 +6,10 @@ import "core:mem"
import "core:strings" import "core:strings"
import "core:sync" 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 Name :: distinct u32
NONE :: Name(0) NONE :: Name(0)
@ -27,6 +31,7 @@ setup_global_container :: proc(cnt: ^Container) {
init :: proc(cnt: ^Container) { init :: proc(cnt: ^Container) {
mem.dynamic_arena_init(&cnt.names_allocator) mem.dynamic_arena_init(&cnt.names_allocator)
assert(len(cnt.names_array) == 0) assert(len(cnt.names_array) == 0)
append(&cnt.names_array, "None") append(&cnt.names_array, "None")
} }
@ -39,6 +44,22 @@ destroy :: proc() {
global_container = nil 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 { from_string :: proc(str: string) -> Name {
sync.atomic_rw_mutex_guard(&global_container.lock) sync.atomic_rw_mutex_guard(&global_container.lock)
existing, ok := global_container.names_lookup[str] existing, ok := global_container.names_lookup[str]

View File

@ -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) curve, err := parse_csv_2d(transmute([]u8)test_invalid_csv, context.temp_allocator)
defer free_all(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) testing.expect_value(t, len(curve.points), 0)
} }

View File

@ -15,6 +15,7 @@
package game package game
import "assets" import "assets"
import "common:name"
import "core:fmt" import "core:fmt"
import "core:hash" import "core:hash"
import "core:log" import "core:log"
@ -26,7 +27,6 @@ import "game:render"
import rl "libs:raylib" import rl "libs:raylib"
import "libs:raylib/rlgl" import "libs:raylib/rlgl"
import "libs:tracy" import "libs:tracy"
import "name"
import "ui" import "ui"
PIXEL_WINDOW_HEIGHT :: 360 PIXEL_WINDOW_HEIGHT :: 360

View File

@ -1,6 +1,7 @@
package physics package physics
import "bvh" import "bvh"
import "common:name"
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
import "core:math" import "core:math"
@ -10,7 +11,6 @@ import "core:slice"
import "core:strings" import "core:strings"
import "game:debug" import "game:debug"
import he "game:halfedge" import he "game:halfedge"
import "game:name"
import "game:ui" import "game:ui"
import rl "libs:raylib" import rl "libs:raylib"
import "libs:raylib/rlgl" import "libs:raylib/rlgl"

View File

@ -2,9 +2,9 @@ package physics
import "bvh" import "bvh"
import "collision" import "collision"
import "common:name"
import lg "core:math/linalg" import lg "core:math/linalg"
import "game:container/spanpool" import "game:container/spanpool"
import "game:name"
import "libs:tracy" import "libs:tracy"
MAX_CONTACTS :: 1024 * 16 MAX_CONTACTS :: 1024 * 16

View File

@ -3,6 +3,7 @@ package physics
import "base:runtime" import "base:runtime"
import "bvh" import "bvh"
import "collision" import "collision"
import "common:name"
import "core:container/bit_array" import "core:container/bit_array"
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
@ -12,7 +13,6 @@ import "core:math/rand"
import "core:slice" import "core:slice"
import "game:debug" import "game:debug"
import he "game:halfedge" import he "game:halfedge"
import "game:name"
import "libs:tracy" import "libs:tracy"
_ :: name _ :: name

BIN
sexp Executable file

Binary file not shown.

23
src_assets/defs.ass Normal file
View File

@ -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")))

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/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