diff --git a/apps/game/src/app.c b/apps/game/src/app.c index da7e9c694..5574523ec 100644 --- a/apps/game/src/app.c +++ b/apps/game/src/app.c @@ -211,7 +211,7 @@ static void app_action_restart_draw(UiCanvasComp* canvas, const AppActionContext app_action_notify(ctx, string_lit("Restart")); log_i("Restart"); - scene_level_load(ctx->world, g_appLevel); + scene_level_reload(ctx->world); } } @@ -610,7 +610,7 @@ void app_ecs_init(EcsWorld* world, const CliInvocation* invoc) { input_resource_load_map(inputResource, string_lit("global/game-input.imp")); input_resource_load_map(inputResource, string_lit("global/debug-input.imp")); - scene_level_load(world, g_appLevel); + scene_level_load(world, asset_lookup(world, assets, g_appLevel)); scene_prefab_init(world, string_lit("global/game-prefabs.pfb")); scene_weapon_init(world, string_lit("global/game-weapons.wea")); scene_terrain_init( diff --git a/assets/fonts/ui.ftx b/assets/fonts/ui.ftx index 57e7cf2ef..9cd5d2581 100644 --- a/assets/fonts/ui.ftx +++ b/assets/fonts/ui.ftx @@ -19,7 +19,7 @@ { "id": "fonts/materialicons_regular.ttf", "yOffset": -0.15, - "characters": "\uE9E4\uEF5B\uE3AE\uE1DB\uEA5F\uE069\uE338\uE4FC\uE80E\uE5CA\uE9BA\uE5D0\uE89E\uE312\uE5CD\uF016\uE412\uE25E\uE5CF\uE5CE\uE5D7\uE5D6\uE9FE\uE87B\uE71C\uE425\uF230\uEA4A\uEA3C\uE92B\uE162\uE145\uE518\uE8B8\uE322\uE429\uE8F4\uE80B\uE405\uE574\uE868\uE050\uE04F\uE3F4\uE034\uF053\uE7EF" + "characters": "\uE9E4\uEF5B\uE3AE\uE1DB\uEA5F\uE069\uE338\uE4FC\uE80E\uE5CA\uE9BA\uE5D0\uE89E\uE312\uE5CD\uF016\uE412\uE25E\uE5CF\uE5CE\uE5D7\uE5D6\uE9FE\uE87B\uE71C\uE425\uF230\uEA4A\uEA3C\uE92B\uE162\uE145\uE518\uE8B8\uE322\uE429\uE8F4\uE80B\uE405\uE574\uE868\uE050\uE04F\uE3F4\uE034\uF053\uE7EF\uE5D5\uE161" }, { "id": "fonts/shapes.ttf", diff --git a/assets/global/debug-input.imp b/assets/global/debug-input.imp index 1d9839319..563c88558 100644 --- a/assets/global/debug-input.imp +++ b/assets/global/debug-input.imp @@ -13,7 +13,7 @@ "blockers": [ "TextInput" ], "bindings": [ { "type": "Down", "key": "ArrowUp" }, - { "type": "Down", "key": "W" } + { "type": "Down", "key": "W", "illegalModifiers": [ "Control" ] } ] }, { @@ -21,7 +21,7 @@ "blockers": [ "TextInput" ], "bindings": [ { "type": "Down", "key": "ArrowDown" }, - { "type": "Down", "key": "S" } + { "type": "Down", "key": "S", "illegalModifiers": [ "Control" ] } ] }, { @@ -29,7 +29,7 @@ "blockers": [ "TextInput" ], "bindings": [ { "type": "Down", "key": "ArrowRight" }, - { "type": "Down", "key": "D" } + { "type": "Down", "key": "D", "illegalModifiers": [ "Control" ] } ] }, { @@ -37,7 +37,7 @@ "blockers": [ "TextInput" ], "bindings": [ { "type": "Down", "key": "ArrowLeft" }, - { "type": "Down", "key": "A" } + { "type": "Down", "key": "A", "illegalModifiers": [ "Control" ] } ] }, { @@ -48,6 +48,13 @@ { "type": "Down", "key": "MouseExtra1" } ] }, + { + "name": "SaveLevel", + "blockers": [ "TextInput" ], + "bindings": [ + { "type": "Down", "key": "S", "requiredModifiers": [ "Control" ] } + ] + }, { "name": "DebugTimePauseToggle", "blockers": [ "TextInput" ], diff --git a/libs/asset/include/asset_manager.h b/libs/asset/include/asset_manager.h index 7c93389ee..30e40a7c2 100644 --- a/libs/asset/include/asset_manager.h +++ b/libs/asset/include/asset_manager.h @@ -3,6 +3,8 @@ #include "ecs_entity.h" #include "ecs_module.h" +#define asset_query_max_results 512 + typedef struct { String id, data; } AssetMemRecord; @@ -83,6 +85,21 @@ void asset_reload_request(EcsWorld*, EcsEntityId assetEntity); */ bool asset_save(AssetManagerComp*, String id, String data); +/** + * Query for assets that match the given id pattern. + * + * Supported pattern syntax: + * '?' matches any single character. + * '*' matches any number of any characters including none. + * + * NOTE: Returns the number of found assets. + */ +u32 asset_query( + EcsWorld*, + AssetManagerComp*, + String pattern, + EcsEntityId out[PARAM_ARRAY_SIZE(asset_query_max_results)]); + /** * Debug apis. */ diff --git a/libs/asset/src/manager.c b/libs/asset/src/manager.c index a1ba00ddd..24ff2c0fd 100644 --- a/libs/asset/src/manager.c +++ b/libs/asset/src/manager.c @@ -454,6 +454,30 @@ bool asset_save(AssetManagerComp* manager, const String id, const String data) { return asset_repo_save(manager->repo, id, data); } +typedef struct { + EcsWorld* world; + AssetManagerComp* manager; + u32 count; + EcsEntityId* out; +} AssetQueryContext; + +static void asset_query_output(void* ctxRaw, const String id) { + AssetQueryContext* ctx = ctxRaw; + if (LIKELY(ctx->count != asset_query_max_results)) { + ctx->out[ctx->count++] = asset_lookup(ctx->world, ctx->manager, id); + } +} + +u32 asset_query( + EcsWorld* world, + AssetManagerComp* manager, + const String pattern, + EcsEntityId out[PARAM_ARRAY_SIZE(asset_query_max_results)]) { + AssetQueryContext ctx = {.world = world, .manager = manager, .out = out}; + asset_repo_query(manager->repo, pattern, &ctx, asset_query_output); + return ctx.count; +} + void asset_register_dep(EcsWorld* world, EcsEntityId asset, const EcsEntityId dependency) { diag_assert(asset); diag_assert(dependency); diff --git a/libs/asset/src/repo.c b/libs/asset/src/repo.c index 9f6e8fc40..17d72b8c4 100644 --- a/libs/asset/src/repo.c +++ b/libs/asset/src/repo.c @@ -1,7 +1,25 @@ #include "core_alloc.h" +#include "core_array.h" +#include "core_diag.h" #include "repo_internal.h" +static const String g_assetRepoQueryResultStrs[] = { + string_static("RepoQuerySuccess"), + string_static("RepoQueryErrorNotSupported"), + string_static("RepoQueryErrorPatternNotSupported"), + string_static("RepoQueryErrorWhileQuerying"), +}; + +ASSERT( + array_elems(g_assetRepoQueryResultStrs) == AssetRepoQueryResult_Count, + "Incorrect number of AssetRepoQueryResult strings"); + +String asset_repo_query_result_str(const AssetRepoQueryResult result) { + diag_assert(result < AssetRepoQueryResult_Count); + return g_assetRepoQueryResultStrs[result]; +} + void asset_repo_destroy(AssetRepo* repo) { repo->destroy(repo); } AssetSource* asset_repo_source_open(AssetRepo* repo, String id) { return repo->open(repo, id); } @@ -30,3 +48,14 @@ bool asset_repo_changes_poll(AssetRepo* repo, u64* outUserData) { } return false; } + +AssetRepoQueryResult asset_repo_query( + AssetRepo* repo, + const String filterPattern, + void* context, + const AssetRepoQueryHandler handler) { + if (repo->query) { + return repo->query(repo, filterPattern, context, handler); + } + return AssetRepoQueryResult_ErrorNotSupported; +} diff --git a/libs/asset/src/repo_fs.c b/libs/asset/src/repo_fs.c index a987e570b..160125d41 100644 --- a/libs/asset/src/repo_fs.c +++ b/libs/asset/src/repo_fs.c @@ -1,5 +1,6 @@ #include "core_alloc.h" #include "core_file.h" +#include "core_file_iterator.h" #include "core_file_monitor.h" #include "core_path.h" #include "log_logger.h" @@ -94,6 +95,104 @@ static bool asset_repo_fs_changes_poll(AssetRepo* repo, u64* outUserData) { return false; } +typedef enum { + AssetRepoFsQuery_Recursive = 1 << 0, +} AssetRepoFsQueryFlags; + +static AssetRepoQueryResult asset_repo_fs_query_iteration( + AssetRepoFs* repoFs, + const String directory, + const String pattern, + const AssetRepoFsQueryFlags flags, + void* context, + const AssetRepoQueryHandler handler) { + + if (UNLIKELY(directory.size) > 256) { + // Sanity check the maximum directory length (relative to the repo root-path). + log_w("AssetRepository: Directory path length exceeds maximum"); + return AssetRepoQueryResult_ErrorWhileQuerying; + } + + Allocator* alloc = alloc_bump_create_stack(768); + DynString dirBuffer = dynstring_create(alloc, 512); + + // Open a file iterator for the absolute path starting from the repo root-path. + path_append(&dirBuffer, repoFs->rootPath); + path_append(&dirBuffer, directory); + FileIterator* itr = file_iterator_create(alloc, dynstring_view(&dirBuffer)); + + FileIteratorEntry entry; + FileIteratorResult itrResult; + while ((itrResult = file_iterator_next(itr, &entry)) == FileIteratorResult_Found) { + // Construct a file path relative to the repo root-path. + dynstring_clear(&dirBuffer); + path_append(&dirBuffer, directory); + path_append(&dirBuffer, entry.name); + const String path = dynstring_view(&dirBuffer); + + switch (entry.type) { + case FileType_Regular: + if (string_match_glob(path, pattern, StringMatchFlags_None)) { + handler(context, path); + } + break; + case FileType_Directory: + if (flags & AssetRepoFsQuery_Recursive) { + // TODO: Handle errors for sub-directory iteration failure. + asset_repo_fs_query_iteration(repoFs, path, pattern, flags, context, handler); + } + break; + case FileType_None: + case FileType_Unknown: + break; + } + } + file_iterator_destroy(itr); + + if (UNLIKELY(itrResult != FileIteratorResult_End)) { + log_w( + "AssetRepository: Error while performing file query", + log_param("result", fmt_text(file_iterator_result_str(itrResult)))); + return AssetRepoQueryResult_ErrorWhileQuerying; + } + return AssetRepoQueryResult_Success; +} + +static AssetRepoQueryResult asset_repo_fs_query( + AssetRepo* repo, const String pattern, void* context, const AssetRepoQueryHandler handler) { + AssetRepoFs* repoFs = (AssetRepoFs*)repo; + + // Find a root directory for the query. + const String directory = path_parent(pattern); + + static const String g_globChars = string_static("*?"); + if (UNLIKELY(!sentinel_check(string_find_first_any(directory, g_globChars)))) { + /** + * Filtering in the directory part part is not supported at the moment. + * Supporting this would require recursing from the first non-filtered directory. + */ + log_w("AssetRepository: Unsupported file query pattern"); + return AssetRepoQueryResult_ErrorPatternNotSupported; + } + + AssetRepoFsQueryFlags flags = 0; + + /** + * Recursive queries are defined by a file-name starting with a wildcard. + * + * For example a query of `dir/ *.txt` will match both 'dir/hello.txt' and 'dir/sub/hello.txt', + * '*.txt' will match any 'txt' files regardless how deeply its nested. This means there's no + * way to search for direct children starting with a wildcard at the moment, in the future we + * can consider supporting more exotic syntax like 'dir/ ** / *.txt' for recursive queries. + */ + const String fileFilter = path_filename(pattern); + if (string_starts_with(fileFilter, string_lit("*"))) { + flags |= AssetRepoFsQuery_Recursive; + } + + return asset_repo_fs_query_iteration(repoFs, directory, pattern, flags, context, handler); +} + static void asset_repo_fs_destroy(AssetRepo* repo) { AssetRepoFs* repoFs = (AssetRepoFs*)repo; @@ -113,6 +212,7 @@ AssetRepo* asset_repo_create_fs(String rootPath) { .destroy = asset_repo_fs_destroy, .changesWatch = asset_repo_fs_changes_watch, .changesPoll = asset_repo_fs_changes_poll, + .query = asset_repo_fs_query, }, .rootPath = string_dup(g_alloc_heap, rootPath), .monitor = file_monitor_create(g_alloc_heap, rootPath), diff --git a/libs/asset/src/repo_internal.h b/libs/asset/src/repo_internal.h index 061dc9304..81fd11c86 100644 --- a/libs/asset/src/repo_internal.h +++ b/libs/asset/src/repo_internal.h @@ -6,6 +6,17 @@ typedef struct sAssetRepo AssetRepo; typedef struct sAssetSource AssetSource; +typedef void (*AssetRepoQueryHandler)(void* ctx, String assetId); + +typedef enum { + AssetRepoQueryResult_Success, + AssetRepoQueryResult_ErrorNotSupported, + AssetRepoQueryResult_ErrorPatternNotSupported, + AssetRepoQueryResult_ErrorWhileQuerying, + + AssetRepoQueryResult_Count, +} AssetRepoQueryResult; + /** * Asset repository. * NOTE: Api is thread-safe. @@ -16,6 +27,7 @@ struct sAssetRepo { void (*destroy)(AssetRepo*); void (*changesWatch)(AssetRepo*, String id, u64 userData); bool (*changesPoll)(AssetRepo*, u64* outUserData); + AssetRepoQueryResult (*query)(AssetRepo*, String pattern, void* ctx, AssetRepoQueryHandler); }; struct sAssetSource { @@ -24,6 +36,8 @@ struct sAssetSource { void (*close)(AssetSource*); }; +String asset_repo_query_result_str(AssetRepoQueryResult); + AssetRepo* asset_repo_create_fs(String rootPath); AssetRepo* asset_repo_create_mem(const AssetMemRecord* records, usize recordCount); void asset_repo_destroy(AssetRepo*); @@ -32,5 +46,6 @@ AssetSource* asset_repo_source_open(AssetRepo*, String id); bool asset_repo_save(AssetRepo*, String id, String data); void asset_repo_source_close(AssetSource*); -void asset_repo_changes_watch(AssetRepo*, String id, u64 userData); -bool asset_repo_changes_poll(AssetRepo*, u64* outUserData); +void asset_repo_changes_watch(AssetRepo*, String id, u64 userData); +bool asset_repo_changes_poll(AssetRepo*, u64* outUserData); +AssetRepoQueryResult asset_repo_query(AssetRepo*, String pattern, void* ctx, AssetRepoQueryHandler); diff --git a/libs/asset/src/repo_mem.c b/libs/asset/src/repo_mem.c index fed4c0535..3969a607d 100644 --- a/libs/asset/src/repo_mem.c +++ b/libs/asset/src/repo_mem.c @@ -7,6 +7,7 @@ typedef struct { StringHash idHash; + String id; String data; } RepoEntry; @@ -37,10 +38,26 @@ static AssetSource* asset_source_mem_open(AssetRepo* repo, const String id) { return src; } +static AssetRepoQueryResult asset_repo_mem_query( + AssetRepo* repo, const String pattern, void* ctx, const AssetRepoQueryHandler handler) { + AssetRepoMem* repoMem = (AssetRepoMem*)repo; + + dynarray_for_t(&repoMem->entries, RepoEntry, entry) { + if (string_match_glob(entry->id, pattern, StringMatchFlags_None)) { + handler(ctx, entry->id); + } + } + + return AssetRepoQueryResult_Success; +} + static void asset_repo_mem_destroy(AssetRepo* repo) { AssetRepoMem* repoMem = (AssetRepoMem*)repo; - dynarray_for_t(&repoMem->entries, RepoEntry, entry) { string_free(g_alloc_heap, entry->data); }; + dynarray_for_t(&repoMem->entries, RepoEntry, entry) { + string_free(g_alloc_heap, entry->id); + string_free(g_alloc_heap, entry->data); + }; dynarray_destroy(&repoMem->entries); alloc_free_t(g_alloc_heap, repoMem); @@ -56,6 +73,7 @@ AssetRepo* asset_repo_create_mem(const AssetMemRecord* records, const usize reco .destroy = asset_repo_mem_destroy, .changesWatch = null, .changesPoll = null, + .query = asset_repo_mem_query, }, .entries = dynarray_create_t(g_alloc_heap, RepoEntry, recordCount), }; @@ -63,6 +81,7 @@ AssetRepo* asset_repo_create_mem(const AssetMemRecord* records, const usize reco for (usize i = 0; i != recordCount; ++i) { RepoEntry entry = { .idHash = string_hash(records[i].id), + .id = string_dup(g_alloc_heap, records[i].id), .data = string_dup(g_alloc_heap, records[i].data), }; *dynarray_insert_sorted_t(&repo->entries, RepoEntry, asset_compare_entry, &entry) = entry; diff --git a/libs/asset/test/test_manager.c b/libs/asset/test/test_manager.c index d5cc77881..fd8ec6da0 100644 --- a/libs/asset/test/test_manager.c +++ b/libs/asset/test/test_manager.c @@ -218,6 +218,41 @@ spec(manager) { check(!ecs_world_has_t(world, entity, AssetDirtyComp)); } + it("supports querying all assets with a wildcard") { + AssetManagerComp* manager = ecs_utils_write_first_t(world, ManagerView, AssetManagerComp); + + EcsEntityId results[asset_query_max_results]; + const u32 resultCount = asset_query(world, manager, string_lit("*"), results); + + check_eq_int(resultCount, 2); + + const EcsEntityId entityA = asset_lookup(world, manager, string_lit("a.raw")); + const EcsEntityId entityB = asset_lookup(world, manager, string_lit("b.raw")); + + check(results[0] != results[1]); + check(results[0] == entityA || results[0] == entityB); + check(results[1] == entityA || results[0] == entityB); + } + + it("fails to find any assets with an empty pattern") { + AssetManagerComp* manager = ecs_utils_write_first_t(world, ManagerView, AssetManagerComp); + + EcsEntityId results[asset_query_max_results]; + const u32 resultCount = asset_query(world, manager, string_lit(""), results); + + check_eq_int(resultCount, 0); + } + + it("finds one assets when searching for a specific asset") { + AssetManagerComp* manager = ecs_utils_write_first_t(world, ManagerView, AssetManagerComp); + + EcsEntityId results[asset_query_max_results]; + const u32 resultCount = asset_query(world, manager, string_lit("a.raw"), results); + + check_eq_int(resultCount, 1); + check_eq_int(results[0], asset_lookup(world, manager, string_lit("a.raw"))); + } + teardown() { ecs_runner_destroy(runner); ecs_world_destroy(world); diff --git a/libs/core/include/core_file_iterator.h b/libs/core/include/core_file_iterator.h index 9c687c036..b137930c4 100644 --- a/libs/core/include/core_file_iterator.h +++ b/libs/core/include/core_file_iterator.h @@ -14,7 +14,7 @@ typedef struct sFileIterator FileIterator; */ typedef struct { FileType type; - String name; // Allocation remains valid until the iterator is advanced. + String name; // NOTE: String is allocated in scratch memory, should NOT be stored. } FileIteratorEntry; /** diff --git a/libs/core/include/core_path.h b/libs/core/include/core_path.h index 599347306..2c26d7022 100644 --- a/libs/core/include/core_path.h +++ b/libs/core/include/core_path.h @@ -83,7 +83,7 @@ String path_parent(String); bool path_canonize(DynString*, String path); /** - * Append a new segment to a path. Will insert a '/' seperator if required. + * Append a new segment to a path. Will insert a '/' separator if required. */ void path_append(DynString*, String path); diff --git a/libs/core/src/path.c b/libs/core/src/path.c index 9108387cb..36e803b9e 100644 --- a/libs/core/src/path.c +++ b/libs/core/src/path.c @@ -14,22 +14,22 @@ static String g_path_seperators = string_static("/\\"); -static bool path_ends_with_seperator(String str) { +static bool path_ends_with_seperator(const String str) { return mem_contains(g_path_seperators, *string_last(str)); } -static bool path_starts_with_posix_root(String path) { +static bool path_starts_with_posix_root(const String path) { return !string_is_empty(path) && *string_begin(path) == '/'; } -static bool path_starts_with_win32_root(String path) { +static bool path_starts_with_win32_root(const String path) { if (path.size < 3) { return false; } if (!ascii_is_letter(*string_begin(path))) { return false; } - String postDriveLetter = string_slice(path, 1, 2); + const String postDriveLetter = string_slice(path, 1, 2); return string_eq(postDriveLetter, string_lit(":/")) || string_eq(postDriveLetter, string_lit(":\\")); } @@ -49,44 +49,44 @@ void path_init() { g_path_tempdir = path_pal_tempdir(array_mem(g_path_tempdir_buffer)); } -bool path_is_absolute(String path) { +bool path_is_absolute(const String path) { return path_starts_with_posix_root(path) || path_starts_with_win32_root(path); } -bool path_is_root(String path) { +bool path_is_root(const String path) { return (path.size == 1 && path_starts_with_posix_root(path)) || (path.size == 3 && path_starts_with_win32_root(path)); } -String path_filename(String path) { +String path_filename(const String path) { const usize lastSegStart = string_find_last_any(path, g_path_seperators); return sentinel_check(lastSegStart) ? path : string_slice(path, lastSegStart + 1, path.size - lastSegStart - 1); } -String path_extension(String path) { - String fileName = path_filename(path); - const usize extensionStart = string_find_last_any(fileName, string_lit(".")); +String path_extension(const String path) { + const String fileName = path_filename(path); + const usize extensionStart = string_find_last_any(fileName, string_lit(".")); return sentinel_check(extensionStart) ? string_empty : string_slice(fileName, extensionStart + 1, fileName.size - extensionStart - 1); } -String path_stem(String path) { - String fileName = path_filename(path); - const usize extensionStart = string_find_first_any(fileName, string_lit(".")); +String path_stem(const String path) { + const String fileName = path_filename(path); + const usize extensionStart = string_find_first_any(fileName, string_lit(".")); return sentinel_check(extensionStart) ? fileName : string_slice(fileName, 0, extensionStart); } -String path_parent(String path) { +String path_parent(const String path) { const usize lastSegStart = string_find_last_any(path, g_path_seperators); if (sentinel_check(lastSegStart)) { return string_empty; } - // For the root directory we preserve the seperator, for any other directory we do not. - String parentWithSep = string_slice(path, 0, lastSegStart + 1); + // For the root directory we preserve the separator, for any other directory we do not. + const String parentWithSep = string_slice(path, 0, lastSegStart + 1); return path_is_root(parentWithSep) ? parentWithSep : string_slice(path, 0, lastSegStart); } @@ -153,7 +153,7 @@ bool path_canonize(DynString* str, String path) { return success; } -void path_append(DynString* str, String path) { +void path_append(DynString* str, const String path) { if (str->size && !path_ends_with_seperator(dynstring_view(str))) { dynstring_append_char(str, '/'); } @@ -186,7 +186,7 @@ String path_build_scratch_raw(const String* segments) { return res; } -void path_name_random(DynString* str, Rng* rng, String prefix, String extension) { +void path_name_random(DynString* str, Rng* rng, const String prefix, const String extension) { static const u8 g_chars[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', @@ -213,7 +213,7 @@ void path_name_random(DynString* str, Rng* rng, String prefix, String extension) } } -String path_name_random_scratch(Rng* rng, String prefix, String extension) { +String path_name_random_scratch(Rng* rng, const String prefix, const String extension) { Mem scratchMem = alloc_alloc(g_alloc_scratch, prefix.size + 32 + extension.size, 1); DynString str = dynstring_create_over(scratchMem); @@ -224,7 +224,7 @@ String path_name_random_scratch(Rng* rng, String prefix, String extension) { return res; } -void path_name_timestamp(DynString* str, String prefix, String extension) { +void path_name_timestamp(DynString* str, const String prefix, const String extension) { if (!string_is_empty(prefix)) { dynstring_append(str, prefix); dynstring_append_char(str, '_'); @@ -242,7 +242,7 @@ void path_name_timestamp(DynString* str, String prefix, String extension) { } } -String path_name_timestamp_scratch(String prefix, String extension) { +String path_name_timestamp_scratch(const String prefix, const String extension) { Mem scratchMem = alloc_alloc(g_alloc_scratch, prefix.size + 32 + extension.size, 1); DynString str = dynstring_create_over(scratchMem); diff --git a/libs/debug/src/level.c b/libs/debug/src/level.c index a33d64f26..5ca67362b 100644 --- a/libs/debug/src/level.c +++ b/libs/debug/src/level.c @@ -1,78 +1,179 @@ +#include "asset_manager.h" #include "core_alloc.h" #include "core_diag.h" #include "ecs_world.h" +#include "input_manager.h" #include "scene_level.h" #include "ui.h" // clang-format off -static const String g_tooltipLoad = string_static("Load a level asset into the scene."); -static const String g_tooltipSave = string_static("Save the current scene as a level asset."); -static const String g_defaultLevel = string_static("levels/default.lvl"); +static const String g_tooltipFilter = string_static("Filter levels by identifier.\nSupports glob characters \a.b*\ar and \a.b?\ar."); +static const String g_levelQueryPattern = string_static("levels/*.lvl"); // clang-format on +typedef enum { + DebugLevelFlags_RefreshAssets = 1 << 0, + DebugLevelFlags_Reload = 1 << 1, + DebugLevelFlags_Unload = 1 << 2, + DebugLevelFlags_SaveCurrent = 1 << 3, + + DebugLevelFlags_None = 0, + DebugLevelFlags_Default = DebugLevelFlags_RefreshAssets, +} DebugLevelFlags; + ecs_comp_define(DebugLevelPanelComp) { - DynString levelIdInput; - UiPanel panel; + DebugLevelFlags flags; + DynString idFilter; + DynArray levelAssets; // EcsEntityId[] + UiPanel panel; + UiScrollview scrollview; + u32 totalRows; }; static void ecs_destruct_level_panel(void* data) { DebugLevelPanelComp* comp = data; - dynstring_destroy(&comp->levelIdInput); + dynstring_destroy(&comp->idFilter); + dynarray_destroy(&comp->levelAssets); } -static void level_panel_draw( - EcsWorld* world, - UiCanvasComp* canvas, - const SceneLevelManagerComp* levelManager, - DebugLevelPanelComp* panelComp) { - const String title = fmt_write_scratch("{} Level Panel", fmt_ui_shape(Globe)); - ui_panel_begin( - canvas, &panelComp->panel, .title = title, .topBarColor = ui_color(100, 0, 0, 192)); +ecs_view_define(AssetView) { ecs_access_read(AssetComp); } - const bool isLoading = scene_level_is_loading(levelManager); - const String levelCurrent = scene_level_current_id(levelManager); - const String levelInput = dynstring_view(&panelComp->levelIdInput); +typedef struct { + EcsWorld* world; + DebugLevelPanelComp* panelComp; + const SceneLevelManagerComp* levelManager; + AssetManagerComp* assets; +} DebugLevelContext; + +static void level_assets_refresh(DebugLevelContext* ctx) { + EcsEntityId assetEntities[asset_query_max_results]; + const u32 assetCount = asset_query(ctx->world, ctx->assets, g_levelQueryPattern, assetEntities); + + dynarray_clear(&ctx->panelComp->levelAssets); + for (u32 i = 0; i != assetCount; ++i) { + *dynarray_push_t(&ctx->panelComp->levelAssets, EcsEntityId) = assetEntities[i]; + } +} - UiTable table = ui_table(); - ui_table_add_column(&table, UiTableColumn_Fixed, 125); +static bool level_id_filter(DebugLevelContext* ctx, const String levelId) { + if (!ctx->panelComp->idFilter.size) { + return true; + } + const String rawFilter = dynstring_view(&ctx->panelComp->idFilter); + const String filter = fmt_write_scratch("*{}*", fmt_text(rawFilter)); + return string_match_glob(levelId, filter, StringMatchFlags_IgnoreCase); +} + +static void level_panel_options_draw(UiCanvasComp* canvas, DebugLevelContext* ctx) { + ui_layout_push(canvas); + + UiTable table = ui_table(.spacing = ui_vector(5, 5), .rowHeight = 20); + ui_table_add_column(&table, UiTableColumn_Fixed, 30); + ui_table_add_column(&table, UiTableColumn_Fixed, 30); + ui_table_add_column(&table, UiTableColumn_Fixed, 30); + ui_table_add_column(&table, UiTableColumn_Fixed, 60); ui_table_add_column(&table, UiTableColumn_Flexible, 0); ui_table_next_row(canvas, &table); - ui_label(canvas, string_lit("State")); - ui_table_next_column(canvas, &table); - ui_label(canvas, isLoading ? string_lit("Loading") : string_lit("Idle")); - ui_table_next_row(canvas, &table); - ui_label(canvas, string_lit("Current")); - ui_table_next_column(canvas, &table); - ui_label(canvas, levelCurrent.size ? levelCurrent : string_lit(""), .selectable = true); + const bool levelIsLoaded = scene_level_current(ctx->levelManager); + const UiWidgetFlags levelButtonFlags = levelIsLoaded ? 0 : UiWidget_Disabled; - ui_table_next_row(canvas, &table); - ui_label(canvas, string_lit("Level")); + if (ui_button(canvas, .flags = levelButtonFlags, .label = string_lit("\uE5D5"))) { + ctx->panelComp->flags |= DebugLevelFlags_Reload; + } ui_table_next_column(canvas, &table); - ui_textbox(canvas, &panelComp->levelIdInput, .placeholder = g_defaultLevel); - - ui_table_next_row(canvas, &table); - ui_label(canvas, string_lit("Actions")); + if (ui_button(canvas, .flags = levelButtonFlags, .label = string_lit("\uE161"))) { + ctx->panelComp->flags |= DebugLevelFlags_SaveCurrent; + } ui_table_next_column(canvas, &table); - ui_layout_push(canvas); - { - ui_layout_resize(canvas, UiAlign_MiddleLeft, ui_vector(75, 0), UiBase_Absolute, Ui_X); - if (ui_button(canvas, .label = string_lit("Load"), .tooltip = g_tooltipLoad)) { - scene_level_load(world, string_is_empty(levelInput) ? g_defaultLevel : levelInput); + if (ui_button(canvas, .flags = levelButtonFlags, .label = string_lit("\uE9BA"))) { + ctx->panelComp->flags |= DebugLevelFlags_Unload; + } + ui_table_next_column(canvas, &table); + ui_label(canvas, string_lit("Filter:")); + ui_table_next_column(canvas, &table); + ui_textbox( + canvas, + &ctx->panelComp->idFilter, + .placeholder = string_lit("*"), + .tooltip = g_tooltipFilter); + + ui_layout_pop(canvas); +} + +static void level_panel_draw(UiCanvasComp* canvas, DebugLevelContext* ctx, EcsView* assetView) { + const String title = fmt_write_scratch("{} Level Panel", fmt_ui_shape(Globe)); + ui_panel_begin( + canvas, &ctx->panelComp->panel, .title = title, .topBarColor = ui_color(100, 0, 0, 192)); + + level_panel_options_draw(canvas, ctx); + ui_layout_grow(canvas, UiAlign_BottomCenter, ui_vector(0, -35), UiBase_Absolute, Ui_Y); + ui_layout_container_push(canvas, UiClip_None); + + const bool isLoading = scene_level_is_loading(ctx->levelManager); + const bool disabled = isLoading; + ui_style_push(canvas); + if (disabled) { + ui_style_color_mult(canvas, 0.5f); + } + + UiTable table = ui_table(.spacing = ui_vector(10, 5)); + ui_table_add_column(&table, UiTableColumn_Fixed, 200); + ui_table_add_column(&table, UiTableColumn_Flexible, 0); + + ui_table_draw_header( + canvas, + &table, + (const UiTableColumnName[]){ + {string_lit("Level"), string_lit("Level identifier.")}, + {string_lit("Actions"), string_empty}, + }); + + const f32 totalHeight = ui_table_height(&table, ctx->panelComp->totalRows); + ui_scrollview_begin(canvas, &ctx->panelComp->scrollview, totalHeight); + ctx->panelComp->totalRows = 0; + + EcsIterator* assetItr = ecs_view_itr(assetView); + dynarray_for_t(&ctx->panelComp->levelAssets, EcsEntityId, levelAsset) { + if (!ecs_view_maybe_jump(assetItr, *levelAsset)) { + continue; } - ui_layout_next(canvas, Ui_Right, 10); - if (ui_button(canvas, .label = string_lit("Save"), .tooltip = g_tooltipSave)) { - scene_level_save(world, string_is_empty(levelInput) ? g_defaultLevel : levelInput); + const String id = asset_id(ecs_view_read_t(assetItr, AssetComp)); + const bool loaded = scene_level_current(ctx->levelManager) == *levelAsset; + + if (!level_id_filter(ctx, id)) { + continue; + } + ++ctx->panelComp->totalRows; + + ui_table_next_row(canvas, &table); + ui_table_draw_row_bg( + canvas, &table, loaded ? ui_color(16, 64, 16, 192) : ui_color(48, 48, 48, 192)); + + ui_label(canvas, id, .selectable = true); + ui_table_next_column(canvas, &table); + + ui_layout_resize(canvas, UiAlign_MiddleLeft, ui_vector(60, 0), UiBase_Absolute, Ui_X); + if (ui_button(canvas, .flags = disabled ? UiWidget_Disabled : 0, .label = string_lit("Load"))) { + scene_level_load(ctx->world, *levelAsset); } } - ui_layout_pop(canvas); - ui_panel_end(canvas, &panelComp->panel); + + ui_scrollview_end(canvas, &ctx->panelComp->scrollview); + + ui_style_pop(canvas); + ui_layout_container_pop(canvas); + ui_panel_end(canvas, &ctx->panelComp->panel); } -ecs_view_define(PanelUpdateGlobalView) { ecs_access_read(SceneLevelManagerComp); } +ecs_view_define(PanelUpdateGlobalView) { + ecs_access_read(InputManagerComp); + ecs_access_read(SceneLevelManagerComp); + ecs_access_write(AssetManagerComp); +} ecs_view_define(PanelUpdateView) { ecs_access_write(DebugLevelPanelComp); @@ -85,15 +186,50 @@ ecs_system_define(DebugLevelUpdatePanelSys) { if (!globalItr) { return; } + AssetManagerComp* assets = ecs_view_write_t(globalItr, AssetManagerComp); + const InputManagerComp* input = ecs_view_read_t(globalItr, InputManagerComp); const SceneLevelManagerComp* levelManager = ecs_view_read_t(globalItr, SceneLevelManagerComp); + EcsView* assetView = ecs_world_view_t(world, AssetView); EcsView* panelView = ecs_world_view_t(world, PanelUpdateView); + + if (input_triggered_lit(input, "SaveLevel")) { + const EcsEntityId currentLevelAsset = scene_level_current(levelManager); + if (currentLevelAsset) { + scene_level_save(world, currentLevelAsset); + } + } + for (EcsIterator* itr = ecs_view_itr(panelView); ecs_view_walk(itr);) { DebugLevelPanelComp* panelComp = ecs_view_write_t(itr, DebugLevelPanelComp); UiCanvasComp* canvas = ecs_view_write_t(itr, UiCanvasComp); + DebugLevelContext ctx = { + .world = world, + .panelComp = panelComp, + .levelManager = levelManager, + .assets = assets, + }; + + if (panelComp->flags & DebugLevelFlags_RefreshAssets) { + level_assets_refresh(&ctx); + panelComp->flags &= ~DebugLevelFlags_RefreshAssets; + } + if (panelComp->flags & DebugLevelFlags_Reload) { + scene_level_reload(world); + panelComp->flags &= ~DebugLevelFlags_Reload; + } + if (panelComp->flags & DebugLevelFlags_Unload) { + scene_level_unload(world); + panelComp->flags &= ~DebugLevelFlags_Unload; + } + if (panelComp->flags & DebugLevelFlags_SaveCurrent) { + scene_level_save(world, scene_level_current(levelManager)); + panelComp->flags &= ~DebugLevelFlags_SaveCurrent; + } + ui_canvas_reset(canvas); - level_panel_draw(world, canvas, levelManager, panelComp); + level_panel_draw(canvas, &ctx, assetView); if (panelComp->panel.flags & UiPanelFlags_Close) { ecs_world_entity_destroy(world, ecs_view_entity(itr)); @@ -107,20 +243,27 @@ ecs_system_define(DebugLevelUpdatePanelSys) { ecs_module_init(debug_level_module) { ecs_register_comp(DebugLevelPanelComp, .destructor = ecs_destruct_level_panel); + ecs_register_view(AssetView); ecs_register_view(PanelUpdateGlobalView); ecs_register_view(PanelUpdateView); ecs_register_system( - DebugLevelUpdatePanelSys, ecs_view_id(PanelUpdateGlobalView), ecs_view_id(PanelUpdateView)); + DebugLevelUpdatePanelSys, + ecs_view_id(AssetView), + ecs_view_id(PanelUpdateGlobalView), + ecs_view_id(PanelUpdateView)); } EcsEntityId debug_level_panel_open(EcsWorld* world, const EcsEntityId window) { const EcsEntityId panelEntity = ui_canvas_create(world, window, UiCanvasCreateFlags_ToFront); + ecs_world_add_t( world, panelEntity, DebugLevelPanelComp, - .levelIdInput = dynstring_create(g_alloc_heap, 32), - .panel = ui_panel(.position = ui_vector(0.75f, 0.5f), .size = ui_vector(375, 250))); + .flags = DebugLevelFlags_Default, + .idFilter = dynstring_create(g_alloc_heap, 32), + .levelAssets = dynarray_create_t(g_alloc_heap, EcsEntityId, 8), + .panel = ui_panel(.position = ui_vector(0.75f, 0.5f), .size = ui_vector(375, 250))); return panelEntity; } diff --git a/libs/scene/include/scene_level.h b/libs/scene/include/scene_level.h index 39b08d449..683ecdff5 100644 --- a/libs/scene/include/scene_level.h +++ b/libs/scene/include/scene_level.h @@ -1,9 +1,12 @@ #pragma once +#include "ecs_entity.h" #include "ecs_module.h" ecs_comp_extern(SceneLevelManagerComp); -bool scene_level_is_loading(const SceneLevelManagerComp*); -String scene_level_current_id(const SceneLevelManagerComp*); -void scene_level_load(EcsWorld*, String levelId); -void scene_level_save(EcsWorld*, String levelId); +bool scene_level_is_loading(const SceneLevelManagerComp*); +EcsEntityId scene_level_current(const SceneLevelManagerComp*); +void scene_level_load(EcsWorld*, EcsEntityId levelAsset); +void scene_level_reload(EcsWorld*); +void scene_level_unload(EcsWorld*); +void scene_level_save(EcsWorld*, EcsEntityId levelAsset); diff --git a/libs/scene/src/level.c b/libs/scene/src/level.c index e0fda9964..20a41f2e2 100644 --- a/libs/scene/src/level.c +++ b/libs/scene/src/level.c @@ -24,30 +24,17 @@ typedef enum { } LevelLoadState; ecs_comp_define(SceneLevelManagerComp) { - bool isLoading; - String loadedLevelId; + bool isLoading; + EcsEntityId loadedLevelAsset; }; + ecs_comp_define(SceneLevelRequestLoadComp) { - String levelId; - EcsEntityId levelAsset; + EcsEntityId levelAsset; // 0 indicates reloading the current level. LevelLoadState state; }; -ecs_comp_define(SceneLevelRequestSaveComp) { String levelId; }; - -static void ecs_destruct_level_manager(void* data) { - SceneLevelManagerComp* comp = data; - string_maybe_free(g_alloc_heap, comp->loadedLevelId); -} -static void ecs_destruct_level_request_load(void* data) { - SceneLevelRequestLoadComp* comp = data; - string_maybe_free(g_alloc_heap, comp->levelId); -} - -static void ecs_destruct_level_request_save(void* data) { - SceneLevelRequestSaveComp* comp = data; - string_maybe_free(g_alloc_heap, comp->levelId); -} +ecs_comp_define(SceneLevelRequestUnloadComp); +ecs_comp_define(SceneLevelRequestSaveComp) { EcsEntityId levelAsset; }; static i8 level_compare_object_id(const void* a, const void* b) { return compare_u32(field_ptr(a, AssetLevelObject, id), field_ptr(b, AssetLevelObject, id)); @@ -87,11 +74,11 @@ static void scene_level_process_load(EcsWorld* world, const AssetLevel* level) { log_i("Level loaded", log_param("objects", fmt_int(level->objects.count))); } -ecs_view_define(LoadGlobalView) { - ecs_access_write(AssetManagerComp); - ecs_access_maybe_write(SceneLevelManagerComp); +ecs_view_define(LoadGlobalView) { ecs_access_maybe_write(SceneLevelManagerComp); } +ecs_view_define(LoadAssetView) { + ecs_access_read(AssetComp); + ecs_access_maybe_read(AssetLevelComp); } -ecs_view_define(LoadAssetView) { ecs_access_read(AssetLevelComp); } ecs_view_define(LoadRequestView) { ecs_access_write(SceneLevelRequestLoadComp); } ecs_system_define(SceneLevelLoadSys) { @@ -101,7 +88,6 @@ ecs_system_define(SceneLevelLoadSys) { return; } - AssetManagerComp* assets = ecs_view_write_t(globalItr, AssetManagerComp); SceneLevelManagerComp* manager = ecs_view_write_t(globalItr, SceneLevelManagerComp); if (!manager) { manager = ecs_world_add_t(world, ecs_world_global(world), SceneLevelManagerComp); @@ -121,6 +107,14 @@ ecs_system_define(SceneLevelLoadSys) { log_w("Level load already in progress"); goto Done; } + if (!req->levelAsset) { + // levelAsset of 0 indicates that the currently loaded level should be reloaded. + if (!manager->loadedLevelAsset) { + log_w("Failed to reload level: No level is currently loaded"); + goto Done; + } + req->levelAsset = manager->loadedLevelAsset; + } manager->isLoading = true; ++req->state; // Fallthrough. @@ -129,13 +123,14 @@ ecs_system_define(SceneLevelLoadSys) { ++req->state; // Fallthrough. case LevelLoadState_AssetAcquire: - req->levelAsset = asset_lookup(world, assets, req->levelId); asset_acquire(world, req->levelAsset); ++req->state; goto Wait; case LevelLoadState_AssetWait: if (ecs_world_has_t(world, req->levelAsset, AssetFailedComp)) { - log_e("Failed to load level asset", log_param("id", fmt_text(req->levelId))); + ecs_view_jump(assetItr, req->levelAsset); + const String assetId = asset_id(ecs_view_read_t(assetItr, AssetComp)); + log_e("Failed to load level asset", log_param("id", fmt_text(assetId))); manager->isLoading = false; goto Done; } @@ -145,15 +140,17 @@ ecs_system_define(SceneLevelLoadSys) { ++req->state; // Fallthrough. case LevelLoadState_Create: - if (ecs_view_maybe_jump(assetItr, req->levelAsset) == null) { - log_e("Invalid level asset", log_param("id", fmt_text(req->levelId))); + ecs_view_jump(assetItr, req->levelAsset); + const AssetLevelComp* levelComp = ecs_view_read_t(assetItr, AssetLevelComp); + if (!levelComp) { + const String assetId = asset_id(ecs_view_read_t(assetItr, AssetComp)); + log_e("Invalid level asset", log_param("id", fmt_text(assetId))); manager->isLoading = false; goto Done; } - const AssetLevelComp* levelComp = ecs_view_read_t(assetItr, AssetLevelComp); scene_level_process_load(world, &levelComp->level); - mem_swap(mem_var(manager->loadedLevelId), mem_var(req->levelId)); - manager->isLoading = false; + manager->isLoading = false; + manager->loadedLevelAsset = req->levelAsset; goto Done; } diag_crash_msg("Unexpected load state"); @@ -167,6 +164,31 @@ ecs_system_define(SceneLevelLoadSys) { } } +ecs_view_define(UnloadGlobalView) { ecs_access_write(SceneLevelManagerComp); } +ecs_view_define(UnloadRequestView) { ecs_access_with(SceneLevelRequestUnloadComp); } + +ecs_system_define(SceneLevelUnloadSys) { + EcsView* globalView = ecs_world_view_t(world, UnloadGlobalView); + EcsIterator* globalItr = ecs_view_maybe_at(globalView, ecs_world_global(world)); + if (!globalItr) { + return; + } + SceneLevelManagerComp* manager = ecs_view_write_t(globalItr, SceneLevelManagerComp); + + EcsView* requestView = ecs_world_view_t(world, UnloadRequestView); + EcsView* instanceView = ecs_world_view_t(world, InstanceView); + + for (EcsIterator* itr = ecs_view_itr(requestView); ecs_view_walk(itr);) { + if (manager->isLoading) { + log_e("Level unload failed; load in progress"); + } else if (manager->loadedLevelAsset) { + scene_level_process_unload(world, instanceView); + manager->loadedLevelAsset = 0; + } + ecs_world_entity_destroy(world, ecs_view_entity(itr)); + } +} + static void scene_level_object_push( DynArray* objects, // AssetLevelObject[], sorted on id. EcsIterator* instanceItr) { @@ -221,7 +243,11 @@ static void scene_level_process_save(AssetManagerComp* assets, const String id, dynarray_destroy(&objects); } -ecs_view_define(SaveGlobalView) { ecs_access_write(AssetManagerComp); } +ecs_view_define(SaveGlobalView) { + ecs_access_write(AssetManagerComp); + ecs_access_read(SceneLevelManagerComp); +} +ecs_view_define(SaveAssetView) { ecs_access_read(AssetComp); } ecs_view_define(SaveRequestView) { ecs_access_read(SceneLevelRequestSaveComp); } ecs_system_define(SceneLevelSaveSys) { @@ -230,22 +256,34 @@ ecs_system_define(SceneLevelSaveSys) { if (!globalItr) { return; } + const SceneLevelManagerComp* manager = ecs_view_read_t(globalItr, SceneLevelManagerComp); + AssetManagerComp* assets = ecs_view_write_t(globalItr, AssetManagerComp); - AssetManagerComp* assets = ecs_view_write_t(globalItr, AssetManagerComp); - EcsView* requestView = ecs_world_view_t(world, SaveRequestView); - EcsView* instanceView = ecs_world_view_t(world, InstanceView); + EcsView* requestView = ecs_world_view_t(world, SaveRequestView); + EcsView* assetView = ecs_world_view_t(world, SaveAssetView); + EcsView* instanceView = ecs_world_view_t(world, InstanceView); + + EcsIterator* assetItr = ecs_view_itr(assetView); for (EcsIterator* itr = ecs_view_itr(requestView); ecs_view_walk(itr);) { const SceneLevelRequestSaveComp* req = ecs_view_read_t(itr, SceneLevelRequestSaveComp); - scene_level_process_save(assets, req->levelId, instanceView); + if (manager->isLoading) { + log_e("Level save failed; load in progress"); + } else { + ecs_view_jump(assetItr, req->levelAsset); + const String assetId = asset_id(ecs_view_read_t(assetItr, AssetComp)); + + scene_level_process_save(assets, assetId, instanceView); + } ecs_world_entity_destroy(world, ecs_view_entity(itr)); } } ecs_module_init(scene_level_module) { - ecs_register_comp(SceneLevelManagerComp, .destructor = ecs_destruct_level_manager); - ecs_register_comp(SceneLevelRequestLoadComp, .destructor = ecs_destruct_level_request_load); - ecs_register_comp(SceneLevelRequestSaveComp, .destructor = ecs_destruct_level_request_save); + ecs_register_comp(SceneLevelManagerComp); + ecs_register_comp(SceneLevelRequestLoadComp); + ecs_register_comp_empty(SceneLevelRequestUnloadComp); + ecs_register_comp(SceneLevelRequestSaveComp); ecs_register_view(InstanceView); @@ -256,31 +294,46 @@ ecs_module_init(scene_level_module) { ecs_register_view(LoadAssetView), ecs_register_view(LoadRequestView)); + ecs_register_system( + SceneLevelUnloadSys, + ecs_view_id(InstanceView), + ecs_register_view(UnloadGlobalView), + ecs_register_view(UnloadRequestView)); + ecs_register_system( SceneLevelSaveSys, ecs_view_id(InstanceView), ecs_register_view(SaveGlobalView), + ecs_register_view(SaveAssetView), ecs_register_view(SaveRequestView)); } bool scene_level_is_loading(const SceneLevelManagerComp* manager) { return manager->isLoading; } -String scene_level_current_id(const SceneLevelManagerComp* manager) { - return manager->loadedLevelId; +EcsEntityId scene_level_current(const SceneLevelManagerComp* manager) { + return manager->loadedLevelAsset; +} + +void scene_level_load(EcsWorld* world, const EcsEntityId levelAsset) { + diag_assert(ecs_entity_valid(levelAsset)); + + const EcsEntityId reqEntity = ecs_world_entity_create(world); + ecs_world_add_t(world, reqEntity, SceneLevelRequestLoadComp, .levelAsset = levelAsset); } -void scene_level_load(EcsWorld* world, const String levelId) { - diag_assert(!string_is_empty(levelId)); +void scene_level_reload(EcsWorld* world) { + const EcsEntityId reqEntity = ecs_world_entity_create(world); + ecs_world_add_t(world, reqEntity, SceneLevelRequestLoadComp, .levelAsset = 0); +} +void scene_level_unload(EcsWorld* world) { const EcsEntityId reqEntity = ecs_world_entity_create(world); - ecs_world_add_t( - world, reqEntity, SceneLevelRequestLoadComp, .levelId = string_dup(g_alloc_heap, levelId)); + ecs_world_add_empty_t(world, reqEntity, SceneLevelRequestUnloadComp); } -void scene_level_save(EcsWorld* world, const String levelId) { - diag_assert(!string_is_empty(levelId)); +void scene_level_save(EcsWorld* world, const EcsEntityId levelAsset) { + diag_assert(ecs_entity_valid(levelAsset)); const EcsEntityId reqEntity = ecs_world_entity_create(world); - ecs_world_add_t( - world, reqEntity, SceneLevelRequestSaveComp, .levelId = string_dup(g_alloc_heap, levelId)); + ecs_world_add_t(world, reqEntity, SceneLevelRequestSaveComp, .levelAsset = levelAsset); } diff --git a/libs/ui/include/ui_shape.h b/libs/ui/include/ui_shape.h index 2c35eba4e..8fe8346a8 100644 --- a/libs/ui/include/ui_shape.h +++ b/libs/ui/include/ui_shape.h @@ -9,6 +9,7 @@ X(0xE050, VolumeUp) \ X(0xE069, WebAsset) \ X(0xE145, Add) \ + X(0xE161, Save) \ X(0xE162, SelectAll) \ X(0xE1DB, Storage) \ X(0xE25E, FormatShapes) \ @@ -29,6 +30,7 @@ X(0xE5CE, ExpandLess) \ X(0xE5CF, ExpandMore) \ X(0xE5D0, Fullscreen) \ + X(0xE5D5, Refresh) \ X(0xE5D6, UnfoldLess) \ X(0xE5D7, UnfoldMore) \ X(0xE71C, Animation) \