work on trile storage

This commit is contained in:
Tuomas Katajisto 2025-08-31 17:57:56 +03:00
parent 784293bf89
commit 6b62b78595
18 changed files with 10669 additions and 43 deletions

View File

@ -1,7 +1,8 @@
#scope_file
cam : Camera = .{
far = 1000.0
far = 2000.0,
near = 1.0
};
#scope_export

8744
game/resources/triles.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

4
modules/Jaison/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.build/
*.dSYM
/modules
examples/example

3
modules/Jaison/.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "unicode_utils"]
path = unicode_utils
url = git@github.com:rluba/jai-unicode

21
modules/Jaison/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Raphael Luba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,391 @@
#import, file "../module.jai";
main :: () {
{
// Use the temporary allocator so we don't have to worry about individually freeing
// all the allocations.
push_allocator(temp);
typed_parsing();
typed_printing();
generic_parsing();
generic_printing();
rename_by_note();
custom_rename_procedure();
decode_into_struct_with_using();
enum_errors();
}
// Since the program ends here, this doesn't matter, but just setting an example.
reset_temporary_storage();
}
// Data structures for typed parsing/printing
LevelData :: struct {
kind: LevelKind;
flags: LevelFlags;
secret: bool;
player: Entity;
player2: *Entity;
union {
score: float;
score_v2: float;
}
entities: [..] Entity;
floats: [] float;
LevelKind :: enum {
EASY :: 0;
HARD :: 1;
LEGENDARY :: 2;
}
LevelFlags :: enum_flags {
FLAG_A :: 0x1;
FLAG_B :: 0x2;
FLAG_C :: 0x4;
}
}
Entity :: struct {
name: string;
x, y: int;
dirty: bool; @JsonIgnore
}
LEVEL_DATA_1_JSON := #string DONE
{"kind": ".HARD", "flags": "FLAG_A | LevelFlags.FLAG_C", "secret": false,"score":5.5,"player": {"name": "Pat","x": 10,"y": 10},"player2": {"name": "Chris"},"entities": [{"name": "fdsa","x": 0,"y": 0},{"name": "fdsa","x": 0,"y": 0}], "floats": [0.00, 1.11111111111111111, 2.0202, 3e-5, 4.444444, -5.0]}
DONE;
LEVEL_DATA_2_JSON := #string DONE
{"kind": 2, "score_v2": 25.1}
DONE;
Fixed_Size_Data :: struct {
data: [3] int;
}
DATA_ARRAY_1 := #string DONE
{"data": [1, 2, 3]}
DONE;
DATA_ARRAY_2 := #string DONE
{"data": [1, 2]}
DONE;
typed_parsing :: () {
{
success, level := json_parse_string(LEVEL_DATA_1_JSON, LevelData, ignore_unknown=false);
// success, level := json_parse_file("level.json", LevelData, ignore_unknown=false);
assert(success);
log("Typed parsing result 1:\n%\nscore: %\n\n", level, level.score);
assert(level.floats.count == 6);
assert(level.floats[1] > 1); // Regression test, we had a bug here…
}
{
// Test parsing integers into enum slots & alternative union fields
success, level := json_parse_string(LEVEL_DATA_2_JSON, LevelData, ignore_unknown=false);
assert(success);
log("Typed parsing result 2:\n%\nscore: %\n\n", level, level.score);
assert(level.kind == .LEGENDARY);
assert(level.score == 25.1);
}
{
// Test parsing into fixed-sized arrays
success, result := json_parse_string(DATA_ARRAY_1, Fixed_Size_Data, ignore_unknown=false);
assert(success);
log("Typed parsing result 3:\n%\n\n", result);
assert(result.data[0] == 1);
assert(result.data[1] == 2);
assert(result.data[2] == 3);
}
{
// Test parsing into fixed-sized arrays (incorrect size)
success, result := json_parse_string(DATA_ARRAY_2, Fixed_Size_Data, ignore_unknown=false);
assert(!success);
}
}
typed_printing :: () {
level := LevelData.{kind=.LEGENDARY, flags=LevelData.LevelFlags.FLAG_B|.FLAG_C, secret=true, score=500};
level.player = .{name="Pat", x=4, y=4, dirty=true};
array_add(*level.entities, .{name="Chris", x=6, y=6});
json_string := json_write_string(level);
log("Typed printing result:\n%\n\n", json_string);
// success := json_write_file("level.json", level, indent_char="");
// assert(success);
}
generic_parsing :: () {
// In this scenario, some parts of the structure are known, but other parts are not.
json := #string DONE
{
"version": 3,
"entities": [
{
"name": "Player",
"x": 2,
"y": 2,
"player_index": 0
},
{
"name": "Snake",
"x": 4,
"y": 4,
"snake_color": 1
}
],
"stuff": [null, true, false]
}
DONE
success, root := json_parse_string(json);
// success, root := json_parse_file("level.json");
assert(success);
log("Generic parsing result:");
// Print things out, for demonstration purposes
traverse_node :: (node: JSON_Value, depth: int) {
INDENTATION :: 4;
print("% ", node.type);
if node.type == {
case .NULL;
print("\n");
case .BOOLEAN;
print("%\n", node.boolean);
case .NUMBER;
print("%\n", node.number);
case .STRING;
print("%\n", node.str);
case .OBJECT;
print("{\n");
for node.object {
for 1..(depth+1)*INDENTATION print(" ");
print("%: ", it_index);
traverse_node(it, depth+1);
}
for 1..depth*INDENTATION print(" ");
print("}\n");
case .ARRAY;
print("[\n");
for node.array {
for 1..(depth+1)*INDENTATION print(" ");
traverse_node(it, depth + 1);
}
for 1..depth*INDENTATION print(" ");
print("]\n");
}
}
traverse_node(root, 0);
print("\n");
// Convenience function for grabbing object members
get :: (json_val: JSON_Value, key: string, expected_type: JSON_Type) -> JSON_Value {
assert(json_val.type == .OBJECT);
table := json_val.object;
success, val := Hash_Table.table_find_new(table, key);
assert(success);
assert(val.type == expected_type);
return val;
}
// Check for version number that may or may not exist
version: float64 = -1;
assert(root.type == .OBJECT);
success2, val := Hash_Table.table_find_new(root.object, "version");
if success2 {
if val.type == .NUMBER {
version = val.number;
}
}
log("version: %\n", version);
// Traverse a structure we are confident about
for get(root, "entities", .ARRAY).array {
entity_name := get(it, "name", .STRING).str;
x := get(it, "x", .NUMBER).number / 32;
y := get(it, "y", .NUMBER).number / 32;
if entity_name == {
case "Player";
player_index := cast(int) get(it, "player_index", .NUMBER).number;
log("Player with player_index=%\n", player_index);
case "Snake";
snake_color := cast(int) get(it, "snake_color", .NUMBER).number;
log("Snake with snake_color=%\n", snake_color);
case;
//...
}
}
log("\n");
}
generic_printing :: () {
// We want to write JSON with arbitrary structure.
// Create and initialize object
root_obj: JSON_Object;
root := json_value(*root_obj);
// Add music index to object, in certain cases
should_add_music := true;
if should_add_music {
json_set(*root_obj, "music_index", .{type=.NUMBER, number=3});
}
// Create an array of values
temp: [..] JSON_Value;
junk := JSON_Value.{type=.STRING, str="junk"};
array_add(*temp, junk);
array_add(*temp, junk);
// Create json array value
array := JSON_Value.{type=.ARRAY};
array.array = temp;
// Add array to object
json_set(*root_obj, "junk_array", array);
// Print result
json_string := json_write_string(root);
log("Generic_printing result:\n%\n\n", json_string);
//json_write_file("level.json", root);
}
rename_by_note :: () {
// Sometimes the JSON we are parsing contains members with names we cannot use or don´t wanna.
Message :: struct {
value: string;
_context: struct { // We cannot use "context" because it´s a reserved keyword in Jai.
channel: int;
parent: int;
} @JsonName(context) // This member in JSON is "context" but we need to have it as "_context". So we can use the JsonName note to encode and decode it as "context".
}
json := #string DONE
{
"value": "Hello!",
"context": {
"channel": 1,
"parent": 897820
}
}
DONE
success, message_decoded := json_parse_string(json, Message, ignore_unknown = false);
assert(success, "Could not decode message!");
log("Rename_by_note decode result:\n%\n\n", message_decoded);
assert(message_decoded._context.channel == 1);
message_encoded := json_write_string(message_decoded);
log("Rename_by_note encode result:\n%\n\n", message_encoded);
assert(message_encoded.count != 0);
assert(find_index_from_left(message_encoded, "_context") == -1);
}
custom_rename_procedure :: () {
// We can also pass a custom rename procedure for renaming certain members.
rename_to_upper :: (member: *Type_Info_Struct_Member) -> string {
return to_upper_copy(member.name,,temp);
}
player := Entity.{
name="Player",
x = 10,
y = 50
};
player_encoded := json_write_string(player, rename=rename_to_upper);
assert(player_encoded.count != 0);
log("encoded with rename_to_upper:\n%\n\n", player_encoded);
assert(find_index_from_left(player_encoded, "name") == -1);
assert(find_index_from_left(player_encoded, "NAME") != -1);
success, player_decoded := json_parse_string(player_encoded, Entity, ignore_unknown = false, rename = rename_to_upper);
assert(success);
log("decoded with rename_to_upper:\n%\n\n", player_decoded);
assert(player_decoded.name == "Player");
}
decode_into_struct_with_using :: () {
Coordinates :: struct {
x: int;
y: int;
}
Tile :: struct {
color: string;
using coordinates: Coordinates;
}
JSON :: #string END
{
"color": "green",
"x": 1,
"y": 3
}
END
success, tile := json_parse_string(JSON, Tile, ignore_unknown = false);
assert(success);
log("Decoded tile: %", tile);
assert(tile.color == "green");
assert(tile.x == 1);
assert(tile.y == 3);
}
enum_errors :: () {
// Testing error messages when parsing invalid enum values
{
LEVEL_DATA_BROKEN_JSON := #string DONE
{"kind": ".VERY_HARD"}
DONE;
success, level := json_parse_string(LEVEL_DATA_BROKEN_JSON, LevelData, ignore_unknown=false);
assert(!success);
}
{
LEVEL_DATA_BROKEN_JSON := #string DONE
{"flags": "FLAG_A | FLAG_D"}
DONE;
success, level := json_parse_string(LEVEL_DATA_BROKEN_JSON, LevelData, ignore_unknown=false);
assert(!success);
}
}
#import "Basic";
#import "Hash_Table"; // To be able to iterate over node.object
#import "String"; // For to_upper_copy
#import "Compiler"; // For Type_Info_Struct_Member
Hash_Table :: #import "Hash_Table";

370
modules/Jaison/generic.jai Normal file
View File

@ -0,0 +1,370 @@
// Generic JSON parsing/writing functions. Result is always a JSON_Value,
// which is awful to read and even more awful to create for complex structures.
// But its useful for some cases where re-creating the whole JSON structure as
// custom Jai struct types is inconvenient or not possible.
// This generic interface was the very first part I wrote in Jai and hasnt been thorougly tested.
// Tread with care. There may be dragons.
JSON_Type :: enum u8 {
NULL :: 0;
BOOLEAN :: 1;
NUMBER :: 3;
STRING :: 2;
ARRAY :: 5;
OBJECT :: 4;
}
JSON_Value :: struct {
type: JSON_Type;
union {
boolean: bool;
number: float64;
str: string;
array: [] JSON_Value;
object: *JSON_Object;
};
}
JSON_Object :: Table(string, JSON_Value);
json_free :: (using val: JSON_Value) {
if #complete type == {
case .NULL;
case .BOOLEAN;
case .NUMBER;
case .STRING;
free(str);
case .ARRAY;
for array {
json_free(it);
}
array_free(array);
case .OBJECT;
for object {
free(it_index);
json_free(it);
}
deinit(object);
free(object);
}
}
json_parse_string :: (content: string) -> success: bool, JSON_Value {
if !content then return false, .{};
result, remainder, success := parse_value(content);
if !success return false, result;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder.count {
log_error("Unexpected trailing characters: %", remainder);
return false, result;
}
return true, result;
}
// For debug purposes
print_val :: (using val: JSON_Value) {
if #complete type == {
case .NULL;
print("null");
case .BOOLEAN;
print("%", boolean);
case .NUMBER;
print("%", number);
case .STRING;
print("\"%\"", str);
case .ARRAY;
print("[");
for array print_val(it);
print("]");
case .OBJECT;
print("%", <<object);
}
}
json_value :: (str: string) -> JSON_Value {
val: JSON_Value;
val.type =.STRING;
val.str = str;
return val;
}
json_value :: (obj: *JSON_Object) -> JSON_Value {
val: JSON_Value;
val.type =.OBJECT;
val.object = obj;
return val;
}
json_set_null :: (val: *JSON_Value) {
val.type = .NULL;
}
json_set :: (val: *JSON_Value, value: bool) {
val.type = .BOOLEAN;
val.boolean = value;
}
json_set :: (val: *JSON_Value, value: int) {
val.type = .NUMBER;
val.number = cast(float64) value;
}
json_set :: (val: *JSON_Value, value: float64) {
val.type = .NUMBER;
val.number = value;
}
json_set :: (val: *JSON_Value, value: string) {
val.type = .STRING;
val.str = value;
}
json_set :: (val: *JSON_Value, value: [] JSON_Value) {
val.type = .ARRAY;
val.array = value;
}
json_set :: (val: *JSON_Value, value: *JSON_Object) {
val.type = .OBJECT;
val.object = value;
}
json_write_json_value :: (builder: *String_Builder, using val: JSON_Value, indent_char := "\t", level := 0) {
if #complete type == {
case JSON_Type.NULL;
append(builder, "null");
case JSON_Type.BOOLEAN;
append(builder, ifx boolean "true" else "false");
case JSON_Type.NUMBER;
print_item_to_builder(builder, number);
case JSON_Type.STRING;
json_append_escaped(builder, str);
case JSON_Type.ARRAY;
append(builder, "[");
for array {
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
json_write_json_value(builder, it, indent_char, level + 1);
if it_index != array.count - 1 append(builder, ",");
}
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "]");
case JSON_Type.OBJECT;
append(builder, "{");
obj := object;
keys: [..] string;
defer array_free(keys);
array_reserve(*keys, obj.count);
for v, k: <<obj {
array_add(*keys, k);
}
intro_sort(keys, compare);
for keys {
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
json_append_escaped(builder, it);
append(builder, ": ");
found, v := table_find_new(obj, it);
assert(found, "Missing table value %", it);
json_write_json_value(builder, v, indent_char, level + 1);
if it_index != obj.count - 1 append(builder, ",");
}
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "}");
}
}
json_set :: (obj: *JSON_Object, path: string, val: JSON_Value) -> bool {
dotpos := find_index_from_left(path, #char ".");
if dotpos == -1 {
table_set(obj, path, val);
return true;
}
next := slice(path, 0, dotpos);
remainder := advance(path, dotpos + 1);
if !next.count return false;
if !remainder.count return false;
success, next_value := table_find_new(obj, next);
next_obj: *JSON_Object;
if success {
if next_value.type != JSON_Type.OBJECT return false;
next_obj = xx next_value.object;
} else {
next_obj = cast(*JSON_Object) alloc(size_of(JSON_Object));
memset(next_obj, 0, size_of(JSON_Object));
next_value = json_value(next_obj);
table_add(obj, next, next_value);
}
return json_set(next_obj, remainder, val);
}
get_as :: (val: JSON_Value, $T: Type) -> T {
#insert #run () -> string {
if T == bool {
return #string END
assert(val.type == .BOOLEAN, "Expected a % but got %", T, val.type);
return val.boolean;
END;
} else if T == float || T == float64 {
return #string END
assert(val.type == .NUMBER, "Expected a % but got %", T, val.type);
return cast(T) val.number;
END;
} else if T == string {
return #string END
assert(val.type == .STRING, "Expected a % but got %", T, val.type);
return val.str;
END;
} else if T == [] JSON_Value {
return #string END
assert(val.type == .ARRAY, "Expected a % but got %", T, val.type);
return val.array;
END;
} else if T == JSON_Object {
return #string END
assert(val.type == .OBJECT, "Expected a % but got %", T, val.type);
return <<val.object;
END;
} else {
compiler_report("Unsupported type");
return "";
}
}();
}
#scope_module
parse_value :: (to_parse: string) -> JSON_Value, remainder: string, success: bool {
result: JSON_Value;
remainder := trim_left(to_parse, WHITESPACE_CHARS);
success := false;
if remainder[0] == {
case #char "n";
remainder, success = expect_and_slice(remainder, "null");
if !success return result, remainder, false;
json_set_null(*result);
result.type = JSON_Type.NULL;
return result, remainder, true;
case #char "t";
remainder, success = expect_and_slice(remainder, "true");
if !success return result, remainder, false;
json_set(*result, true);
return result, remainder, true;
case #char "f";
remainder, success = expect_and_slice(remainder, "false");
if !success return result, remainder, false;
json_set(*result, false);
return result, remainder, true;
case #char "\"";
str: string;
str, remainder, success = parse_string(remainder);
json_set(*result, str);
case #char "[";
result.type = JSON_Type.ARRAY;
result.array, remainder, success = parse_array(remainder);
case #char "{";
obj := cast(*JSON_Object) alloc(size_of(JSON_Object));
<<obj, remainder, success = parse_object(remainder);
result = json_value(obj);
case;
result.type = JSON_Type.NUMBER;
result.number, success, remainder = string_to_float64(remainder);
}
return result, remainder, success;
}
parse_array:: (str: string) -> result: [] JSON_Value, remainder: string, success: bool {
assert(str[0] == #char "[", "Invalid object start %", str);
remainder := advance(str);
result: [..] JSON_Value;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] == #char "]" {
remainder = advance(remainder);
return result, remainder, true;
}
while true {
value: JSON_Value;
success: bool;
value, remainder, success = parse_value(remainder);
if !success return result, remainder, false;
array_add(*result, value);
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] != #char "," break;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
}
if remainder[0] != #char "]" return result, remainder, false;
remainder = advance(remainder);
return result, remainder, true;
}
parse_object :: (str: string) -> result: JSON_Object, remainder: string, success: bool {
assert(str[0] == #char "{", "Invalid object start %", str);
remainder := advance(str);
result: JSON_Object;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] == #char "}" {
remainder = advance(remainder);
return result, remainder, true;
}
init(*result, 32);
while true {
if remainder[0] != #char "\"" return result, remainder, false;
key: string;
value: JSON_Value;
success: bool;
key, remainder, success = parse_string(remainder);
if !success return result, remainder, false;
existing := table_find_pointer(*result, key);
if existing return result, remainder, false;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] != #char ":" return result, remainder, false;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
value, remainder, success = parse_value(remainder);
if !success return result, remainder, false;
table_add(*result, key, value);
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] != #char "," break;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
}
if remainder[0] != #char "}" return result, remainder, false;
remainder = advance(remainder);
return result, remainder, true;
}
#import "Compiler";

219
modules/Jaison/module.jai Normal file
View File

@ -0,0 +1,219 @@
// This file contains just the JSON serialization functions. See generic.jai and typed.jai for parse fuctions.
// Generates a JSON string from either a JSON_Value or any custom type.
// "indent_char" does what it says on the tin.
// "ignore" is only used for custom types to determine which properties of your custom type should be serialized.
// The default ignore function ignores all struct members that have the note @JsonIgnore.
// "rename" is used for renaming certain members.
// It gets called with the Type_Info_Struct_Member and must return the new name of the field.
// The default procedure rename members by their @JsonName note. Eg: @JsonName(renamed_member).
json_write_string :: (value: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note) -> string {
builder: String_Builder;
defer free_buffers(*builder);
json_append_value(*builder, value, indent_char, ignore, rename);
return builder_to_string(*builder);
}
json_append_value :: (builder: *String_Builder, val: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note) {
#if T == JSON_Value {
json_write_json_value(builder, val, indent_char);
} else {
info := type_info(T);
json_write_native(builder, *val, info, indent_char, ignore, rename);
}
}
// This function is useful if you have a JSON template string and just want to
// safely insert a value without having to replicate the full json structure in Jai.
// The return value does NOT include quotes around the string.
//
// Example:
// JSON_TEMPLATE :: #string END
// {
// "complicated": {
// "json": {
// "structure": {
// "for_a_stupid_api": {
// "that_needs": [
// {"a_deeply_nested_value": "%1"}
// ]
// }
// }
// }
// }
// }
// END
// escaped_value := json_escape_string(my_unsafe_value);
// defer free(escaped_value);
// json_str := print(JSON_TEMPLATE, escaped_value);
json_escape_string :: (str: string) -> string {
builder: String_Builder;
defer free_buffers(*builder);
json_append_escaped(*builder, str);
escaped := builder_to_string(*builder);
return escaped;
}
Ignore_Proc :: #type (member: *Type_Info_Struct_Member) -> bool;
Rename_Proc :: #type (member: *Type_Info_Struct_Member) -> string;
ignore_by_note :: (member: *Type_Info_Struct_Member) -> bool {
for note: member.notes {
if note == "JsonIgnore" return true;
}
return false;
}
rename_by_note :: (member: *Type_Info_Struct_Member) -> string {
for note: member.notes {
if !begins_with(note, "JsonName(") continue;
if note.count <= 10 || note[note.count-1] != #char ")" {
log_error("Invalid JsonName note format. Expected a name in parenthesis, but the note was \"%\".", note);
continue;
}
return slice(note, 9, note.count-10);
}
return member.name;
}
#scope_module
WHITESPACE_CHARS :: " \t\n\r";
#load "generic.jai";
#load "typed.jai";
json_append_escaped :: (builder: *String_Builder, str: string) {
remaining := str;
next_pos := index_of_illegal_string_char(remaining);
append(builder, "\"");
while (next_pos >= 0) {
append(builder, slice(remaining, 0, next_pos));
if remaining[next_pos] == {
case #char "\\";
append(builder, "\\\\");
case #char "\"";
append(builder, "\\\"");
case #char "\n";
append(builder, "\\n");
case #char "\r";
append(builder, "\\r");
case #char "\t";
append(builder, "\\t");
case;
// ToDo: handle illegal multi-byte characters
// print("Escaping: %\n\n", slice(remaining, next_pos, remaining.count - next_pos));
print_to_builder(builder, "\\u%", formatInt(remaining[next_pos], base=16, minimum_digits=4));
}
remaining = advance(remaining, next_pos + 1);
next_pos = index_of_illegal_string_char(remaining);
}
append(builder, remaining);
append(builder, "\"");
}
index_of_illegal_string_char :: (str: string) -> s64 {
for 0..str.count - 1 {
if str[it] == #char "\\" || str[it] == #char "\"" || str[it] <= 0x1F {
return it;
}
}
return -1;
}
expect_and_slice :: (str: string, expected: string) -> remainder: string, success: bool {
if str.count < expected.count || !equal(slice(str, 0, expected.count), expected) {
log_error("Unexpected token. Expected \"%\" but got: %", expected, str);
return str, false;
}
remainder := advance(str, expected.count);
return remainder, true;
}
parse_string :: (str: string) -> result: string, remainder: string, success: bool {
assert(str[0] == #char "\"", "Invalid string start %", str);
inside := advance(str);
needsUnescape := false;
while inside[0] != #char "\"" {
if inside.count < 2 return "", str, false;
if inside[0] == #char "\\" {
needsUnescape = true;
if inside.count < 2 return "", str, false;
advance(*inside);
}
advance(*inside);
}
length := inside.data - str.data - 1;
result := slice(str, 1, length);
if needsUnescape {
success: bool;
result, success = unescape(result);
if !success return "", str, false;
} else {
result = copy_string(result);
}
remainder := slice(str, length + 2, str.count - length - 2);
return result, remainder, true;
}
unescape :: (str: string) -> result: string, success: bool {
result := alloc_string(str.count);
rc := 0;
for i: 0..str.count-1 {
if str[i] != #char "\\" {
// Check for invalid characters for JSON
if str[i] < 0x20 return "", false;
result[rc] = str[i];
rc += 1;
} else {
if i == str.count - 1 return "", false;
i += 1;
if str[i] == {
case #char "\""; #through;
case #char "/"; #through;
case #char "\\";
result[rc] = str[i];
rc += 1;
case #char "b";
result[rc] = 0x08;
rc += 1;
case #char "f";
result[rc] = 0x0c;
rc += 1;
case #char "n";
result[rc] = #char "\n";
rc += 1;
case #char "r";
result[rc] = #char "\r";
rc += 1;
case #char "t";
result[rc] = #char "\t";
rc += 1;
case #char "u";
if i + 4 >= str.count return "", false;
unicode_char, success := parse_unicode(slice(str, i + 1, 4));
if !success return "", false;
utf8_len := encode_utf8(unicode_char, *(result.data[rc]));
rc += utf8_len;
i += 4;
case;
return "", false;
}
}
}
result.count = rc;
return result, true;
}
#import "Basic";
#import "String";
#import "Hash_Table";
#import,dir "./unicode_utils";
#import "IntroSort";

63
modules/Jaison/readme.md Normal file
View File

@ -0,0 +1,63 @@
# JSON serialization / deserialization module for Jai
*Attention: This version requires Jai beta 0.1.080!*
Use `v1.0.0` for older betas.
This module offers two interfaces:
* one uses a "generic tree" built from `JSON_Value`
* the other is a typed version that serializes / deserializes your custom data structures.
The generic `JSON_Value` graphs are a pain to consume and even worse to produce by hand.
But they allow you to parse any JSON, even if you dont know the structure (or cant reproduce it in Jai because it varies).
The typed interface is what you want for most cases.
## Parsing / Deserialization
Parsing is as simple as:
```Jai
// Typed version:
success, result := json_parse_string(json_str, Your_Type_To_Parse_Into);
// … or if you want to get a generic structure back:
success, result := json_parse_string(json_str);
```
There are also a convenience functions for parsing if the JSON data is in a file:
```Jai
success, result := json_parse_file(json_filename, Your_Type_To_Parse_Into);
// … or
success, result := json_parse_file(json_filename);
```
See [`typed.jai`](./typed.jai) and [`generic.jai`](./generic.jai) for details and additional options.
### Mixed typed and generic data
If you dont know the structure of some subfield of your `Your_Type_To_Parse_Into` structure, but still want to get these values from the JSON data,
you can declare these fields as the generic type `JSON_Value` or `*JSON_Value` and the generic parse function will take over at that point:
```
Your_Type_To_Parse_Into :: struct {
name: string;
age: int;
something_we_dont_know_much_about: *JSON_Value; // Whatever structure hides in the JSON, it will be parsed into JSON_Value.
}
```
## Printing / Serialization
Generating a string works the same for both interfaces:
```Jai
json_str := json_write_string(my_value);
```
where `my_value` is either a `JSON_Value` or any other data structure.
See [`module.jai`](./module.jai) for details and additional parameters.
## Dependencies
This module uses [the `unicode_utils` module](https://github.com/rluba/jai-unicode), which is included as a submodule.

625
modules/Jaison/typed.jai Normal file
View File

@ -0,0 +1,625 @@
// Parse a JSON string into the given Type.
// All members of Type that are not present in the JSON are kept at their default values.
// All fields in the JSON that have no corresponding member in Type are ignored by default
// but you can pass ignore_unknown = false to fail instead.
json_parse_string :: (content: string, $T: Type, ignore_unknown := true, rename := rename_by_note) -> success: bool, T {
result: T;
if !content then return false, result;
info := type_info(T);
remainder, success := parse_value(content, cast(*u8)*result, info, ignore_unknown, "", rename=rename);
if !success return false, result;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder.count {
log_error("Unexpected trailing characters: %", remainder);
return false, result;
}
return true, result;
}
json_parse_file :: (filename: string, $T: Type, ignore_unknown := true, rename := rename_by_note) -> success: bool, T {
file_data, success := read_entire_file(filename);
result: T;
if !success {
log_error("Could not read file: %", filename);
return false, result;
}
defer free(file_data);
if (context.log_level >= .VERBOSE) {
log("Read file: %", success);
}
success, result = json_parse_string(file_data, T, ignore_unknown, rename=rename);
return success, result;
}
json_write_native :: (builder: *String_Builder, data: *void, info: *Type_Info, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, level := 0) {
if info.type == {
case .BOOL;
append(builder, ifx <<(cast(*bool) data) "true" else "false");
case .INTEGER; #through;
case .FLOAT;
any_val: Any;
any_val.type = info;
any_val.value_pointer = data;
print_item_to_builder(builder, any_val);
case .ENUM;
any_val: Any;
any_val.type = info;
any_val.value_pointer = data;
append(builder, #char "\"");
print_item_to_builder(builder, any_val);
append(builder, #char "\"");
case .STRING;
json_append_escaped(builder, <<(cast(*string) data));
case .ARRAY;
info_array := cast(*Type_Info_Array) info;
element_size := info_array.element_type.runtime_size;
assert(element_size != -1);
stride := element_size;
array_data := data;
array_count := info_array.array_count;
if info_array.array_count == -1 {
array_count = << cast(*s64) data;
array_dest: **void = data + 8;
array_data = << array_dest;
}
append(builder, "[");
if array_data {
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
for 0..array_count-1 {
json_write_native(builder, array_data, info_array.element_type, indent_char, ignore, rename, level + 1);
if it != array_count - 1 append(builder, ",");
array_data += stride;
}
}
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "]");
case .STRUCT;
struct_info := cast(*Type_Info_Struct) info;
if is_generic_json_value(info) {
value := cast(*JSON_Value) data;
json_write_json_value(builder, <<value, indent_char, level);
} else {
append(builder, #char "{");
first := true;
json_write_native_members(builder, data, struct_info.members, indent_char, ignore, rename, level, *first);
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "}");
}
case .POINTER;
ptr_info := cast(*Type_Info_Pointer) info;
ptr := << cast(**void) data;
if ptr {
json_write_native(builder, ptr, ptr_info.pointer_to, indent_char, ignore, rename, level);
} else {
append(builder, "null");
}
case;
assert(false, "Unsupported type: %", info.type);
}
}
#scope_file
json_write_native_members :: (builder: *String_Builder, data: *void, members: [] Type_Info_Struct_Member, indent_char := "\t", ignore := ignore_by_note, rename: Rename_Proc, level := 0, first: *bool) {
for * member: members {
if member.flags & .CONSTANT continue;
if ignore(member) continue;
if (member.type.type == .STRUCT && member.flags & .USING) {
info := cast(*Type_Info_Struct) member.type;
json_write_native_members(builder, data + member.offset_in_bytes, info.members, indent_char, ignore, rename, level, first);
} else {
if !<<first append(builder, ",");
<<first = false;
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
renamed_name := rename(member);
name := ifx renamed_name.count > 0 renamed_name else member.name;
json_append_escaped(builder, name);
append(builder, ": ");
json_write_native(builder, data + member.offset_in_bytes, member.type, indent_char, ignore, rename, level + 1);
}
}
}
is_generic_json_value_or_pointer_to_it :: (info: *Type_Info) -> bool {
value_info := info;
if info.type == .POINTER {
pointer_info := cast(*Type_Info_Pointer) info;
value_info = pointer_info.pointer_to;
}
return is_generic_json_value(info);
}
is_generic_json_value :: (info: *Type_Info) -> bool {
return info == type_info(JSON_Value);
}
parse_value :: (to_parse: string, slot: *u8, info: *Type_Info, ignore_unknown: bool, field_name: string, rename: Rename_Proc) -> remainder: string, success: bool {
remainder := trim_left(to_parse, WHITESPACE_CHARS);
success := true;
prepare_slot :: (expected_type: Type_Info_Tag, info: *Type_Info, slot: *u8, to_parse: string) -> *u8, success: bool, is_generic: bool, info: *Type_Info {
value_info := info;
if info.type == .POINTER {
pointer_info := cast(*Type_Info_Pointer) info;
value_info = pointer_info.pointer_to;
}
if info.type == .ENUM {
info_enum := cast(*Type_Info_Enum)info;
value_info = info_enum.internal_type;
}
is_generic := is_generic_json_value(value_info);
if !is_generic && value_info.type != expected_type {
teaser := to_parse;
if teaser.count > 50 teaser.count = 50;
builder: String_Builder;
print_type_to_builder(*builder, info);
type_name := builder_to_string(*builder,, temp);
log_error("Cannot parse % value into type \"%\". Remaining input is: %…", expected_type, type_name, teaser);
return null, false, false, value_info;
}
if info.type == .POINTER {
value_slot := alloc(value_info.runtime_size);
initializer: (*void) #no_context;
if value_info.type == .STRUCT {
struct_info := cast(*Type_Info_Struct) value_info;
initializer = struct_info.initializer;
}
if initializer {
initializer(value_slot);
} else {
memset(value_slot, 0, value_info.runtime_size);
}
<<cast(**u8)slot = value_slot;
return value_slot, true, is_generic, value_info;
} else {
return slot, true, is_generic, value_info;
}
}
is_generic: bool;
if remainder[0] == {
case #char "n";
remainder, success = expect_and_slice(remainder, "null");
if !success return remainder, false;
if slot {
if info.type == .POINTER {
<<cast(**void) slot = null;
} else {
builder: String_Builder;
print_type_to_builder(*builder, info);
type_name := builder_to_string(*builder,, temp);
log_error("Got NULL value for non-pointer type \"%\" of field \"%\". Keeping default value instead.", type_name, field_name);
}
}
return remainder, true;
case #char "t";
remainder, success = expect_and_slice(remainder, "true");
if !success return remainder, false;
if slot {
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.BOOL, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, true);
} else {
<<cast(*bool)value_slot = true;
}
}
}
case #char "f";
remainder, success = expect_and_slice(remainder, "false");
if !success return remainder, false;
if slot {
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.BOOL, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, false);
} else {
<<cast(*bool)value_slot = false;
}
}
}
case #char "\"";
if slot && info && info.type == .ENUM {
info_enum := cast(*Type_Info_Enum)info;
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.INTEGER, info_enum.internal_type, slot, to_parse);
remainder, success = parse_enum_string(remainder, value_slot, info_enum);
} else {
value: string;
value, remainder, success = parse_string(remainder);
stored := false;
defer if !stored free(value);
if success && slot {
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.STRING, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, value);
} else {
<<cast(*string)value_slot = value;
}
stored = true;
}
}
}
case #char "[";
value_slot: *u8;
value_info: *Type_Info;
if slot {
value_slot, success, is_generic, value_info = prepare_slot(.ARRAY, info, slot, to_parse);
}
if success {
if is_generic {
value: [] JSON_Value;
value, remainder, success = parse_array(remainder);
json_set(cast(*JSON_Value)value_slot, value);
} else {
remainder, success = parse_array(remainder, value_slot, cast(*Type_Info_Array) value_info, ignore_unknown, rename=rename);
}
}
case #char "{";
value_slot: *u8;
value_info: *Type_Info;
if slot {
value_slot, success, is_generic, value_info = prepare_slot(.STRUCT, info, slot, to_parse);
}
if success {
if is_generic {
value := New(JSON_Object);
<<value, remainder, success = parse_object(remainder);
json_set(cast(*JSON_Value)value_slot, value);
} else {
remainder, success = parse_object(remainder, value_slot, cast(*Type_Info_Struct) value_info, ignore_unknown, rename=rename);
}
}
case;
if slot == null || info.type == .FLOAT || is_generic_json_value_or_pointer_to_it(info) {
float_value: float64;
float_value, success, remainder = string_to_float64(remainder);
if success && slot {
value_slot: *u8;
value_info: *Type_Info;
value_slot, success, is_generic, value_info = prepare_slot(.FLOAT, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, float_value);
} else {
if value_info.runtime_size == 4 {
(<< cast(*float) slot) = cast(float) float_value;
} else {
assert(value_info.runtime_size == 8);
(<< cast(*float64) slot) = float_value;
}
}
}
}
} else {
if slot {
value_slot: *u8;
value_info: *Type_Info;
value_slot, success, is_generic, value_info = prepare_slot(.INTEGER, info, slot, to_parse);
if success {
if is_generic {
int_value: s64;
int_value, success, remainder = string_to_int(remainder, T = s64);
if success {
json_set(cast(*JSON_Value)value_slot, int_value);
} else {
log_error("Could not parse \"%\" as an integer.", to_parse);
}
} else {
info_int := cast(*Type_Info_Integer) value_info;
success, remainder = parse_and_write_integer(info_int, value_slot, to_parse);
}
}
} else {
int_value: s64;
int_value, success, remainder = string_to_int(remainder, T = s64);
if !success {
log_error("Could not parse \"%\" as an integer.", to_parse);
}
}
}
}
return remainder, success;
}
parse_enum_string :: (str: string, slot: *u8, info_enum: *Type_Info_Enum) -> remainder: string, success: bool {
value, remainder, success := parse_string(str);
defer free(value);
if !success return remainder, false;
// Parse by members' names
normalize_enum_value :: inline (name: string) -> string #expand {
normalized := trim(name);
if normalized.count > info_enum.name.count && starts_with(normalized, info_enum.name) && normalized[info_enum.name.count] == #char "." {
normalized = slice(normalized, info_enum.name.count+1, normalized.count-info_enum.name.count-1);
} else if starts_with(normalized, ".") {
normalized = slice(normalized, 1, normalized.count-1);
}
return normalized;
}
int_info := info_enum.internal_type;
int_value: s64;
if info_enum.enum_type_flags & .FLAGS {
values := split(value, "|",, temp);
for v: values {
name := normalize_enum_value(v);
found_name := false;
for info_enum.names {
if name == it {
found_name = true;
int_value |= info_enum.values[it_index];
break;
}
}
if !found_name {
log_error("Enum \"%\" does not contain a member named \"%\".", info_enum.name, name);
success = false;
}
}
} else {
success = false;
name := normalize_enum_value(value);
for info_enum.names {
if name == it {
int_value = info_enum.values[it_index];
success = true;
break;
}
}
if !success {
log_error("Enum \"%\" does not contain a member named \"%\".", info_enum.name, name);
}
}
if success {
if int_info.signed {
valid, low, high := Reflection.range_check_and_store(int_value, int_info, slot);
if !valid {
log_error("The value '%' is out of range. (It must be between % and %.)", int_value, low, high);
return remainder, false;
}
} else {
valid, low, high := Reflection.range_check_and_store(cast(u64) int_value, int_info, slot);
if !valid {
log_error("The value '%' is out of range. (It must be between % and %.)", int_value, low, high);
return remainder, false;
}
}
}
return remainder, success;
}
parse_array :: (str: string, slot: *u8, info: *Type_Info_Array, ignore_unknown: bool, rename: Rename_Proc) -> remainder: string, success: bool {
element_size: int;
if slot {
element_size = info.element_type.runtime_size;
assert(element_size != -1, "Unknown element size");
}
assert(str[0] == #char "[", "Invalid object start %", str);
remainder := advance(str);
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] == #char "]" {
remainder = advance(remainder);
// @Robustness: Do we need to zero out the array?
return remainder, true;
}
if slot {
array: Resizable_Array;
initializer: (*void) #no_context;
if info.element_type.type == .STRUCT {
struct_info := cast(*Type_Info_Struct) info.element_type;
initializer = struct_info.initializer;
}
while true {
maybe_grow(*array, element_size);
element_data := array.data + array.count * element_size;
if initializer {
initializer(element_data);
} else {
memset(element_data, 0, element_size);
}
success: bool;
remainder, success = parse_value(remainder, element_data, info.element_type, ignore_unknown, "", rename=rename);
if !success return remainder, false;
array.count += 1;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] != #char "," break;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
}
if info.array_type == .VIEW {
view := (cast(*Array_View_64) slot);
view.count = array.count;
view.data = array.data;
} else if info.array_count == -1 {
// Resizable array
<<(cast(*Resizable_Array) slot) = array;
} else {
// Fixed-size array
if (info.array_count != array.count) {
log_error("Expected array of size %, but found array of size %\n", info.array_count, array.count);
return remainder, false;
}
memcpy(slot, array.data, array.count * element_size);
}
} else {
while true {
success: bool;
remainder, success = parse_value(remainder, null, null, ignore_unknown, "", rename=rename);
if !success return remainder, false;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] != #char "," break;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
}
}
if remainder[0] != #char "]" return remainder, false;
remainder = advance(remainder);
return remainder, true;
}
Member_Offset :: struct {
member: *Type_Info_Struct_Member;
offset_in_bytes: s64;
}
// This procedure is somewhat copied from Basic.get_field.
fill_member_table :: (table: *Table(string, Member_Offset), info: *Type_Info_Struct, rename: Rename_Proc, base_offset := 0) {
for * member: info.members {
offset := base_offset + member.offset_in_bytes;
name := rename(member);
assert(!table_find_pointer(table, name), "Redeclaration of member \"%\": % vs. %", name, <<member, <<table_find_pointer(table, name));
table_set(table, name, .{member, offset});
if (member.flags & .USING) && (member.type.type == .STRUCT) {
fill_member_table(table, cast(*Type_Info_Struct)member.type, rename, offset);
}
}
}
parse_object :: (str: string, slot: *u8, info: *Type_Info_Struct, ignore_unknown: bool, rename: Rename_Proc) -> remainder: string, success: bool {
assert(str[0] == #char "{", "Invalid object start %", str);
remainder := advance(str);
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] == #char "}" {
remainder = advance(remainder);
return remainder, true;
}
// @Speed: Building this table every time is pretty silly.
// We should probably either not build it at all or cache it somewhere.
member_table: Table(string, Member_Offset);
init(*member_table);
defer deinit(*member_table);
if info fill_member_table(*member_table, info, rename);
while true {
if remainder[0] != #char "\"" return remainder, false;
key: string;
success: bool;
key, remainder, success = parse_string(remainder);
if !success return remainder, false;
defer free(key);
member_found, member_offset := table_find_new(*member_table, key);
member_slot: *u8;
member_info: *Type_Info;
if member_found {
member_slot = slot + member_offset.offset_in_bytes;
member_info = member_offset.member.type;
} else if !ignore_unknown {
log_error("Missing member % in %", key, <<info);
return remainder, false;
}
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] != #char ":" return remainder, false;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
remainder, success = parse_value(remainder, member_slot, member_info, ignore_unknown, key, rename);
if !success return remainder, false;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder[0] != #char "," break;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
}
if remainder[0] != #char "}" return remainder, false;
remainder = advance(remainder);
return remainder, true;
}
parse_and_write_integer :: (info: *Type_Info_Integer, pointer: *void, string_value: string) -> bool, remainder: string {
if info.signed {
success, remainder := parse_and_write_integer(info, pointer, string_value, signed = true);
return success, remainder;
} else {
success, remainder := parse_and_write_integer(info, pointer, string_value, signed = false);
return success, remainder;
}
}
parse_and_write_integer :: (info: *Type_Info_Integer, pointer: *void, string_value: string, $signed: bool) -> bool, remainder: string {
#if signed {
Int_Type :: s64;
} else {
Int_Type :: u64;
}
int_value, int_success, remainder := string_to_int(string_value, T = Int_Type);
if !int_success {
#if signed {
log_error("Could not parse \"%\" as an integer.", string_value);
} else {
log_error("Could not parse \"%\" as an unsigned integer.", string_value);
}
return false, remainder;
}
valid, low, high := Reflection.range_check_and_store(int_value, info, pointer);
if !valid {
log_error("The value '%' is out of range. (It must be between % and %.)", int_value, low, high);
return false, remainder;
}
return true, remainder;
}
Reflection :: #import "Reflection";

View File

@ -0,0 +1,59 @@
is_utf_cont :: inline (b: u8) -> bool {
return (b & 0xc0) == 0x80;
}
parse_unicode :: (str: string) -> result: u16, success: bool {
val, success, remainder := string_to_int(str, base = 16);
if !success || val > 0xFFFF || remainder.count return 0, false;
return xx val, true;
}
encode_utf8 :: (val: u16, result: *u8) -> len: u8 {
if val & 0xF800 {
result[0] = xx (0xE0 | ((val & 0xF000) >> 12));
result[1] = xx (0x80 | ((val & 0x0FC0) >> 6));
result[2] = xx (0x80 | (val & 0x003F));
return 3;
} else if val & 0x0F80 {
result[0] = xx (0xC0 | ((val & 0x0FC0) >> 6));
result[1] = xx (0x80 | (val & 0x003F));
return 2;
} else {
result[0] = xx (val & 0x7F);
return 1;
}
}
is_valid_utf8 :: (str: string) -> valid:bool {
for i: 0..str.count-1 {
cur := str[i];
if cur >= 0x80 {
// Must be between 0xc2 and 0xf4 inclusive to be valid
if (cur - 0xc2) > (0xf4 - 0xc2) return false;
if cur < 0xe0 { // 2-byte sequence
if i + 1 >= str.count || !is_utf_cont(str[i+1]) return false;
i += 1;
} else if cur < 0xf0 { // 3-byte sequence
if i + 2 >= str.count || !is_utf_cont(str[i+1]) || !is_utf_cont(str[i+2]) return false;
// Check for surrogate chars
if cur == 0xed && str[i+1] > 0x9f return false;
// ToDo: Check if total >= 0x800
// uc = ((uc & 0xf)<<12) | ((*str & 0x3f)<<6) | (str[1] & 0x3f);
i += 2;
} else { // 4-byte sequence
if i + 3 >= str.count || !is_utf_cont(str[i+1]) || !is_utf_cont(str[i+2]) || !is_utf_cont(str[i+3]) return false;
// Make sure its in valid range (0x10000 - 0x10ffff)
if cur == 0xf0 && str[i + 1] < 0x90 return false;
if cur == 0xf4 && str[i + 1] > 0x8f return false;
i += 3;
}
}
}
return true;
}
#scope_file
#import "Basic";

View File

@ -132,7 +132,6 @@ flush_arb_commands :: () {
sgl_viewport(0,0,w,h,true);
sg_apply_scissor_rect(s.x, s.y, s.w, s.h, true);
case .REMOVE_SCISSOR;
print("REMOVING scissor!\n");
w,h := get_window_size();
sg_apply_scissor_rect(0, 0, w, w, true);
case .DRAW_TEXT;

View File

@ -41,6 +41,7 @@ Level_Editor_Tab :: enum {
};
current_tab : Level_Editor_Tab = .TOOLS;
current_trile : *Trile = null;
get_level_editor_camera :: () -> Camera {
camera: Camera;
@ -196,19 +197,6 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
}
}
draw_trile_picker :: (theme: *GR.Overall_Theme) {
r := GR.get_rect(ui_w(85,85), ui_h(5,0), ui_w(15, 15), ui_h(95, 0));
draw_bg_rectangle(r, theme);
tpt := get_trile_table_ptr();
r.h = ui_h(4,4);
count := 0;
for v : tpt {
GR.button(r, v.name, *theme.button_theme, count);
count += 1;
r.y += r.h;
}
}
#scope_export
tick_level_editor :: () {
@ -229,7 +217,7 @@ draw_level_editor :: () {
vs_params.mvp = mvp.floats;
vs_params.camera = cam.position.component;
trilegfx := get_trile_gfx("test");
trilegfx := get_trile_gfx(editor_current_trile.name);
sg_update_buffer(gPipelines.trile.bind.vertex_buffers[3], *(sg_range.{
ptr = positions.data,

View File

@ -38,39 +38,40 @@ current_tab : Trile_Editor_Tab = .TOOLSET;
current_tool : Trile_Editor_Tool = .PAINT;
current_mode : Trile_Editor_Tool_Mode = .AREA;
current_trile : *Trile;
#scope_export
new_trile :: (name: string) {
print("Create new trile: %\n", name);
set_trile_gfx(current_trile.name, generate_trile_gfx_matias(current_trile));
ntrile :: (name: string) {
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
newt : Trile;
newt.name = sprint("%", name);
set_trile(newt.name, newt);
current_trile = get_trile(newt.name);
editor_current_trile = get_trile(newt.name);
} @Command
edit_trile :: (name: string) {
set_trile_gfx(current_trile.name, generate_trile_gfx_matias(current_trile));
current_trile = get_trile(name);
ltrile :: (name: string) {
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
nt := get_trile(name);
if !nt {
console_add_output_line("Failed to load a trile with that name...");
return;
}
editor_current_trile = nt;
} @Command
#scope_file
apply_tool_to_trixel :: (x: s64, y: s64, z: s64) {
if current_tool == .PAINT {
current_trile.trixels[x][y][z].material.color = current_color;
current_trile.trixels[x][y][z].material.metallic = metallic;
current_trile.trixels[x][y][z].material.roughness = roughness;
current_trile.trixels[x][y][z].material.emittance = emittance;
editor_current_trile.trixels[x][y][z].material.color = current_color;
editor_current_trile.trixels[x][y][z].material.metallic = metallic;
editor_current_trile.trixels[x][y][z].material.roughness = roughness;
editor_current_trile.trixels[x][y][z].material.emittance = emittance;
}
if current_tool == .ADD {
current_trile.trixels[x][y][z].empty = false;
editor_current_trile.trixels[x][y][z].empty = false;
}
if current_tool == .REMOVE {
current_trile.trixels[x][y][z].empty = true;
editor_current_trile.trixels[x][y][z].empty = true;
}
}
@ -128,13 +129,13 @@ current_color : Vector3 = .{1.0, 0.0, 0.0};
reset_trile :: () {
newt : Trile;
set_trile("test", newt);
current_trile = get_trile("test");
editor_current_trile = get_trile("test");
} @Command
tick_trile_editor :: () {
if console_open_ignore_input then return;
if !current_trile then current_trile = get_trile("test");
if !editor_current_trile then editor_current_trile = get_trile("test");
if input_button_states[Key_Code.MOUSE_BUTTON_LEFT] & .START {
handle_tool_click();
@ -216,7 +217,7 @@ draw_tool_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
GR.slider(r, *brush_radius, 0, 12, 1, *theme.slider_theme);
r.y += r.h * 2;
if GR.button(r, "Save and gen", *theme.button_theme) {
set_trile_gfx("test", generate_trile_gfx_matias(current_trile));
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
};
}
@ -258,6 +259,7 @@ draw_trile_editor :: () {
}
draw_trile :: () {
// if editor_current_trile == null then return;
cam := get_trile_editor_camera();
mvp := create_viewproj(*cam);
@ -273,6 +275,7 @@ draw_trile :: () {
min_distance : float = 999.0;
ray := get_mouse_ray(*cam);
hovered_trixel_x = -1;
@ -282,7 +285,7 @@ draw_trile :: () {
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
if current_trile.trixels[x][y][z].empty then continue;
if editor_current_trile.trixels[x][y][z].empty then continue;
hit := does_ray_hit_cube(ray, .{ .{x * TRIXEL_SIZE, y * TRIXEL_SIZE, z * TRIXEL_SIZE}, .{TRIXEL_SIZE, TRIXEL_SIZE, TRIXEL_SIZE}});
if hit.hit && hit.distance < min_distance {
hovered_trixel_x = x;
@ -299,13 +302,13 @@ draw_trile :: () {
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
if current_trile.trixels[x][y][z].empty then continue;
if editor_current_trile.trixels[x][y][z].empty then continue;
trixels[trixel_count].pos.x = x * (1.0 / 16.0) + TRIXEL_SIZE_HALF;
trixels[trixel_count].pos.y = y * (1.0 / 16.0) + TRIXEL_SIZE_HALF;
trixels[trixel_count].pos.z = z * (1.0 / 16.0) + TRIXEL_SIZE_HALF;
trixels[trixel_count].pos.w = 1.0;
trixel_color := current_trile.trixels[x][y][z].material.color;
trixel_color := editor_current_trile.trixels[x][y][z].material.color;
if hovered_trixel_x == x &&
hovered_trixel_y == y &&
@ -313,7 +316,7 @@ draw_trile :: () {
trixel_color = .{1.0, 0.0, 0.0};
}
trixels[trixel_count].col = .{trixel_color.x, trixel_color.y, trixel_color.z, material_encode_to_float(current_trile.trixels[x][y][z].material)};
trixels[trixel_count].col = .{trixel_color.x, trixel_color.y, trixel_color.z, material_encode_to_float(editor_current_trile.trixels[x][y][z].material)};
trixel_count += 1;
}
}
@ -394,4 +397,6 @@ draw_trile_editor_ui :: (theme: *GR.Overall_Theme) {
case .TOOLSET;
draw_tool_tab(theme, r);
}
draw_trile_picker(theme);
}

View File

@ -82,6 +82,19 @@ create_texture_from_pack :: (path: string) -> sg_image {
return img;
}
load_string_from_pack :: (path: string) -> string {
ok, entry := table_find_new(*g_asset_pack.lookup, path);
if !ok {
print("Failed to load string from pack: %\n", path);
return "";
}
s: string;
s.data = entry.data.data;
s.count = entry.data.count;
return s;
}
asset_list :: () -> string #expand {
count := 0;
for v : g_asset_pack.lookup {

View File

@ -101,7 +101,17 @@ init_after_asset_pack :: () {
ui_init_font_fields(*state.font_default);
init_ui();
ltriles();
tt := get_trile_table_ptr();
if tt.count == 0 {
set_trile("test", Trile.{});
} else {
name : string;
for v : tt {
name = v.name;
break;
}
}
init_editor();
game_init();
}

View File

@ -7,7 +7,7 @@ trile_table : Table(string, Trile);
#scope_export
editor_current_trile : *Trile = null;
Trile_GFX :: struct {
trixel_colors : sg_image;
@ -58,6 +58,7 @@ set_trile_gfx :: (name: string, gfx: Trile_GFX, skip_preexist_check: bool = fals
}
set_trile :: (name: string, trile: Trile) {
print("Setting trile with name: %\n",name);
table_set(*trile_table, name, trile);
}
@ -70,7 +71,7 @@ get_trile :: (name: string) -> (*Trile, success: bool) {
return trileptr, true;
}
list_trile :: () -> string {
lstrile :: () -> string {
count := 0;
for v : trile_table {
console_add_output_line(sprint("%", v.name));
@ -79,6 +80,31 @@ list_trile :: () -> string {
return tprint("% triles", count);
} @Command
striles :: () {
Jaison :: #import "Jaison";
triles : [..]TrileSerialize;
triles.allocator = temp;
for v : trile_table {
array_add(*triles, trile_to_serialize_form(v));
}
#if OS != .WASM {
file :: #import "File";
json := Jaison.json_write_string(triles, " ");
file.write_entire_file("./game/resources/triles.json", json);
}
} @Command
ltriles :: () {
Jaison :: #import "Jaison";
s := load_string_from_pack("./game/resources/triles.json");
success, triles := Jaison.json_parse_string(s, [..]TrileSerialize,, temp);
for triles {
set_trile(sprint("%",it.name), trile_from_serialize_form(it));
print("Loaded %\n", it.name);
}
} @Command
Material :: struct {
addRoughness : u8 = 0;
roughness : u8 = 4;
@ -97,6 +123,45 @@ Trile :: struct {
trixels : [16][16][16] Trixel;
};
TrixelSerialize :: [5]u8;
TrileSerialize :: struct {
name : string = "test";
trixels : [16][16][16] TrixelSerialize;
};
trile_to_serialize_form :: (t: Trile) -> TrileSerialize {
ts := TrileSerialize.{
name = t.name,
};
for i: 0..15 {
for j: 0..15 {
for k: 0..15 {
r,g,b,a := material_to_rgba(t.trixels[i][j][k].material);
ts.trixels[i][j][k] = .[ifx t.trixels[i][j][k].empty then cast(u8)0 else cast(u8)1, r,g,b,a];
}
}
}
return ts;
}
trile_from_serialize_form :: (ts: TrileSerialize) -> Trile {
t := Trile.{
name = sprint("%",ts.name)
};
for i: 0..15 {
for j: 0..15 {
for k: 0..15 {
matinfo := ts.trixels[i][j][k];
mat := material_from_rgba(matinfo[1], matinfo[2], matinfo[3], matinfo[4]);
t.trixels[i][j][k].material = mat;
t.trixels[i][j][k].empty = cast(bool) matinfo[0];
}
}
}
return t;
}
material_encode_to_char :: (mat: Material) -> u8 {
return (mat.addRoughness & 0x1) | ((mat.emittance & 0x3) << 1) |
((mat.metallic & 0x3) << 3) | ((mat.roughness & 0x7) << 5);
@ -106,6 +171,14 @@ material_encode_to_float :: (mat: Material) -> float {
return cast(float)(material_encode_to_char(mat)) / 255.0;
}
material_decode_from_char :: (packedMaterial: u8) -> Material {
mat : Material;
mat.emittance = (packedMaterial >> 1) & 3;
mat.roughness = (packedMaterial >> 5) & 7;
mat.metallic = (packedMaterial >> 3) & 3;
return mat;
}
material_to_rgba :: (mat: Material) -> (r: u8, g: u8, b: u8, a: u8) {
r : u8 = cast(u8) (mat.color.x * 255.0);
g : u8 = cast(u8) (mat.color.y * 255.0);
@ -114,4 +187,26 @@ material_to_rgba :: (mat: Material) -> (r: u8, g: u8, b: u8, a: u8) {
return r,g,b,a;
}
material_from_rgba :: (r: u8, g: u8, b: u8, a: u8) -> Material {
mat := material_decode_from_char(a);
mat.color.x = (cast(float) r)/255.0;
mat.color.y = (cast(float) g)/255.0;
mat.color.z = (cast(float) b)/255.0;
return mat;
}
draw_trile_picker :: (theme: *GR.Overall_Theme) {
r := GR.get_rect(ui_w(85,85), ui_h(5,0), ui_w(15, 15), ui_h(95, 0));
draw_bg_rectangle(r, theme);
tpt := get_trile_table_ptr();
r.h = ui_h(4,4);
count := 0;
for v : tpt {
print("Current trile name: %\n", editor_current_trile.name);
if GR.button(r, v.name, *t_button_selectable(theme, editor_current_trile != null && editor_current_trile.name == v.name), count) {
editor_current_trile = get_trile(v.name);
}
count += 1;
r.y += r.h;
}
}