#scope_file Pool :: #import "Pool"; CHUNK_SIZE :: 32; Current_World :: struct { world : World; pool : Pool.Pool; // A memory pool to allocate stuff for the lifetime of this level being active. For example RDMs. valid : bool = false; }; current_world : Current_World; #scope_export Chunk_Key :: struct { x: s32; y: s32; z: s32; } operator == :: (a: Chunk_Key, b: Chunk_Key) -> bool { return a.x == b.x && a.y == b.y && a.z == b.z; } chunk_key_hash :: (key: Chunk_Key) -> u32 { h := cast(u32) 2166136261; h = (h ^ (cast,no_check(u32) key.x)) * 16777619; h = (h ^ (cast,no_check(u32) key.y)) * 16777619; h = (h ^ (cast,no_check(u32) key.z)) * 16777619; return h; } chunk_key_compare :: (a: Chunk_Key, b: Chunk_Key) -> bool { return a.x == b.x && a.y == b.y && a.z == b.z; } Trile_Instance :: struct { x: u8; // local position within chunk (0..31) y: u8; z: u8; orientation: u8; // 0..23, index into cube rotation group } Chunk_Trile_Group :: struct { trile_name: string; instances: [..]Trile_Instance; } Chunk :: struct { coord: Chunk_Key; groups: [..]Chunk_Trile_Group; rdm_atlas: sg_image; rdm_lookup: sg_image; rdm_valid: bool; rdm_dirty: bool; rdm_atlas_path: string; rdm_lookup_path: string; #if !FLAG_RELEASE_BUILD { rdm_lookup_cpu: []float; rdm_lookup_w: s32; rdm_lookup_h: s32; } } Editor_Note :: struct { position : Chunk_Key; text : string; } Rdm_Instance_Override :: struct { x : s32; y : s32; z : s32; size_override : s32; quality_override : s32; } World :: struct { name : string; conf : World_Config; chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare); emitter_instances : [..]Particle_Emitter_Instance; notes : [..]Editor_Note; rdm_overrides : [..]Rdm_Instance_Override; } get_rdm_instance_override :: (world: *World, x: s32, y: s32, z: s32) -> (size_override: s32, quality_override: s32) { for world.rdm_overrides { if it.x == x && it.y == y && it.z == z then return it.size_override, it.quality_override; } return 0, 0; } set_rdm_instance_override :: (world: *World, x: s32, y: s32, z: s32, size_override: s32, quality_override: s32) { for *world.rdm_overrides { if it.x == x && it.y == y && it.z == z { if size_override == 0 && quality_override == 0 { array_ordered_remove_by_index(*world.rdm_overrides, it_index); } else { it.size_override = size_override; it.quality_override = quality_override; } return; } } if size_override != 0 || quality_override != 0 { array_add(*world.rdm_overrides, .{x=x, y=y, z=z, size_override=size_override, quality_override=quality_override}); } } // Convert a world-space integer position to chunk coordinate. world_to_chunk_coord :: (wx: s32, wy: s32, wz: s32) -> Chunk_Key { return .{ x = floor_div(wx, CHUNK_SIZE), y = floor_div(wy, CHUNK_SIZE), z = floor_div(wz, CHUNK_SIZE), }; } // Convert a world-space integer position to local position within its chunk (0..31). world_to_local :: (wx: s32, wy: s32, wz: s32) -> (u8, u8, u8) { return cast(u8) floor_mod(wx, CHUNK_SIZE), cast(u8) floor_mod(wy, CHUNK_SIZE), cast(u8) floor_mod(wz, CHUNK_SIZE); } // Convert chunk coord + local position back to world position. chunk_local_to_world :: (chunk: Chunk_Key, lx: u8, ly: u8, lz: u8) -> (s32, s32, s32) { return chunk.x * CHUNK_SIZE + cast(s32) lx, chunk.y * CHUNK_SIZE + cast(s32) ly, chunk.z * CHUNK_SIZE + cast(s32) lz; } // Floor division that handles negatives correctly (rounds toward -infinity). floor_div :: (a: s32, b: s32) -> s32 { d := a / b; r := a % b; if (r != 0) && ((r ^ b) < 0) then d -= 1; return d; } // Floor modulo that always returns a non-negative result. floor_mod :: (a: s32, b: s32) -> s32 { r := a % b; if (r != 0) && ((r ^ b) < 0) then r += b; return r; } nworld :: (name: string) { unload_current_world(); current_world.world = .{}; current_world.world.name = sprint("%", name); current_world.valid = true; } @Command; lworld :: (name: string) { load_world(name); } @Command; init_world_system :: () { Pool.set_allocators(*current_world.pool); } unload_current_world :: () { if !current_world.valid then return; rdm_loader_cancel_all(); for *chunk: current_world.world.chunks { if chunk.rdm_valid { sg_destroy_image(chunk.rdm_atlas); sg_destroy_image(chunk.rdm_lookup); #if !FLAG_RELEASE_BUILD { if chunk.rdm_lookup_cpu.data then free(chunk.rdm_lookup_cpu.data); chunk.rdm_lookup_cpu = .{}; } chunk.rdm_atlas = .{}; chunk.rdm_lookup = .{}; chunk.rdm_valid = false; } for *group: chunk.groups { array_free(group.instances); } array_free(chunk.groups); } deinit(*current_world.world.chunks); Pool.reset(*current_world.pool); current_world.valid = false; } set_loaded_world :: (world: World) { unload_current_world(); current_world.world = world; current_world.valid = true; resolve_emitter_definitions(*current_world.world); } resolve_emitter_definitions :: (world: *World) { for *inst: world.emitter_instances { inst.definition = get_emitter_def(inst.definition_name); } } clear_world :: () { if !current_world.valid then return; for *chunk: current_world.world.chunks { for *group: chunk.groups { array_free(group.instances); } array_free(chunk.groups); } deinit(*current_world.world.chunks); current_world.world.chunks = .{}; } @Command get_current_world :: () -> *Current_World { return *current_world; } // --- Binary serialization (.world format) --- WORLD_MAGIC :: u32.[0x4C575254][0]; // "TRWL" as little-endian u32 WORLD_VERSION :: cast(u16) 3; World_Json :: struct { version : s32; name : string; config : World_Json_Config; chunks : [..]World_Json_Chunk; emitters : [..]World_Json_Emitter; notes : [..]World_Json_Note; rdm_overrides : [..]World_Json_Rdm_Override; } World_Json_Config :: struct { skyBase : [3]float; skyTop : [3]float; sunDisk : [3]float; horizonHalo : [3]float; sunHalo : [3]float; sunLightColor : [3]float; sunPosition : [3]float; sunIntensity : float; skyIntensity : float; hasClouds : s32; planeHeight : float; animatePlaneHeight : s32; waterColor : [3]float; deepColor : [3]float; waterShininess : float; rdmDiffSaturation : float; } World_Json_Chunk :: struct { x : s32; y : s32; z : s32; offset : s32; size : s32; rdm_atlas : string; rdm_lookup : string; } World_Json_Emitter :: struct { definition_name : string; position : [3]float; offset : [3]float; } World_Json_Note :: struct { text : string; position : [3]s32; } World_Json_Rdm_Override :: struct { x : s32; y : s32; z : s32; size_override : s32; quality_override : s32; } // World_Config serialized as a fixed-size binary blob. // We serialize it field-by-field to avoid padding issues. World_Config_Binary :: struct { sky_base: [3]float; sky_top: [3]float; sun_disk: [3]float; horizon_halo: [3]float; sun_halo: [3]float; sun_light_color: [3]float; sun_position: [3]float; sun_intensity: float; sky_intensity: float; has_clouds: s32; plane_height: float; animate_plane_height: s32; water_color: [3]float; deep_color: [3]float; water_shininess: float; rdm_diff_saturation: float; } world_config_to_binary :: (conf: *World_Config) -> World_Config_Binary { b: World_Config_Binary; b.sky_base = conf.skyBase.component; b.sky_top = conf.skyTop.component; b.sun_disk = conf.sunDisk.component; b.horizon_halo = conf.horizonHalo.component; b.sun_halo = conf.sunHalo.component; b.sun_light_color = conf.sunLightColor.component; b.sun_position = conf.sunPosition.component; b.sun_intensity = conf.sunIntensity; b.sky_intensity = conf.skyIntensity; b.has_clouds = conf.hasClouds; b.plane_height = conf.planeHeight; b.animate_plane_height = conf.animatePlaneHeight; b.water_color = conf.waterColor.component; b.deep_color = conf.deepColor.component; b.water_shininess = conf.waterShininess; b.rdm_diff_saturation = conf.rdmDiffSaturation; return b; } world_config_from_binary :: (b: *World_Config_Binary) -> World_Config { conf: World_Config; conf.skyBase.component = b.sky_base; conf.skyTop.component = b.sky_top; conf.sunDisk.component = b.sun_disk; conf.horizonHalo.component = b.horizon_halo; conf.sunHalo.component = b.sun_halo; conf.sunLightColor.component = b.sun_light_color; conf.sunPosition.component = b.sun_position; conf.sunIntensity = b.sun_intensity; conf.skyIntensity = b.sky_intensity; conf.hasClouds = b.has_clouds; conf.planeHeight = b.plane_height; conf.animatePlaneHeight = b.animate_plane_height; conf.waterColor.component = b.water_color; conf.deepColor.component = b.deep_color; conf.waterShininess = b.water_shininess; conf.rdmDiffSaturation = b.rdm_diff_saturation; return conf; } write_bytes :: (builder: *String_Builder, data: *void, count: s64) { s: string; s.data = data; s.count = count; append(builder, s); } write_value :: (builder: *String_Builder, value: $T) { v := value; write_bytes(builder, *v, size_of(T)); } read_value :: (data: []u8, cursor: *s64, $T: Type) -> T { result: T; assert(cursor.* + size_of(T) <= data.count, "read_value: out of bounds"); memcpy(*result, data.data + cursor.*, size_of(T)); cursor.* += size_of(T); return result; } read_string :: (data: []u8, cursor: *s64, len: s64) -> string { assert(cursor.* + len <= data.count, "read_string: out of bounds"); result := sprint("%", string.{count = len, data = data.data + cursor.*}); cursor.* += len; return result; } sworld :: () { if !current_world.valid { log_error("Cannot save: no world loaded"); return; } #if OS != .WASM { file :: #import "File"; name := current_world.world.name; dir := tprint("%/worlds/%", GAME_RESOURCES_DIR, name); file.make_directory_if_it_does_not_exist(dir, recursive = true); json_data, bin_data := save_world(*current_world.world); file.write_entire_file(tprint("%/world.json", dir), json_data); file.write_entire_file(tprint("%/chunks.bin", dir), bin_data); log_info("Saved world '%' (json=% bytes, bin=% bytes)", name, json_data.count, bin_data.count); } } @Command world_config_to_json :: (conf: *World_Config) -> World_Json_Config { jc: World_Json_Config; jc.skyBase = conf.skyBase.component; jc.skyTop = conf.skyTop.component; jc.sunDisk = conf.sunDisk.component; jc.horizonHalo = conf.horizonHalo.component; jc.sunHalo = conf.sunHalo.component; jc.sunLightColor = conf.sunLightColor.component; jc.sunPosition = conf.sunPosition.component; jc.sunIntensity = conf.sunIntensity; jc.skyIntensity = conf.skyIntensity; jc.hasClouds = conf.hasClouds; jc.planeHeight = conf.planeHeight; jc.animatePlaneHeight = conf.animatePlaneHeight; jc.waterColor = conf.waterColor.component; jc.deepColor = conf.deepColor.component; jc.waterShininess = conf.waterShininess; jc.rdmDiffSaturation = conf.rdmDiffSaturation; return jc; } world_config_from_json :: (jc: *World_Json_Config) -> World_Config { conf: World_Config; conf.skyBase.component = jc.skyBase; conf.skyTop.component = jc.skyTop; conf.sunDisk.component = jc.sunDisk; conf.horizonHalo.component = jc.horizonHalo; conf.sunHalo.component = jc.sunHalo; conf.sunLightColor.component = jc.sunLightColor; conf.sunPosition.component = jc.sunPosition; conf.sunIntensity = jc.sunIntensity; conf.skyIntensity = jc.skyIntensity; conf.hasClouds = jc.hasClouds; conf.planeHeight = jc.planeHeight; conf.animatePlaneHeight = jc.animatePlaneHeight; conf.waterColor.component = jc.waterColor; conf.deepColor.component = jc.deepColor; conf.waterShininess = ifx jc.waterShininess > 0 then jc.waterShininess else 64.0; conf.rdmDiffSaturation = ifx jc.rdmDiffSaturation > 0 then jc.rdmDiffSaturation else 1.0; return conf; } save_world :: (world: *World) -> (json: string, chunks_bin: string) { bin_builder: String_Builder; Chunk_Save_Entry :: struct { coord: Chunk_Key; offset: s32; size: s32; rdm_atlas_path: string; rdm_lookup_path: string; } chunk_entries: [..]Chunk_Save_Entry; chunk_entries.allocator = temp; running_offset: s32 = 0; for chunk: world.chunks { if chunk.groups.count == 0 then continue; chunk_builder: String_Builder; chunk_builder.allocator = temp; num_types := cast(u16) chunk.groups.count; write_value(*chunk_builder, num_types); for group: chunk.groups { gname_len := cast(u16) group.trile_name.count; write_value(*chunk_builder, gname_len); append(*chunk_builder, group.trile_name); count := cast(u16) group.instances.count; write_value(*chunk_builder, count); for inst: group.instances { write_value(*chunk_builder, inst); } } chunk_data := builder_to_string(*chunk_builder,, temp); data_size := cast(s32) chunk_data.count; atlas_path := chunk.rdm_atlas_path; lookup_path := chunk.rdm_lookup_path; if !atlas_path.count { atlas_path = tprint("%_%_%.rdm_atlas", chunk.coord.x, chunk.coord.y, chunk.coord.z); lookup_path = tprint("%_%_%.rdm_lookup", chunk.coord.x, chunk.coord.y, chunk.coord.z); } array_add(*chunk_entries, .{ coord = chunk.coord, offset = running_offset, size = data_size, rdm_atlas_path = atlas_path, rdm_lookup_path = lookup_path, }); append(*bin_builder, chunk_data); running_offset += data_size; } wj: World_Json; wj.version = 4; wj.name = world.name; wj.config = world_config_to_json(*world.conf); for entry: chunk_entries { jc: World_Json_Chunk; jc.x = entry.coord.x; jc.y = entry.coord.y; jc.z = entry.coord.z; jc.offset = entry.offset; jc.size = entry.size; jc.rdm_atlas = entry.rdm_atlas_path; jc.rdm_lookup = entry.rdm_lookup_path; array_add(*wj.chunks, jc); } for inst: world.emitter_instances { je: World_Json_Emitter; je.definition_name = inst.definition_name; je.position = inst.position.component; je.offset = inst.offset.component; array_add(*wj.emitters, je); } for note: world.notes { jn: World_Json_Note; jn.text = note.text; jn.position = .[note.position.x, note.position.y, note.position.z]; array_add(*wj.notes, jn); } for ov: world.rdm_overrides { array_add(*wj.rdm_overrides, .{x=ov.x, y=ov.y, z=ov.z, size_override=ov.size_override, quality_override=ov.quality_override}); } json_str := Jaison.json_write_string(wj, " "); return json_str, builder_to_string(*bin_builder); } load_world_from_json :: (json_str: string, chunk_bin: []u8) -> (World, bool) { world: World; success, wj := Jaison.json_parse_string(json_str, World_Json); if !success { log_error("Failed to parse world JSON"); return world, false; } world.name = sprint("%", wj.name); world.conf = world_config_from_json(*wj.config); for jc: wj.chunks { chunk: Chunk; chunk.coord = .{x = jc.x, y = jc.y, z = jc.z}; chunk.rdm_atlas_path = sprint("%", jc.rdm_atlas); chunk.rdm_lookup_path = sprint("%", jc.rdm_lookup); offset := cast(s64) jc.offset; size := cast(s64) jc.size; if offset + size > chunk_bin.count { log_error("Chunk data out of bounds: offset=%, size=%, bin=%", offset, size, chunk_bin.count); return world, false; } chunk_data: []u8; chunk_data.data = chunk_bin.data + offset; chunk_data.count = size; chunk_cursor: s64 = 0; num_types := read_value(chunk_data, *chunk_cursor, u16); for t: 0..cast(s64)num_types-1 { group: Chunk_Trile_Group; gname_len := cast(s64) read_value(chunk_data, *chunk_cursor, u16); group.trile_name = read_string(chunk_data, *chunk_cursor, gname_len); count := cast(s64) read_value(chunk_data, *chunk_cursor, u16); for i: 0..count-1 { inst := read_value(chunk_data, *chunk_cursor, Trile_Instance); array_add(*group.instances, inst); } array_add(*chunk.groups, group); } table_set(*world.chunks, chunk.coord, chunk); } for je: wj.emitters { inst: Particle_Emitter_Instance; inst.definition_name = sprint("%", je.definition_name); inst.position.component = je.position; inst.offset.component = je.offset; inst.active = true; array_add(*world.emitter_instances, inst); } for jn: wj.notes { note: Editor_Note; note.text = sprint("%", jn.text); note.position = .{x = jn.position[0], y = jn.position[1], z = jn.position[2]}; array_add(*world.notes, note); } for jov: wj.rdm_overrides { array_add(*world.rdm_overrides, .{x=jov.x, y=jov.y, z=jov.z, size_override=jov.size_override, quality_override=jov.quality_override}); } return world, true; } load_world_from_data :: (data: []u8) -> (World, bool) { world: World; cursor: s64 = 0; if data.count < size_of(u32) + size_of(u16) { log_error("World file too small"); return world, false; } // Header magic := read_value(data, *cursor, u32); if magic != WORLD_MAGIC { log_error("Invalid world file magic"); return world, false; } version := read_value(data, *cursor, u16); if version != 1 && version != 2 && version != 3 { log_error("Unsupported world version: %", version); return world, false; } name_len := cast(s64) read_value(data, *cursor, u16); world.name = read_string(data, *cursor, name_len); // World config conf_bin := read_value(data, *cursor, World_Config_Binary); world.conf = world_config_from_binary(*conf_bin); num_chunks := read_value(data, *cursor, u32); // Read chunk table Chunk_Table_Entry :: struct { coord: Chunk_Key; offset: u32; size: u32; } chunk_table: [..]Chunk_Table_Entry; chunk_table.allocator = temp; for i: 0..cast(s64)num_chunks-1 { entry: Chunk_Table_Entry; entry.coord.x = read_value(data, *cursor, s32); entry.coord.y = read_value(data, *cursor, s32); entry.coord.z = read_value(data, *cursor, s32); entry.offset = read_value(data, *cursor, u32); entry.size = read_value(data, *cursor, u32); array_add(*chunk_table, entry); } // Read chunk data for entry: chunk_table { chunk_cursor: s64 = cast(s64) entry.offset; chunk: Chunk; chunk.coord = entry.coord; num_types := read_value(data, *chunk_cursor, u16); for t: 0..cast(s64)num_types-1 { group: Chunk_Trile_Group; gname_len := cast(s64) read_value(data, *chunk_cursor, u16); group.trile_name = read_string(data, *chunk_cursor, gname_len); count := cast(s64) read_value(data, *chunk_cursor, u16); for i: 0..count-1 { inst := read_value(data, *chunk_cursor, Trile_Instance); array_add(*group.instances, inst); } array_add(*chunk.groups, group); } table_set(*world.chunks, chunk.coord, chunk); } if chunk_table.count > 0 { last := chunk_table[chunk_table.count - 1]; cursor = cast(s64)(last.offset + last.size); } if version >= 2 && cursor < data.count { num_emitters := cast(s64) read_value(data, *cursor, u16); for i: 0..num_emitters-1 { inst: Particle_Emitter_Instance; name_len := cast(s64) read_value(data, *cursor, u16); inst.definition_name = read_string(data, *cursor, name_len); inst.position.x = read_value(data, *cursor, float); inst.position.y = read_value(data, *cursor, float); inst.position.z = read_value(data, *cursor, float); inst.active = true; array_add(*world.emitter_instances, inst); } } if version >= 3 && cursor < data.count { num_notes := cast(s64) read_value(data, *cursor, u16); for i: 0..num_notes-1 { note: Editor_Note; text_len := cast(s64) read_value(data, *cursor, u16); note.text = read_string(data, *cursor, text_len); note.position.x = read_value(data, *cursor, s32); note.position.y = read_value(data, *cursor, s32); note.position.z = read_value(data, *cursor, s32); array_add(*world.notes, note); } } return world, true; } World_Config :: struct { // All of the @Notes are for the autoedit functionality. skyBase : Vector3 = .{0.38, 0.81, 0.95}; @Color skyTop : Vector3 = .{0.17, 0.4, 0.95}; @Color sunDisk : Vector3 = .{1.0, 1.0, 1.0}; @Color horizonHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color sunHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color sunLightColor : Vector3 = .{1.0, 1.0, 1.0}; @Color sunPosition : Vector3 = #run normalize(Vector3.{0.2, 0.3, 0.4}); sunIntensity : float = 2.0; @Slider,0,100,0.5 skyIntensity : float = 1.0; @Slider,0,10,0.5 hasClouds : s32 = 1; @Slider,0,1,1 planeHeight : float = 0.0; @Slider,0,3,0.1 animatePlaneHeight : s32 = 1; @Slider,0,1,1 waterColor : Vector3 = .{1.0, 1.0, 1.0}; @Color // @ToDo: sensible default values. deepColor : Vector3 = .{1.0, 1.0, 1.0}; @Color // @ToDo: sensible default values. waterShininess : float = 64.0; @Slider,1,512,8 rdmDiffSaturation : float = 1.0; @Slider,0,2,0.05 hsv_lighting : s32 = 1; @Slider,0,1,1 // ambientColor : Vector3 = .{1.0, 1.0, 1.0}; @Color // ambientIntensity : float = 0.3; @Slider,0,3,0.1 } // Copies over all the fields of our world config into a given shader type. // Requires that the shader type has all of the fields the world config has. world_config_to_shader_type :: (wc: *World_Config, data: *$T) { generate_copy_code :: () -> string { builder : String_Builder; ti_src := type_info(World_Config); ti_dst := type_info(T); for src_member: ti_src.members { has_field := false; for dst_member: ti_dst.members { if dst_member.name == src_member.name { has_field = true; break; } } if !has_field continue; if src_member.type == type_info(Vector3) then print_to_builder(*builder, "data.% = wc.%.component;\n", src_member.name, src_member.name); else print_to_builder(*builder, "data.% = wc.%;\n", src_member.name, src_member.name); } return builder_to_string(*builder); } data.time = xx get_time(); #insert #run,stallable generate_copy_code(); } effective_plane_height :: (wc: *World_Config) -> float { if wc.animatePlaneHeight { return wc.planeHeight * (1.0 + sin(cast(float)get_time() * 0.5) * 0.1); } return wc.planeHeight; } draw_world_picker :: (r_in: GR.Rect, theme: *GR.Overall_Theme) { r := r_in; r.h = ui_h(4,4); #if OS != .WASM { File_Utilities :: #import "File_Utilities"; world_names: [..]string; world_names.allocator = temp; dir_visitor :: (info: *File_Utilities.File_Visit_Info, names: *[..]string) { if info.short_name == "world.json" || info.short_name == "index.world" { #import "String"; suffix := ifx info.short_name == "world.json" then "/world.json" else "/index.world"; _, left, _ := split_from_right(info.full_name, suffix); _, _, name := split_from_right(left, "/"); if name.count > 0 { for names.* { if it == name then return; } array_add(names, name); } } } File_Utilities.visit_files(tprint("%/worlds", GAME_RESOURCES_DIR), true, *world_names, dir_visitor); count := 0; for name: world_names { is_current := current_world.valid && current_world.world.name == name; if GR.button(r, name, *t_button_selectable(theme, is_current), count) { lworld(name); } count += 1; r.y += r.h; } } else { if current_world.valid { GR.label(r, tprint("Current: %", current_world.world.name), *theme.label_theme); } else { GR.label(r, "No world loaded", *theme.label_theme); } } }