diff --git a/art/driving.dem b/art/driving.dem new file mode 100644 index 0000000..3e5c15e Binary files /dev/null and b/art/driving.dem differ diff --git a/code/game/CMakeLists.txt b/code/game/CMakeLists.txt index d68d03a..06f9c24 100644 --- a/code/game/CMakeLists.txt +++ b/code/game/CMakeLists.txt @@ -38,6 +38,6 @@ add_executable(eco2d target_compile_definitions(eco2d PRIVATE CLIENT) include_directories(src ../modules ../../art/gen) -target_link_libraries(eco2d raylib cwpack eco2d-modules flecs-bundle) +target_link_libraries(eco2d raylib cwpack eco2d-modules flecs-bundle vendors-bundle) link_system_libs(eco2d) diff --git a/code/game/src/debug_replay.c b/code/game/src/debug_replay.c index 66d0087..b2da204 100644 --- a/code/game/src/debug_replay.c +++ b/code/game/src/debug_replay.c @@ -2,7 +2,17 @@ #include "camera.h" #include "entity.h" +#include "cwpack/cwpack.h" + +typedef enum { + RPKIND_KEY, + + // NOTE(zaklaus): Special actions + RPKIND_SPAWN_CAR, +} replay_kind; + typedef struct { + replay_kind kind; pkt_send_keystate pkt; uint64_t delay; } replay_record; @@ -17,6 +27,66 @@ static uint64_t playback_time = 0; static ecs_entity_t mime = 0; static ecs_entity_t plr = 0; +#define REPLAY_MAGIC 0x421DC97E +#define REPLAY_VERSION 2 + +static char replay_filename[1024] = {0}; +static char replaybuf[UINT16_MAX]; + +void debug_replay_store(void) { + assert(replay_filename[0]); + assert(records); + + cw_pack_context pc = {0}; + cw_pack_context_init(&pc, replaybuf, sizeof(replaybuf), 0); + cw_pack_unsigned(&pc, REPLAY_MAGIC); + cw_pack_unsigned(&pc, REPLAY_VERSION); + cw_pack_array_size(&pc, zpl_array_count(records)); + + for (int i = 0; i < zpl_array_count(records); i++) { + cw_pack_bin(&pc, &records[i], sizeof(replay_record)); + } + + zpl_file f = {0}; + zpl_file_create(&f, replay_filename); + zpl_file_write(&f, replaybuf, pc.current - pc.start); + zpl_file_close(&f); +} + +void debug_replay_load(void) { + assert(replay_filename[0]); + + zpl_file f = {0}; + assert(zpl_file_open(&f, replay_filename) == ZPL_FILE_ERROR_NONE); + size_t file_size = zpl_file_size(&f); + zpl_file_read(&f, replaybuf, file_size); + zpl_file_close(&f); + + cw_unpack_context uc = {0}; + cw_unpack_context_init(&uc, replaybuf, file_size, 0); + + cw_unpack_next(&uc); + assert(uc.item.type == CWP_ITEM_POSITIVE_INTEGER && uc.item.as.u64 == REPLAY_MAGIC); + + cw_unpack_next(&uc); + assert(uc.item.type == CWP_ITEM_POSITIVE_INTEGER && uc.item.as.u64 == REPLAY_VERSION); + + cw_unpack_next(&uc); + assert(uc.item.type == CWP_ITEM_ARRAY); + size_t items = uc.item.as.array.size; + + zpl_array_init_reserve(records, zpl_heap(), sizeof(replay_record)*items); + + for (size_t i = 0; i < items; i++) { + cw_unpack_next(&uc); + assert(uc.item.type == CWP_ITEM_BIN); + + replay_record rec = {0}; + zpl_memcopy(&rec, uc.item.as.bin.start, sizeof(replay_record)); + zpl_array_append(records, rec); + } +} + void debug_replay_start(void) { is_recording = true; @@ -46,7 +116,7 @@ void debug_replay_run(void) { plr = camera_get().ent_id; Position const *p1 = ecs_get(world_ecs(), plr, Position); - mime = entity_spawn(EKIND_DEMO_NPC); + mime = entity_spawn(EKIND_MACRO_BOT); Position *pos = ecs_get_mut(world_ecs(), mime, Position, NULL); *pos = *p1; @@ -62,11 +132,25 @@ void debug_replay_update(void) { replay_record *r = &records[record_pos]; playback_time = zpl_time_rel() + r->delay; - Input *i = ecs_get_mut(world_ecs(), mime, Input, NULL); - i->x = r->pkt.x; - i->y = r->pkt.y; - i->use = r->pkt.use; - i->sprint = r->pkt.sprint; + switch (r->kind) { + case RPKIND_KEY: { + Input *i = ecs_get_mut(world_ecs(), mime, Input, NULL); + i->x = r->pkt.x; + i->y = r->pkt.y; + i->use = r->pkt.use; + i->sprint = r->pkt.sprint; + }break; + case RPKIND_SPAWN_CAR: { + ecs_entity_t e = vehicle_spawn(); + + Position const *origin = ecs_get(world_ecs(), mime, Position); + Position *dest = ecs_get_mut(world_ecs(), e, Position, NULL); + *dest = *origin; + }break; + default: { + ZPL_PANIC("unreachable"); + }break; + } record_pos += 1; @@ -85,6 +169,7 @@ void debug_replay_record_keystate(pkt_send_keystate state) { float record_time = zpl_time_rel_ms(); replay_record rec = { + .kind = RPKIND_KEY, .pkt = state, .delay = (record_time - last_record_time), }; @@ -92,3 +177,17 @@ void debug_replay_record_keystate(pkt_send_keystate state) { zpl_array_append(records, rec); last_record_time = zpl_time_rel_ms(); } + +void debug_replay_special_action(replay_kind kind) { + assert(kind != RPKIND_KEY); + if (!is_recording || is_playing) return; + float record_time = zpl_time_rel_ms(); + + replay_record rec = { + .kind = kind, + .delay = (record_time - last_record_time), + }; + + zpl_array_append(records, rec); + last_record_time = zpl_time_rel_ms(); +} \ No newline at end of file diff --git a/code/game/src/debug_ui.c b/code/game/src/debug_ui.c index 566ae97..3511780 100644 --- a/code/game/src/debug_ui.c +++ b/code/game/src/debug_ui.c @@ -4,11 +4,13 @@ #include "camera.h" #include "world/world.h" #include "game.h" +#include "sfd.h" #include "modules/components.h" typedef enum { DITEM_RAW, + DITEM_GAP, DITEM_TEXT, DITEM_BUTTON, DITEM_SLIDER, @@ -31,6 +33,7 @@ typedef struct { #define DBG_SHADOW_OFFSET_XPOS 1 #define DBG_SHADOW_OFFSET_YPOS 1 #define DBG_CTRL_HANDLE_DIM 10 +#define DBG_GAP_HEIGHT DBG_FONT_SPACING * 0.5f static uint8_t is_shadow_rendered; static uint8_t is_debug_open = 1; @@ -115,10 +118,14 @@ static debug_item items[] = { .name = "replay system", .list = { .items = (debug_item[]) { - { .kind = DITEM_TEXT, .name = "macro", .text = "", .proc = DrawLiteral }, + { .kind = DITEM_TEXT, .name = "macro", .proc = DrawReplayFileName }, { .kind = DITEM_TEXT, .name = "samples", .proc = DrawReplaySamples }, - { .kind = DITEM_BUTTON, .name = "load", .on_click = NULL }, - { .kind = DITEM_BUTTON, .name = "save", .on_click = NULL }, + { .kind = DITEM_BUTTON, .name = "new", .on_click = ActReplayNew }, + { .kind = DITEM_BUTTON, .name = "load", .on_click = ActReplayLoad }, + { .kind = DITEM_BUTTON, .name = "save", .on_click = ActReplaySave }, + { .kind = DITEM_BUTTON, .name = "save as...", .on_click = ActReplaySaveAs }, + + { .kind = DITEM_GAP }, { .kind = DITEM_COND, .on_success = CondReplayStatusOff }, { .kind = DITEM_BUTTON, .name = "record", .on_click = ActReplayBegin }, @@ -164,6 +171,9 @@ debug_draw_result debug_draw_list(debug_item *list, float xpos, float ypos, bool is_shadow_rendered = is_shadow; for (debug_item *it = list; it->kind != DITEM_END; it += 1) { switch (it->kind) { + case DITEM_GAP: { + ypos += DBG_GAP_HEIGHT; + }break; case DITEM_COND: { assert(it->on_success); diff --git a/code/game/src/debug_ui_actions.c b/code/game/src/debug_ui_actions.c index 26de526..1804404 100644 --- a/code/game/src/debug_ui_actions.c +++ b/code/game/src/debug_ui_actions.c @@ -13,6 +13,8 @@ ActSpawnCar(void) { Position const* origin = ecs_get(world_ecs(), plr, Position); Position * dest = ecs_get_mut(world_ecs(), e, Position, NULL); *dest = *origin; + + debug_replay_special_action(RPKIND_SPAWN_CAR); } // NOTE(zaklaus): Replay system @@ -52,3 +54,61 @@ ActReplayClear(void) { debug_replay_clear(); } + +static inline void +ActReplayNew(void) { + debug_replay_clear(); + zpl_zero_size(replay_filename, sizeof(replay_filename)); +} + +static inline void +ActReplaySaveAs(void) { + if (!records) return; + char const *workdir = GetWorkingDirectory(); + + sfd_Options sfd = { + .title = "Save Macro", + .path = "art", + .filter_name = "eco2d Macro", + .filter = "*.dem", + }; + + char const *path = sfd_save_dialog(&sfd); + ChangeDirectory(workdir); + + if (path) { + zpl_strcpy(replay_filename, zpl_bprintf("%s.dem", path)); + debug_replay_store(); + } + +} + +static inline void +ActReplaySave(void) { + if (!replay_filename[0]) { + ActReplaySaveAs(); + } + else debug_replay_store(); +} + +static inline void +ActReplayLoad(void) { + char const *workdir = GetWorkingDirectory(); + + sfd_Options sfd = { + .title = "Load Macro", + .path = "art", + .filter_name = "eco2d Macro", + .filter = "*.dem", + }; + + char const *path = sfd_open_dialog(&sfd); + ChangeDirectory(workdir); + + if (path) { + zpl_strcpy(replay_filename, path); + debug_replay_clear(); + debug_replay_load(); + } +} + diff --git a/code/game/src/debug_ui_widgets.c b/code/game/src/debug_ui_widgets.c index ae4623a..4a8425f 100644 --- a/code/game/src/debug_ui_widgets.c +++ b/code/game/src/debug_ui_widgets.c @@ -70,3 +70,9 @@ DrawReplaySamples(debug_item *it, float xpos, float ypos) { } return DrawFormattedText(xpos, ypos, TextFormat("%d of %d", record_pos, cnt)); } + +static inline debug_draw_result +DrawReplayFileName(debug_item *it, float xpos, float ypos) { + (void)it; + return DrawFormattedText(xpos, ypos, TextFormat("%s", replay_filename[0] ? replay_filename : "")); +} diff --git a/code/game/src/entity_view.h b/code/game/src/entity_view.h index fdb55a5..dd2b8a4 100644 --- a/code/game/src/entity_view.h +++ b/code/game/src/entity_view.h @@ -10,6 +10,7 @@ typedef enum { EKIND_VEHICLE, EKIND_DEMO_NPC, EKIND_MONSTER, + EKIND_MACRO_BOT, EKIND_CHUNK, FORCE_EKIND_UINT16 = UINT16_MAX } entity_kind; diff --git a/code/game/src/platform_raylib.c b/code/game/src/platform_raylib.c index 99752a6..e4c01db 100644 --- a/code/game/src/platform_raylib.c +++ b/code/game/src/platform_raylib.c @@ -108,6 +108,7 @@ void platform_input() { void display_conn_status(); +void DEBUG_draw_entities_low(uint64_t key, entity_view * data); void DEBUG_draw_entities(uint64_t key, entity_view * data); void DEBUG_draw_ground(uint64_t key, entity_view * data); @@ -134,6 +135,7 @@ void platform_render() { ClearBackground(GetColor(0x222034)); BeginMode2D(render_camera); game_world_view_active_entity_map(DEBUG_draw_ground); + game_world_view_active_entity_map(DEBUG_draw_entities_low); game_world_view_active_entity_map(DEBUG_draw_entities); EndMode2D(); display_conn_status(); @@ -217,7 +219,24 @@ void DEBUG_draw_entities(uint64_t key, entity_view * data) { DrawTextEco(title, x-title_w/2, y-size-font_size-fixed_title_offset, font_size, ColorAlpha(RAYWHITE, data->tran_time), font_spacing); #endif DrawCircleEco(x, y, size, ColorAlpha(YELLOW, data->tran_time)); - }break; + }break; + case EKIND_MACRO_BOT: { + float x = data->x; + float y = data->y; + const char *title = TextFormat("Bot %d", key); + int title_w = MeasureTextEco(title, font_size, font_spacing); + DrawRectangleEco(x-title_w/2-title_bg_offset/2, y-size-font_size-fixed_title_offset, title_w+title_bg_offset, font_size, ColorAlpha(GRAY, data->tran_time)); + DrawTextEco(title, x-title_w/2, y-size-font_size-fixed_title_offset, font_size, ColorAlpha(BLACK, data->tran_time), font_spacing); + DrawCircleEco(x, y, size, ColorAlpha(PURPLE, data->tran_time)); + }break; + default:break; + } +} + +void DEBUG_draw_entities_low(uint64_t key, entity_view * data) { + (void)key; + + switch (data->kind) { case EKIND_VEHICLE: { float x = data->x; float y = data->y; diff --git a/code/vendors/CMakeLists.txt b/code/vendors/CMakeLists.txt index a8e9f2d..be42f43 100644 --- a/code/vendors/CMakeLists.txt +++ b/code/vendors/CMakeLists.txt @@ -1,2 +1,7 @@ add_subdirectory(flecs) add_subdirectory(cwpack) + +add_library(vendors-bundle STATIC + sfd.c +) + diff --git a/code/vendors/sfd.c b/code/vendors/sfd.c new file mode 100644 index 0000000..d84c401 --- /dev/null +++ b/code/vendors/sfd.c @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2017 rxi + * + * 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. + */ + +#include +#include +#include +#include + +#include "sfd.h" + + +static const char *last_error; + + +const char* sfd_get_error(void) { + const char *res = last_error; + last_error = NULL; + return res; +} + + +static int next_filter(char *dst, const char **p) { + int len; + + *p += strspn(*p, "|"); + if (**p == '\0') { + return 0; + } + + len = strcspn(*p, "|"); + memcpy(dst, *p, len); + dst[len] = '\0'; + *p += len; + + return 1; +} + + +/****************************************************************************** +** Windows +*******************************************************************************/ + +#ifdef _WIN32 + +#include + +typedef struct { + unsigned long process_id; + void* handle_root; + void* handle_first; +} FindMainWindowInfo; + + +static int find_main_window_callback(HWND handle, LPARAM lParam) { + FindMainWindowInfo* info = (FindMainWindowInfo*)lParam; + unsigned long process_id = 0; + GetWindowThreadProcessId(handle, &process_id); + if (info->process_id == process_id) { + info->handle_first = handle; + if (GetWindow(handle, GW_OWNER) == 0 && IsWindowVisible(handle)) { + info->handle_root = handle; + return 0; + } + } + return 1; +} + + +static HWND find_main_window() { + FindMainWindowInfo info = { + .process_id = GetCurrentProcessId() + }; + EnumWindows(find_main_window_callback, (LPARAM)&info); + return info.handle_root; +} + + +static const char* make_filter_str(sfd_Options *opt) { + static char buf[1024]; + int n; + + buf[0] = '\0'; + n = 0; + + if (opt->filter) { + const char *p; + char b[32]; + const char *name = opt->filter_name ? opt->filter_name : opt->filter; + n += sprintf(buf + n, "%s", name) + 1; + + p = opt->filter; + while (next_filter(b, &p)) { + n += sprintf(buf + n, "%s;", b); + } + + buf[++n] = '\0'; + } + + n += sprintf(buf + n, "All Files") + 1; + n += sprintf(buf + n, "*.*"); + buf[++n] = '\0'; + + return buf; +} + + +static void init_ofn(OPENFILENAME *ofn, sfd_Options *opt) { + static char result_buf[2048]; + result_buf[0] = '\0'; + + memset(ofn, 0, sizeof(*ofn)); + ofn->hwndOwner = find_main_window(); + ofn->lStructSize = sizeof(*ofn); + ofn->lpstrFilter = make_filter_str(opt); + ofn->nFilterIndex = 1; + ofn->lpstrFile = result_buf; + ofn->Flags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT; + ofn->nMaxFile = sizeof(result_buf) - 1; + ofn->lpstrInitialDir = opt->path; + ofn->lpstrTitle = opt->title; + ofn->lpstrDefExt = opt->extension; +} + + +const char* sfd_open_dialog(sfd_Options *opt) { + int ok; + OPENFILENAME ofn; + last_error = NULL; + init_ofn(&ofn, opt); + ok = GetOpenFileName(&ofn); + return ok ? ofn.lpstrFile : NULL; +} + + +const char* sfd_save_dialog(sfd_Options *opt) { + int ok; + OPENFILENAME ofn; + last_error = NULL; + init_ofn(&ofn, opt); + ok = GetSaveFileName(&ofn); + return ok ? ofn.lpstrFile : NULL; +} + +#endif + + +/****************************************************************************** +** Zenity +*******************************************************************************/ + +#ifndef _WIN32 + + +static const char* file_dialog(sfd_Options *opt, int save) { + static char result_buf[2048]; + char buf[2048]; + char *p; + const char *title; + FILE *fp; + int n, len; + + last_error = NULL; + + fp = popen("zenity --version", "r"); + if (fp == NULL || pclose(fp) != 0) { + last_error = "could not open zenity"; + return NULL; + } + + + n = sprintf(buf, "zenity --file-selection"); + + if (save) { + n += sprintf(buf + n, " --save --confirm-overwrite"); + } + + if (opt->title) { + title = opt->title; + } else { + title = save ? "Save File" : "Open File"; + } + + n += sprintf(buf + n, " --title=\"%s\"", title); + + if (opt->path && opt->path[0] != '\0') { + n += sprintf(buf + n, " --filename=\""); + p = realpath(opt->path, buf + n); + if (p == NULL) { + last_error = "call to realpath() failed"; + return NULL; + } + n += strlen(buf + n); + n += sprintf(buf + n, "/\""); + } + + if (opt->filter) { + char b[64]; + const char *p; + n += sprintf(buf + n, " --file-filter=\""); + + if (opt->filter_name) { + n += sprintf(buf + n, "%s | ", opt->filter_name); + } + + p = opt->filter; + while (next_filter(b, &p)) { + n += sprintf(buf + n, "\"%s\" ", b); + } + + n += sprintf(buf + n, "\""); + } + + n += sprintf(buf + n, " --file-filter=\"All Files | *\""); + + + fp = popen(buf, "r"); + len = fread(result_buf, 1, sizeof(result_buf) - 1, fp); + pclose(fp); + + if (len > 0) { + result_buf[len - 1] = '\0'; + if (save && opt->extension && !strstr(result_buf, opt->extension)) { + sprintf(&result_buf[len - 1], ".%s", opt->extension); + } + return result_buf; + } + + return NULL; +} + + +const char* sfd_open_dialog(sfd_Options *opt) { + return file_dialog(opt, 0); +} + + +const char* sfd_save_dialog(sfd_Options *opt) { + return file_dialog(opt, 1); +} + + +#endif diff --git a/code/vendors/sfd.h b/code/vendors/sfd.h new file mode 100644 index 0000000..8149f3e --- /dev/null +++ b/code/vendors/sfd.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2017 rxi + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MIT license. See `sfd.c` for details. + */ + +#ifndef SFD_H +#define SFD_H + +#define SFD_VERSION "0.1.0" + +typedef struct { + const char *title; + const char *path; + const char *filter_name; + const char *filter; + const char *extension; +} sfd_Options; + +const char* sfd_get_error(void); +const char* sfd_open_dialog(sfd_Options *opt); +const char* sfd_save_dialog(sfd_Options *opt); + +#endif