From d887a0987d699533e1c6291b887ee21c9985ec76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Madar=C3=A1sz?= Date: Tue, 10 Aug 2021 20:31:05 +0200 Subject: [PATCH] code: Improvements to the replay system --- art/driving.dem | Bin 0 -> 60069 bytes code/game/CMakeLists.txt | 2 +- code/game/src/debug_replay.c | 111 ++++++++++++- code/game/src/debug_ui.c | 16 +- code/game/src/debug_ui_actions.c | 60 +++++++ code/game/src/debug_ui_widgets.c | 6 + code/game/src/entity_view.h | 1 + code/game/src/platform_raylib.c | 21 ++- code/vendors/CMakeLists.txt | 5 + code/vendors/sfd.c | 261 +++++++++++++++++++++++++++++++ code/vendors/sfd.h | 25 +++ 11 files changed, 497 insertions(+), 11 deletions(-) create mode 100644 art/driving.dem create mode 100644 code/vendors/sfd.c create mode 100644 code/vendors/sfd.h diff --git a/art/driving.dem b/art/driving.dem new file mode 100644 index 0000000000000000000000000000000000000000..3e5c15e51d32d9b4d01c2d13815674693a157bb7 GIT binary patch literal 60069 zcmeI5OOhlt5Ja0bME z$2~k;A?2sKn}2@)pV8+ zg}-vvajYXL^0+$x9bFozN2AXm*e$mJNd`^><&Ht8JCF%EvfU2pcB zaWIN=wmIN_7#X#u>|oStuR32oGrc^=f8Oj*ygo1g#rBDRCHnMAZuglEpZ?qrBRo~i zVZ^BAa*P`HW3o05m}5?VR^v{YV;q<|?0^xj!z(wcANFSYF>0gg7;lDS`WeN^oj2op zRHAR~wA{xSZ!>eu`rIeSxaO66>+F;}TjLr zvt^LmPqgRuCM3ES+vDWA<~d-59WY|l9*1+Eoto+8P8s>F9KfIceEEK-?vi&E2oP2? zu5;&_PYSMM5YB)Ru5+Ip<9bvEJO@0Zx!#N#C&~!dVZ^90hY@zbh*5hS&f|2-6T-+! zCw``#<(Y|3WnNQ8a#x$x80oJi<+};vXN-pQ{WPSn(-6bSRbvN?7zh1?vl z5zdVfqqaEPGqFw8%G#x4a^p#4#MBwZ8RdFZKaW#~J7r|lYOgv^PowSv`tNaW*M)Do zw_gq6^Y52LbIq&3YRqB8tSLL-yLINUH-m6LjF>vy4`*N; zFozND=XlQX4oo^`O--l9o2kPbMvOP+Fv1QPVKqjKS}upx7~y^xVKqirjS-`k%Q0&A z*$;QhXBA(wax-AH`$RZv?q_k_8>W@pYq@bcjPQ&yV$}F_;SsH^VF!y7Jh5D|K3i@xueQnhZ2g=qgPGUIoV|Sg;j0X1 zz{sdMFm)Cu_)d$<6(e^|XU_UQJ2kFnUWHNKOl~uCOm07ymvo>0y!>Qpdz1CP0s&&` zJPwCDHOS2KcDy%szzDCx;*5K4T&v*jxXg^2thvswcwOj!mqELh7zZxT5+8p0e#jN0*>;~m^K(QAGZpwD0?PM=Qm7P~ILh3~DLGWW-G zMmdOcn8zZ@OSj{+K4kM!+%v@(wI-@ce)lY66 zqh=B{XQHVyGsk3YI4gr0?vczYoNcvD2F&3zggK0?thZ0%?TN*t!yJR;t{gkay(hoz zjFu-K{|=?kpsDp4G=w?*`YcXK;AMSQZrk5R($eACVZ1$ijDslA=MAxW zUseCMe9h(bXK$+$oo^M5I;}{qH?G47J79#>7|B)RtWBNsa@PM#?&UjF+CDw+YPTNi zKjofPW^Z%Wx!z12=5VKc?wEcIhrMw>qv{xM%wdE(WrWojVKqjK+T(B@r_(b{Nc0(U zyVh{0jPM*VV$?=)jBDfVK2LJfX;z~3O;)6~heWRT-it-+Q#sL{!=4(uHr|-Su9;)J zEzb5cqjHYkVh&8!JO};sws-hFry+*Z&s$Str0?`TIL5Wfz^sp{Gc#vY)}{_~7~!48 zoSE0hcw3!r-)xUR`Ho)&b};iUVb^9a<1oT%jIbIbMvXa}J$FfG^W=I#MtPgL4kvoNYqN4Z z2iz&wVGgIm2&)-{>lh@%0iRV3>Ak&<6m)wnsefC(Mrsx9`)M45a2-atj^P-!QFYiG zBkV0g%9C3>2NhusBmJicr9xHf&x3|U{p>j~9CmHg4CiLn3dU zW7OC+t1-fAj2N}Wag%s_ZpOjeW+#6BDZ8p7k=h<7a=phfbZiubDy0W*E|PCZ4{?JN6W0&N$k(j zhA@XSV5IM~RId&g>AhXU>0LLDQS19@YK-Le!)lDMH-nfu%wgnsZ|uOh&YeVdzzFxl z2&*wNO3l>C<@9UXPRl->hV z2#MeClp1@8QF|PY=`@;<`+GZC9LKYB4btZ}GpFvv^0jx}kZ4USx;WXnWhENl`J zi8Ej%+F5%m$7->+qQ}VbYQ}-#m<+fMuUzi43_IX{3}Wg;oVQ-lpYK$k-_-YN4e9%7 z$k{li(@`0W@@DEVhmldatvcIlW81+quMe+WfBLt6`qRH5hGWuU*G4UaQ|>_CdR=Io zzMqEl-Wu}QoRWjP=ZkXlYW>7lrSi4kB@Hp0Xb!5P=_gmsI2gs5xzoqVfL9@RFYumY z2aJqbpR?65iJA<|?3g;EIHPjoei&hIyytj*xDF%ifH{n?nn6sRnK`)`aHj@|euA&w o3GpOy9qx2-w$HTf9(LvCo<3&IEqCIL+RW)3@69+ct8g36e~ZrnPyhe` literal 0 HcmV?d00001 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