code: Improvements to the replay system
parent
3c46cca96d
commit
d887a0987d
Binary file not shown.
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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 = "<unnamed>", .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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 : "<unnamed>"));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
add_subdirectory(flecs)
|
||||
add_subdirectory(cwpack)
|
||||
|
||||
add_library(vendors-bundle STATIC
|
||||
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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#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 <windows.h>
|
||||
|
||||
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
|
|
@ -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
|
Loading…
Reference in New Issue