571 lines
18 KiB
Plaintext
571 lines
18 KiB
Plaintext
#scope_file
|
|
|
|
#import "String";
|
|
hash :: #import "Hash";
|
|
Pool :: #import "Pool";
|
|
|
|
#load "loaders.jai";
|
|
|
|
// MAX_PACK_SIZE, MAX_WORLD_SIZE, MAX_CHUNKS_SIZE, MAX_SHGRID_SIZE,
|
|
// MAX_RDM_ATLAS_SIZE, MAX_RDM_MANIFEST_SIZE are injected as build strings
|
|
// by first.jai after scanning the actual asset files at compile time.
|
|
|
|
MAX_FETCH_SLOTS :: 16;
|
|
|
|
#scope_export
|
|
|
|
// Replaces the old should_block / should_block_engine bool pair.
|
|
Load_Priority :: enum u8 {
|
|
BLOCK_ENGINE; // must finish before the first game frame
|
|
LOADING_SCREEN; // show loading screen while pending
|
|
BACKGROUND; // stream silently; game runs normally
|
|
}
|
|
|
|
Fetch_Type :: enum {
|
|
PACK;
|
|
WORLD;
|
|
WORLD_CHUNKS;
|
|
RDM_ATLAS;
|
|
RDM_MANIFEST;
|
|
SHGRID;
|
|
}
|
|
|
|
Fetch_Request :: struct {
|
|
type : Fetch_Type;
|
|
path : string;
|
|
priority : Load_Priority;
|
|
|
|
// PACK
|
|
pack_name : string;
|
|
|
|
// WORLD / RDM / SH
|
|
world_name : string;
|
|
chunk_key : Chunk_Key;
|
|
|
|
// WORLD_CHUNKS: JSON carried forward from the WORLD fetch
|
|
world_json_data : []u8;
|
|
}
|
|
|
|
// One in-flight sfetch request. `buf` is allocated when dispatched and
|
|
// freed (or handed to Loaded_Pack) when the callback fires.
|
|
Fetch_Slot :: struct {
|
|
occupied : bool;
|
|
req : Fetch_Request;
|
|
buf : []u8;
|
|
}
|
|
|
|
Asset_Manager :: struct {
|
|
slots : [MAX_FETCH_SLOTS]Fetch_Slot;
|
|
queue : [..]Fetch_Request;
|
|
loadedPacks : [..]Loaded_Pack;
|
|
pending_hot_reload : bool;
|
|
hot_reload_unpause_pending : bool;
|
|
}
|
|
|
|
g_asset_manager : Asset_Manager;
|
|
|
|
#scope_file
|
|
|
|
#load "rdm_loader.jai";
|
|
#load "sh_loader.jai";
|
|
|
|
fetch_type_buffer_size :: (type: Fetch_Type) -> s64 {
|
|
if #complete type == {
|
|
case .PACK; return MAX_PACK_SIZE;
|
|
case .WORLD; return MAX_WORLD_SIZE;
|
|
case .WORLD_CHUNKS; return MAX_CHUNKS_SIZE;
|
|
case .RDM_ATLAS; return MAX_RDM_ATLAS_SIZE;
|
|
case .RDM_MANIFEST; return MAX_RDM_MANIFEST_SIZE;
|
|
case .SHGRID; return MAX_SHGRID_SIZE;
|
|
}
|
|
}
|
|
|
|
fetch_callback :: (res: *sfetch_response_t) #c_call {
|
|
push_context,defer_pop default_context;
|
|
|
|
// sokol copied the slot index into res.user_data when we sent the request.
|
|
slot_idx := (cast(*u32) res.user_data).*;
|
|
slot := *g_asset_manager.slots[slot_idx];
|
|
|
|
if !res.finished then return;
|
|
|
|
slot.occupied = false;
|
|
req := slot.req;
|
|
|
|
if res.failed {
|
|
handle_fetch_failed(*req);
|
|
free(slot.buf.data);
|
|
slot.buf = .{};
|
|
return;
|
|
}
|
|
|
|
data: []u8;
|
|
data.data = cast(*u8) res.data.ptr;
|
|
data.count = cast(s64) res.data.size;
|
|
|
|
process_completed_fetch(*req, data);
|
|
|
|
if req.type == .PACK {
|
|
// process_completed_fetch transferred ownership to Loaded_Pack.data_buffer.
|
|
slot.buf = .{};
|
|
} else {
|
|
free(slot.buf.data);
|
|
slot.buf = .{};
|
|
}
|
|
}
|
|
|
|
handle_fetch_failed :: (req: *Fetch_Request) {
|
|
if req.type == {
|
|
|
|
case .PACK;
|
|
log_error("Failed to load pack '%'", req.pack_name);
|
|
|
|
case .WORLD;
|
|
if ends_with(req.path, "world.json") {
|
|
fallback: Fetch_Request;
|
|
fallback.type = .WORLD;
|
|
fallback.priority = req.priority;
|
|
fallback.world_name = req.world_name;
|
|
fallback.path = sprint("%/worlds/%/index.world", GAME_RESOURCES_DIR, req.world_name);
|
|
array_add(*g_asset_manager.queue, fallback);
|
|
return;
|
|
}
|
|
log_error("Failed to load world '%'", req.world_name);
|
|
|
|
case .WORLD_CHUNKS;
|
|
free(req.world_json_data.data);
|
|
log_error("Failed to load chunks.bin for world '%'", req.world_name);
|
|
|
|
case .RDM_ATLAS;
|
|
log_info("RDM: no global atlas for world '%' (ok if none baked yet)", req.world_name);
|
|
|
|
case .RDM_MANIFEST;
|
|
log_info("RDM: no manifest for world '%' (ok if none baked yet)", req.world_name);
|
|
|
|
case .SHGRID;
|
|
sh_loader_handle_failed(req);
|
|
|
|
}
|
|
}
|
|
|
|
process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
|
|
if req.type == {
|
|
|
|
case .PACK;
|
|
pack: Loaded_Pack;
|
|
Pool.set_allocators(*pack.pool);
|
|
pack.nameHash = hash.get_hash(req.pack_name);
|
|
pack.name = sprint("%", req.pack_name);
|
|
pack.data_buffer = data;
|
|
success := init_from_memory(*pack.content, pack.data_buffer, sprint("%", req.pack_name));
|
|
if !success {
|
|
log_error("Failed to load pack!!");
|
|
free(pack.data_buffer.data);
|
|
pack.data_buffer = .{};
|
|
return;
|
|
}
|
|
add_resources_from_pack(*pack);
|
|
array_add(*g_asset_manager.loadedPacks, pack);
|
|
|
|
case .WORLD;
|
|
is_json := data.count > 0 && data[0] == #char "{";
|
|
if is_json {
|
|
json_copy := NewArray(data.count, u8, false);
|
|
memcpy(json_copy.data, data.data, data.count);
|
|
chunks_req: Fetch_Request;
|
|
chunks_req.type = .WORLD_CHUNKS;
|
|
chunks_req.priority = req.priority;
|
|
chunks_req.world_name = req.world_name;
|
|
chunks_req.path = sprint("%/worlds/%/chunks.bin", GAME_RESOURCES_DIR, req.world_name);
|
|
chunks_req.world_json_data = json_copy;
|
|
array_add(*g_asset_manager.queue, chunks_req);
|
|
} else {
|
|
world, ok := load_world_from_data(data);
|
|
if ok {
|
|
set_loaded_world(world);
|
|
rdm_loader_enqueue_world(*get_current_world().world);
|
|
shgrid_loader_enqueue_world(*get_current_world().world);
|
|
log_info("Loaded world (legacy): %", req.world_name);
|
|
} else {
|
|
log_error("Failed to parse world '%'", req.world_name);
|
|
}
|
|
}
|
|
|
|
case .WORLD_CHUNKS;
|
|
json_data := req.world_json_data;
|
|
defer free(json_data.data);
|
|
|
|
json_str: string;
|
|
json_str.data = json_data.data;
|
|
json_str.count = json_data.count;
|
|
|
|
world, ok := load_world_from_json(json_str, data);
|
|
if ok {
|
|
set_loaded_world(world);
|
|
rdm_loader_enqueue_world(*get_current_world().world);
|
|
shgrid_loader_enqueue_world(*get_current_world().world);
|
|
log_info("Loaded world: %", req.world_name);
|
|
} else {
|
|
log_error("Failed to parse world '%'", req.world_name);
|
|
}
|
|
|
|
case .RDM_ATLAS;
|
|
curworld := get_current_world();
|
|
if !curworld.valid || curworld.world.name != req.world_name then return;
|
|
|
|
header_size := cast(s64) size_of(RDM_File_Header);
|
|
if data.count < header_size {
|
|
log_error("RDM: global atlas too small (world %)", req.world_name);
|
|
return;
|
|
}
|
|
header := cast(*RDM_File_Header) data.data;
|
|
if header.magic != RDM_FILE_MAGIC {
|
|
log_error("RDM: bad atlas magic (world %)", req.world_name);
|
|
return;
|
|
}
|
|
atlas_pixel_bytes := cast(u64) header.width * cast(u64) header.height * 4 * size_of(u16);
|
|
atlas_imgdata: sg_image_data;
|
|
atlas_imgdata.subimage[0][0] = .{ data.data + header_size, atlas_pixel_bytes };
|
|
atlas_desc: sg_image_desc = .{
|
|
render_target = false,
|
|
width = header.width,
|
|
height = header.height,
|
|
pixel_format = sg_pixel_format.RGBA16F,
|
|
sample_count = 1,
|
|
data = atlas_imgdata,
|
|
};
|
|
if g_rdm_atlas.id != 0 then sg_destroy_image(g_rdm_atlas);
|
|
g_rdm_atlas = sg_make_image(*atlas_desc);
|
|
log_debug("RDM: loaded global atlas (%x%)", header.width, header.height);
|
|
|
|
case .RDM_MANIFEST;
|
|
curworld := get_current_world();
|
|
if !curworld.valid || curworld.world.name != req.world_name then return;
|
|
|
|
json_str : string;
|
|
json_str.data = data.data;
|
|
json_str.count = data.count;
|
|
ok, entries := Jaison.json_parse_string(json_str, [..]Rdm_Atlas_Entry,, temp);
|
|
if !ok {
|
|
log_error("RDM: failed to parse manifest for world '%'", req.world_name);
|
|
return;
|
|
}
|
|
array_reset_keeping_memory(*curworld.world.rdm_lookup);
|
|
for e: entries array_add(*curworld.world.rdm_lookup, e);
|
|
log_debug("RDM: loaded manifest (% entries)", entries.count);
|
|
|
|
case .SHGRID;
|
|
sh_loader_handle_completed(req, data);
|
|
|
|
}
|
|
}
|
|
|
|
Loaded_Pack :: struct {
|
|
name : string;
|
|
nameHash : u32 = 0;
|
|
data_buffer : []u8;
|
|
content : Load_Package;
|
|
textures : Table(string, sg_image);
|
|
animations : Table(string, Animation);
|
|
audio : Table(string, Audio_Data);
|
|
pool : Pool.Pool;
|
|
}
|
|
|
|
add_resources_from_pack :: (pack: *Loaded_Pack) {
|
|
push_allocator(.{Pool.pool_allocator_proc, *pack.pool});
|
|
|
|
Queued_Sheet_File :: struct {
|
|
name : string;
|
|
image : sg_image;
|
|
sheet_w : s32;
|
|
sheet_h : s32;
|
|
sheet : Aseprite_Sheet;
|
|
}
|
|
sheets_to_init : Table(string, Queued_Sheet_File);
|
|
sheets_to_init.allocator = temp;
|
|
|
|
for v : pack.content.lookup {
|
|
_, name, extension := split_from_left(v.name, ".");
|
|
if extension == {
|
|
case "png";
|
|
img, w, h := create_texture_from_memory(v.data);
|
|
table_set(*pack.textures, sprint("%", v.name), img);
|
|
case "sheet.png";
|
|
img, w, h := create_texture_from_memory(v.data);
|
|
queuedSheet := table_find_pointer(*sheets_to_init, name);
|
|
if !queuedSheet {
|
|
table_set(*sheets_to_init, name, .{ name = name, image = img, sheet_w = w, sheet_h = h });
|
|
} else {
|
|
queuedSheet.image = img;
|
|
queuedSheet.sheet_w = w;
|
|
queuedSheet.sheet_h = h;
|
|
}
|
|
case "sheet.json";
|
|
s := create_string_from_memory(v.data);
|
|
success, sheet := Jaison.json_parse_string(s, Aseprite_Sheet,, temp);
|
|
if !success {
|
|
log_error("Failed to parse animation sheet JSON for sheet(%)", name);
|
|
continue;
|
|
}
|
|
queuedSheet := table_find_pointer(*sheets_to_init, name);
|
|
if !queuedSheet {
|
|
table_set(*sheets_to_init, name, .{ name = name, sheet = sheet });
|
|
} else {
|
|
queuedSheet.sheet = sheet;
|
|
}
|
|
case "colorgrade.png";
|
|
img, x, y := create_texture_from_memory(v.data);
|
|
add_image_to_lut_list(img, v.name);
|
|
case "wav";
|
|
audio := load_wav_from_memory(v.data);
|
|
table_set(*pack.audio, name, audio);
|
|
case "json";
|
|
if name == "particles" {
|
|
s := create_string_from_memory(v.data);
|
|
success, defs := Jaison.json_parse_string(s, [..]Particle_Emitter_Config,, temp);
|
|
if success {
|
|
array_reset(*g_emitter_defs);
|
|
for defs {
|
|
def := it;
|
|
def.name = sprint("%", it.name);
|
|
def.animation_name = sprint("%", it.animation_name);
|
|
array_add(*g_emitter_defs, def);
|
|
}
|
|
log_info("Loaded % particle definitions from pack", g_emitter_defs.count);
|
|
}
|
|
}
|
|
case "hex";
|
|
pal : Loaded_Palette;
|
|
pal.name = sprint("%", name);
|
|
pal.entries = load_hex_palette_from_memory(v.data);
|
|
array_add(*g_palettes, pal);
|
|
log_info("Loaded % colors from hex palette(%)", pal.entries.count, v.name);
|
|
case "ttf";
|
|
case;
|
|
log_warn("File(%) in pack(%) has unknown format", v.name, pack.name);
|
|
}
|
|
log_debug("% -> %", v.name, extension);
|
|
}
|
|
|
|
for qsheet : sheets_to_init {
|
|
for qsheet.sheet.meta.frameTags {
|
|
anim : Animation;
|
|
anim.name = sprint("%", it.name);
|
|
anim.sheet = qsheet.image;
|
|
anim.sheet_w = qsheet.sheet_w;
|
|
anim.sheet_h = qsheet.sheet_h;
|
|
for idx : it.from..it.to {
|
|
frameData := qsheet.sheet.frames[idx];
|
|
array_add(*anim.frames, Frame.{
|
|
frameData.frame.x,
|
|
frameData.frame.y,
|
|
frameData.frame.w,
|
|
frameData.frame.h,
|
|
frameData.duration
|
|
});
|
|
}
|
|
table_add(*pack.animations, anim.name, anim);
|
|
log_debug("Added anim(%)", anim.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
find_pack_by_name :: (name: string) -> (bool, Loaded_Pack) {
|
|
nameHash := get_hash(name);
|
|
for g_asset_manager.loadedPacks {
|
|
if it.nameHash == nameHash {
|
|
return true, it;
|
|
}
|
|
}
|
|
log_warn("Unable to find pack: %", name);
|
|
return false, .{};
|
|
}
|
|
|
|
// Removes the highest-priority item from the queue (lowest enum value = most urgent).
|
|
// Within equal priority, the earliest-queued item wins (stable).
|
|
dequeue_highest_priority :: (queue: *[..]Fetch_Request) -> (Fetch_Request, bool) {
|
|
if queue.count == 0 return .{}, false;
|
|
best := 0;
|
|
for i: 1..queue.count - 1 {
|
|
if queue.*[i].priority < queue.*[best].priority then best = i;
|
|
}
|
|
req := queue.*[best];
|
|
array_ordered_remove_by_index(queue, best);
|
|
return req, true;
|
|
}
|
|
|
|
dispatch_slots :: () {
|
|
for i: 0..MAX_FETCH_SLOTS - 1 {
|
|
slot := *g_asset_manager.slots[i];
|
|
if slot.occupied continue;
|
|
if g_asset_manager.queue.count == 0 break;
|
|
|
|
req, found := dequeue_highest_priority(*g_asset_manager.queue);
|
|
if !found break;
|
|
|
|
slot.occupied = true;
|
|
slot.req = req;
|
|
slot.buf = NewArray(fetch_type_buffer_size(req.type), u8, false);
|
|
|
|
// sokol copies user_data bytes immediately on sfetch_send, so a
|
|
// stack-local index is safe here.
|
|
slot_idx : u32 = cast(u32) i;
|
|
|
|
// Route BACKGROUND requests to channel 1 so they don't starve
|
|
// LOADING_SCREEN / BLOCK_ENGINE work on native (two IO threads).
|
|
channel : u32 = ifx req.priority == .BACKGROUND then cast(u32)1 else cast(u32)0;
|
|
|
|
path_c := to_c_string(req.path);
|
|
defer free(path_c);
|
|
|
|
sfetch_send(*(sfetch_request_t.{
|
|
channel = channel,
|
|
path = path_c,
|
|
callback = fetch_callback,
|
|
buffer = .{ ptr = slot.buf.data, size = cast(u64) slot.buf.count },
|
|
user_data = .{ ptr = *slot_idx, size = cast(u64) size_of(u32) },
|
|
}));
|
|
}
|
|
}
|
|
|
|
#scope_export
|
|
|
|
free_resources_from_pack :: (pack: *Loaded_Pack) {
|
|
for pack.textures sg_destroy_image(it);
|
|
table_reset(*pack.textures);
|
|
table_reset(*pack.audio);
|
|
table_reset(*pack.animations);
|
|
Pool.reset(*pack.pool);
|
|
if pack.data_buffer.data {
|
|
free(pack.data_buffer.data);
|
|
pack.data_buffer = .{};
|
|
}
|
|
}
|
|
|
|
asset_manager_init :: () {
|
|
// No upfront allocations; buffers are allocated per-request in dispatch_slots.
|
|
}
|
|
|
|
mandatory_loads_done :: () -> bool {
|
|
for g_asset_manager.slots {
|
|
if it.occupied && it.req.priority == .BLOCK_ENGINE then return false;
|
|
}
|
|
for g_asset_manager.queue {
|
|
if it.priority == .BLOCK_ENGINE then return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
show_loading_screen :: () -> bool {
|
|
for g_asset_manager.slots {
|
|
if it.occupied && it.req.priority != .BACKGROUND then return true;
|
|
}
|
|
for g_asset_manager.queue {
|
|
if it.priority != .BACKGROUND then return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
asset_manager_tick :: () {
|
|
#if !FLAG_RELEASE_BUILD && OS != .WASM {
|
|
all_idle := true;
|
|
for g_asset_manager.slots if it.occupied { all_idle = false; break; }
|
|
|
|
if g_asset_manager.pending_hot_reload && all_idle && g_asset_manager.queue.count == 0 {
|
|
g_asset_manager.pending_hot_reload = false;
|
|
hot_reload_all_packs();
|
|
}
|
|
if g_asset_manager.hot_reload_unpause_pending && !show_loading_screen() {
|
|
g_mixer.paused = false;
|
|
g_asset_manager.hot_reload_unpause_pending = false;
|
|
}
|
|
}
|
|
|
|
sfetch_dowork();
|
|
dispatch_slots();
|
|
}
|
|
|
|
load_world :: (name: string) {
|
|
unload_current_world();
|
|
req: Fetch_Request;
|
|
req.type = .WORLD;
|
|
req.priority = .LOADING_SCREEN;
|
|
req.world_name = sprint("%", name);
|
|
req.path = sprint("%/worlds/%/world.json", GAME_RESOURCES_DIR, name);
|
|
array_add(*g_asset_manager.queue, req);
|
|
}
|
|
|
|
load_pack :: (name: string, priority: Load_Priority = .LOADING_SCREEN) {
|
|
req: Fetch_Request;
|
|
req.type = .PACK;
|
|
req.priority = priority;
|
|
req.pack_name = sprint("%", name);
|
|
req.path = sprint("%/%.pack", PACK_DIR, name);
|
|
array_add(*g_asset_manager.queue, req);
|
|
}
|
|
|
|
// Asset accessors:
|
|
|
|
#scope_file
|
|
#scope_export
|
|
|
|
get_texture_from_pack :: (pack: string, path: string) -> (sg_image) {
|
|
found, pack := find_pack_by_name(pack);
|
|
invalid_img : sg_image;
|
|
|
|
if !found {
|
|
return invalid_img;
|
|
}
|
|
|
|
ok, img := table_find(*pack.textures, path);
|
|
|
|
if !ok {
|
|
log_warn("Failed to find texture(%) from pack(%)", path, pack.name);
|
|
return invalid_img;
|
|
}
|
|
|
|
return img;
|
|
}
|
|
|
|
get_animation_from_pack :: (pack: string, path: string) -> *Animation {
|
|
found, pack := find_pack_by_name(pack);
|
|
if !found then return null;
|
|
return table_find_pointer(*pack.animations, path);
|
|
}
|
|
|
|
get_audio_from_pack :: (pack: string, path: string) -> *Audio_Data {
|
|
found, pack := find_pack_by_name(pack);
|
|
if !found then return null;
|
|
audio := table_find_pointer(*pack.audio, path);
|
|
if !audio then log_error("Failed to find audio(%)", path);
|
|
return audio;
|
|
}
|
|
|
|
add_font_from_pack :: (pack: string, path: string) {
|
|
pack_ok, pack := find_pack_by_name(pack);
|
|
if !pack_ok then return;
|
|
ok, entry := table_find(*pack.content.lookup, path);
|
|
if !ok {
|
|
log_error("Failed to find font % from pack", path);
|
|
return;
|
|
}
|
|
state.font_default.fons_font = fonsAddFontMem(state.fons, "sans", entry.data.data, xx entry.data.count, 0);
|
|
}
|
|
|
|
load_string_from_pack :: (pack: string, path: string) -> string {
|
|
log_warn("You are circumventing the asset management system");
|
|
pack_ok, pack := find_pack_by_name(pack);
|
|
if !pack_ok return "";
|
|
|
|
ok, entry := table_find(*pack.content.lookup, path);
|
|
if !ok {
|
|
log_error("Failed to load string from pack: %", path);
|
|
return "";
|
|
}
|
|
|
|
s: string;
|
|
s.data = entry.data.data;
|
|
s.count = entry.data.count;
|
|
return s;
|
|
}
|