summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-18 04:26:33 +0800
committerKonAki <konaki@anokonaki.org>2026-10-18 04:26:33 +0800
commit5871c92a35771e5ae84613377387154f8afb0f3b (patch)
tree14f8c3de9c6b946a47b6dc76cda49137120093b7
parentd9d6f47f8a973d6f335c9bac8254d0b295dadac0 (diff)
konedit: asset previews and the pack tab
-rw-r--r--editor/konedit.c365
1 files changed, 346 insertions, 19 deletions
diff --git a/editor/konedit.c b/editor/konedit.c
index ff4020b..181c598 100644
--- a/editor/konedit.c
+++ b/editor/konedit.c
@@ -17,14 +17,17 @@
#include "konui.h"
#include "kontoml.h"
#include "konfs.h"
+#include "konpack.h"
+#include "stb_image.h"
-#if !defined(KON_MODULE_UI) || !defined(KON_MODULE_FONT) || !defined(KON_MODULE_TOML) || !defined(KON_MODULE_FS)
+#if !defined(KON_MODULE_UI) || !defined(KON_MODULE_FONT) || !defined(KON_MODULE_TOML) || !defined(KON_MODULE_FS) || !defined(KON_MODULE_PACKW) || !defined(KON_MODULE_IMAGE)
#error "build with: ./build.sh -d konedit"
#endif
#define PATH 512
#define SIDE_WIDTH 260
-#define TABS 5
+#define TABS 6
+#define PACK_SHOWN 300
/*** colors ***/
@@ -52,9 +55,21 @@ typedef struct project {
int dirty;
char dir[PATH];
char name[64];
- char assets[64];
+ char assets[128];
+ char pack[128];
+ char key[128];
} project_t;
+/* what the Assets tab shows of the file picked on the left, kept so it is not read again every frame */
+typedef struct preview {
+ char path[PATH];
+ long long size;
+ kon_image *image;
+ int width, height;
+ char text[1600];
+ int isText;
+} preview_t;
+
typedef enum dialog {
DIALOG_NONE,
DIALOG_OPEN,
@@ -71,6 +86,10 @@ typedef struct state {
kon_ui_t *ui;
project_t project;
browser_t side, pick;
+ preview_t preview;
+ kon_fsList_t packList;
+ char packInfo[200], packResult[200];
+ int packTreeOpen;
kon_uiArea_t sideArea, mainArea, statusArea, dialogTop, dialogList;
dialog_t dialog;
char pathInput[PATH];
@@ -81,11 +100,12 @@ typedef struct state {
char status[200];
} state_t;
-static const char *tabNames[TABS] = { "Project", "Assets", "Tilemap", "Script", "Level" };
+static const char *tabNames[TABS] = { "Project", "Assets", "Pack", "Tilemap", "Script", "Level" };
static const char *tabAbout[TABS] = {
"",
"",
+ "",
"the tilemap editor: paint tiles on layers, pick tiles from a sheet, save the map the engine loads",
"the dialogue and script editor: write konscript with a text box preview next to it",
"the sector level editor: draw rooms in 2d, see them in 3d, save a kmap"
@@ -146,6 +166,29 @@ static const char *kindOf(const char *name)
return "file";
}
+static int isTextKind(const char *kind)
+{
+ return strcmp(kind, "text") == 0 || strcmp(kind, "settings") == 0 || strcmp(kind, "sector map") == 0;
+}
+
+/* a path from project.toml is from the project folder, unless it starts at a root */
+static void projectPath(const project_t *p, const char *rel, char *out, size_t size)
+{
+ if (kon_fsIsAbsolute(rel)) copy(out, size, rel);
+ else kon_fsJoin(out, size, p->dir, rel);
+}
+
+static long long fileSize(const char *path)
+{
+ FILE *f = fopen(path, "rb");
+ long long size = -1;
+
+ if (!f) return -1;
+ if (fseek(f, 0, SEEK_END) == 0) size = (long long)ftell(f);
+ fclose(f);
+ return size;
+}
+
/*** browser ***/
static int browserGo(browser_t *b, const char *path)
@@ -213,6 +256,74 @@ static int browserDraw(state_t *state, browser_t *b)
return result;
}
+/*** preview ***/
+
+static void previewFree(preview_t *v)
+{
+ if (v->image) kon_freeImage(v->image);
+ memset(v, 0, sizeof(*v));
+}
+
+static void previewLoad(preview_t *v, const char *path, const char *kind, long long size)
+{
+ previewFree(v);
+ copy(v->path, sizeof(v->path), path);
+ v->size = size;
+
+ if (strcmp(kind, "image") == 0) {
+ int w, h, n;
+ unsigned char *pixels = size < (64LL << 20) ? stbi_load(path, &w, &h, &n, 4) : NULL;
+
+ if (pixels) {
+ v->image = kon_loadImage(pixels, w, h, konFormatRGBA8);
+ v->width = w;
+ v->height = h;
+ stbi_image_free(pixels);
+ }
+ } else if (isTextKind(kind)) {
+ FILE *f = fopen(path, "rb");
+
+ if (f) {
+ size_t got = fread(v->text, 1, sizeof(v->text) - 1, f), i, out = 0;
+
+ fclose(f);
+ for (i = 0; i < got; i++) {
+ unsigned char c = (unsigned char)v->text[i];
+
+ if (c == '\r') continue;
+ v->text[out++] = (char)(c == '\n' || (c >= 32 && c < 127) ? c : (c == '\t' ? ' ' : '.'));
+ }
+ v->text[out] = 0;
+ v->isText = 1;
+ }
+ }
+}
+
+/* the size a picture is shown at: small ones are blown up whole times, big ones shrink to fit */
+static void previewSize(int w, int h, int maxW, int maxH, int *outW, int *outH)
+{
+ int k;
+
+ if (maxW < 16) maxW = 16;
+ if (w > maxW || h > maxH) {
+ if ((long long)w * maxH > (long long)h * maxW) {
+ *outW = maxW;
+ *outH = (int)((long long)h * maxW / w);
+ } else {
+ *outH = maxH;
+ *outW = (int)((long long)w * maxH / h);
+ }
+ if (*outW < 1) *outW = 1;
+ if (*outH < 1) *outH = 1;
+ return;
+ }
+ k = maxW / w < maxH / h ? maxW / w : maxH / h;
+ if (k > 8) k = 8;
+ if (k < 1) k = 1;
+ *outW = w * k;
+ *outH = h * k;
+}
+
/*** project ***/
static int projectLoad(project_t *p, const char *dir, char *error, size_t errorSize)
@@ -235,6 +346,8 @@ static int projectLoad(project_t *p, const char *dir, char *error, size_t errorS
copy(p->dir, sizeof(p->dir), dir);
copy(p->name, sizeof(p->name), kon_tomlGetString(toml, "project", "name", kon_fsBaseName(dir)));
copy(p->assets, sizeof(p->assets), kon_tomlGetString(toml, "project", "assets", "assets"));
+ copy(p->pack, sizeof(p->pack), kon_tomlGetString(toml, "pack", "output", "data.kpk"));
+ copy(p->key, sizeof(p->key), kon_tomlGetString(toml, "pack", "key", ""));
kon_freeToml(toml);
return 1;
}
@@ -259,6 +372,10 @@ static int projectSave(project_t *p)
writeString(f, p->name);
fputs("\nassets = ", f);
writeString(f, p->assets);
+ fputs("\n\n[pack]\noutput = ", f);
+ writeString(f, p->pack);
+ fputs("\nkey = ", f);
+ writeString(f, p->key);
fputc('\n', f);
if (fclose(f) != 0) return 0;
p->dirty = 0;
@@ -279,6 +396,7 @@ static int projectCreate(project_t *p, const char *dir, const char *name, char *
copy(p->dir, sizeof(p->dir), dir);
copy(p->name, sizeof(p->name), name[0] ? name : kon_fsBaseName(dir));
copy(p->assets, sizeof(p->assets), "assets");
+ copy(p->pack, sizeof(p->pack), "data.kpk");
kon_fsJoin(assets, sizeof(assets), dir, p->assets);
if (!kon_fsMakeDir(assets) || !projectSave(p)) {
snprintf(error, errorSize, "cannot write in %s", tail(dir, 60));
@@ -288,6 +406,127 @@ static int projectCreate(project_t *p, const char *dir, const char *name, char *
return 1;
}
+/*** pack ***/
+
+static void scanPack(state_t *state)
+{
+ project_t *p = &state->project;
+ char assets[PATH], out[PATH];
+ long long total = 0, onDisk;
+ size_t i;
+
+ kon_fsFree(&state->packList);
+ state->packInfo[0] = 0;
+ if (!p->open) return;
+
+ projectPath(p, p->assets, assets, sizeof(assets));
+ if (!kon_fsWalk(assets, &state->packList)) {
+ snprintf(state->packInfo, sizeof(state->packInfo), "no folder %s", tail(assets, 60));
+ return;
+ }
+ for (i = 0; i < state->packList.count; i++) total += state->packList.items[i].size;
+
+ projectPath(p, p->pack, out, sizeof(out));
+ onDisk = fileSize(out);
+ if (onDisk >= 0) {
+ snprintf(state->packInfo, sizeof(state->packInfo), "%d files, %lld bytes in the assets folder. the pack on disk is %lld bytes",
+ (int)state->packList.count, total, onDisk);
+ } else {
+ snprintf(state->packInfo, sizeof(state->packInfo), "%d files, %lld bytes in the assets folder. no pack built yet",
+ (int)state->packList.count, total);
+ }
+}
+
+/* 1 for a good key, 0 when it is not 32 bytes, -1 when it cannot be opened */
+static int readKey(const char *path, uint8_t key[KON_PACK_KEY_SIZE])
+{
+ FILE *f = fopen(path, "rb");
+ size_t got;
+ uint8_t extra;
+ int ok;
+
+ if (!f) return -1;
+ got = fread(key, 1, KON_PACK_KEY_SIZE, f);
+ ok = got == KON_PACK_KEY_SIZE && fread(&extra, 1, 1, f) == 0;
+ fclose(f);
+ return ok;
+}
+
+static void buildPack(state_t *state)
+{
+ project_t *p = &state->project;
+ char assets[PATH], out[PATH], keyPath[PATH];
+ uint8_t key[KON_PACK_KEY_SIZE];
+ const uint8_t *useKey = NULL;
+ char **paths;
+ int i, n = 0, count, failed = 0;
+ kon_pack_t *check;
+
+ state->packResult[0] = 0;
+ scanPack(state);
+ count = (int)state->packList.count;
+ if (count == 0) {
+ snprintf(state->packResult, sizeof(state->packResult), "nothing to pack");
+ return;
+ }
+ projectPath(p, p->assets, assets, sizeof(assets));
+ projectPath(p, p->pack, out, sizeof(out));
+ if (p->key[0]) {
+ int keyState;
+
+ projectPath(p, p->key, keyPath, sizeof(keyPath));
+ keyState = readKey(keyPath, key);
+ if (keyState < 0) {
+ snprintf(state->packResult, sizeof(state->packResult), "cannot read the key file %s", tail(keyPath, 40));
+ return;
+ }
+ if (keyState == 0) {
+ snprintf(state->packResult, sizeof(state->packResult), "the key file has to be exactly %d bytes: %s", KON_PACK_KEY_SIZE, tail(keyPath, 40));
+ return;
+ }
+ useKey = key;
+ }
+
+ paths = calloc((size_t)count, sizeof(char *));
+ if (!paths) {
+ snprintf(state->packResult, sizeof(state->packResult), "out of memory");
+ return;
+ }
+ for (i = 0; i < count; i++) {
+ char full[PATH];
+
+ kon_fsJoin(full, sizeof(full), assets, state->packList.items[i].name);
+ if (strcmp(full, out) == 0) continue;
+ paths[n] = malloc(strlen(full) + 1);
+ if (!paths[n]) {
+ failed = 1;
+ break;
+ }
+ strcpy(paths[n], full);
+ n++;
+ }
+
+ if (failed) {
+ snprintf(state->packResult, sizeof(state->packResult), "out of memory");
+ } else if (n == 0) {
+ snprintf(state->packResult, sizeof(state->packResult), "nothing to pack");
+ } else if (kon_packWrite(out, useKey, assets, (const char *const *)paths, n) != 0) {
+ snprintf(state->packResult, sizeof(state->packResult), "%s", kon_packError());
+ } else if ((check = kon_packOpen(out, useKey)) == NULL) {
+ snprintf(state->packResult, sizeof(state->packResult), "built it but cannot read it back: %s", kon_packError());
+ } else {
+ snprintf(state->packResult, sizeof(state->packResult), "built %s: %d files, %lld bytes%s", kon_fsBaseName(out), kon_packCount(check),
+ fileSize(out), useKey ? ", encrypted" : "");
+ kon_packClose(check);
+ }
+ for (i = 0; i < n; i++) free(paths[i]);
+ free(paths);
+ scanPack(state);
+ setStatus(state, "%s", state->packResult);
+}
+
+/*** project actions ***/
+
static void projectShow(state_t *state)
{
copy(state->side.root, sizeof(state->side.root), state->project.dir);
@@ -296,6 +535,9 @@ static void projectShow(state_t *state)
state->mainArea.scroll = 0;
state->tab = 0;
state->dialog = DIALOG_NONE;
+ state->packResult[0] = 0;
+ previewFree(&state->preview);
+ scanPack(state);
}
static void openProject(state_t *state, const char *dir)
@@ -339,6 +581,10 @@ static void closeProject(state_t *state)
setStatus(state, "closed %s", state->project.name);
memset(&state->project, 0, sizeof(state->project));
browserFree(&state->side);
+ previewFree(&state->preview);
+ kon_fsFree(&state->packList);
+ state->packInfo[0] = 0;
+ state->packResult[0] = 0;
state->sideArea.scroll = 0;
}
@@ -372,7 +618,10 @@ static void drawMenus(kon_program_t *program, state_t *state)
}
kon_uiEndMenu(ui);
if (kon_uiBeginMenu(ui, "View")) {
- if (kon_uiMenuItem(ui, "Refresh files", "F5") && state->project.open) browserGo(&state->side, state->side.path);
+ if (kon_uiMenuItem(ui, "Refresh files", "F5") && state->project.open) {
+ browserGo(&state->side, state->side.path);
+ scanPack(state);
+ }
}
kon_uiEndMenu(ui);
kon_uiEndMenuBar(ui);
@@ -393,21 +642,24 @@ static void drawSide(state_t *state)
kon_uiEndArea(ui);
}
+/* an input for a text of the project that marks it as unsaved when it changes */
+static void projectInput(state_t *state, const char *title, const char *id, char *value, size_t size)
+{
+ char before[128];
+
+ kon_uiLabel(state->ui, title);
+ copy(before, sizeof(before), value);
+ kon_uiTextInput(state->ui, id, value, size);
+ if (strcmp(before, value) != 0) state->project.dirty = 1;
+}
+
static void drawProjectTab(state_t *state)
{
kon_ui_t *ui = state->ui;
project_t *p = &state->project;
- char before[64];
-
- kon_uiLabel(ui, "name");
- copy(before, sizeof(before), p->name);
- kon_uiTextInput(ui, "project name##name", p->name, sizeof(p->name));
- if (strcmp(before, p->name) != 0) p->dirty = 1;
- kon_uiLabel(ui, "assets folder");
- copy(before, sizeof(before), p->assets);
- kon_uiTextInput(ui, "assets folder##assets", p->assets, sizeof(p->assets));
- if (strcmp(before, p->assets) != 0) p->dirty = 1;
+ projectInput(state, "name", "project name##name", p->name, sizeof(p->name));
+ projectInput(state, "assets folder", "assets folder##assets", p->assets, sizeof(p->assets));
kon_uiSeparator(ui);
kon_uiLabel(ui, tail(p->dir, 60));
@@ -423,7 +675,10 @@ static void drawAssetsTab(state_t *state)
{
kon_ui_t *ui = state->ui;
browser_t *b = &state->side;
+ preview_t *v = &state->preview;
const kon_fsEntry_t *found = NULL;
+ const char *kind;
+ char path[PATH];
size_t i;
if (b->selected[0]) {
@@ -433,13 +688,79 @@ static void drawAssetsTab(state_t *state)
}
if (!found) {
kon_uiLabel(ui, "pick a file on the left");
- kon_uiLabel(ui, "the asset browser and the pack tool come here");
return;
}
+ kind = kindOf(found->name);
+ kon_fsJoin(path, sizeof(path), b->path, found->name);
+ if (strcmp(v->path, path) != 0 || v->size != found->size) previewLoad(v, path, kind, found->size);
+
kon_uiLabel(ui, found->name);
- kon_uiLabelf(ui, "kind: %s", kindOf(found->name));
- kon_uiLabelf(ui, "size: %lld bytes", found->size);
+ kon_uiLabelf(ui, "%s, %lld bytes", kind, found->size);
kon_uiLabel(ui, tail(b->path, 60));
+ kon_uiSeparator(ui);
+
+ if (strcmp(kind, "image") == 0) {
+ if (v->image) {
+ int w, h;
+
+ previewSize(v->width, v->height, state->mainArea.w - 60, 320, &w, &h);
+ kon_uiLabelf(ui, "%d x %d", v->width, v->height);
+ kon_uiImage(ui, v->image, w, h);
+ } else {
+ kon_uiLabel(ui, "cannot read this image");
+ }
+ } else if (v->isText) {
+ const char *at = v->text;
+ int lines = 0;
+
+ while (*at && lines < 24) {
+ char line[120];
+ size_t n = strcspn(at, "\n");
+
+ if (n > sizeof(line) - 1) n = sizeof(line) - 1;
+ memcpy(line, at, n);
+ line[n] = 0;
+ kon_uiLabel(ui, line[0] ? line : " ");
+ at += strcspn(at, "\n");
+ if (*at == '\n') at++;
+ lines++;
+ }
+ if (*at) kon_uiLabel(ui, "...");
+ }
+}
+
+static void drawPackTab(state_t *state)
+{
+ kon_ui_t *ui = state->ui;
+ project_t *p = &state->project;
+ size_t i, shown;
+
+ projectInput(state, "pack file", "pack file##pack", p->pack, sizeof(p->pack));
+ projectInput(state, "key file, 32 bytes, or empty for no encryption", "key file##key", p->key, sizeof(p->key));
+
+ kon_uiSeparator(ui);
+ kon_uiNextWidth(ui, 110);
+ if (kon_uiButton(ui, "Scan")) scanPack(state);
+ kon_uiSameLine(ui);
+ kon_uiNextWidth(ui, 110);
+ if (kon_uiButton(ui, "Build Pack")) buildPack(state);
+ if (p->dirty) {
+ kon_uiSameLine(ui);
+ kon_uiLabel(ui, "unsaved");
+ }
+
+ if (state->packInfo[0]) kon_uiLabel(ui, state->packInfo);
+ if (state->packResult[0]) kon_uiLabel(ui, state->packResult);
+
+ kon_uiSeparator(ui);
+ kon_uiTreeNode(ui, "what goes in the pack", &state->packTreeOpen);
+ if (!state->packTreeOpen) return;
+
+ kon_uiIndent(ui, 8);
+ shown = state->packList.count < PACK_SHOWN ? state->packList.count : PACK_SHOWN;
+ for (i = 0; i < shown; i++) kon_uiLabelf(ui, "%s (%lld)", state->packList.items[i].name, state->packList.items[i].size);
+ if (shown < state->packList.count) kon_uiLabelf(ui, "and %d more", (int)(state->packList.count - shown));
+ kon_uiIndent(ui, -8);
}
static void drawMain(state_t *state)
@@ -455,6 +776,7 @@ static void drawMain(state_t *state)
kon_uiSeparator(ui);
if (state->tab == 0) drawProjectTab(state);
else if (state->tab == 1) drawAssetsTab(state);
+ else if (state->tab == 2) drawPackTab(state);
else kon_uiLabel(ui, tabAbout[state->tab]);
}
kon_uiEndArea(ui);
@@ -542,7 +864,10 @@ static int event(kon_program_t *program, const kon_event_t *ev, void *user)
if (ev->key == KON_KEY_LEFT_CTRL || ev->key == KON_KEY_RIGHT_CTRL) state->ctrl = down;
if (down && ev->key == KON_KEY_S && state->ctrl) saveProject(state);
if (down && ev->key == KON_KEY_ESCAPE && !kon_uiWantsKeyboard(state->ui)) state->dialog = DIALOG_NONE;
- if (down && ev->key == KON_KEY_F5 && state->project.open) browserGo(&state->side, state->side.path);
+ if (down && ev->key == KON_KEY_F5 && state->project.open) {
+ browserGo(&state->side, state->side.path);
+ scanPack(state);
+ }
}
return kon_uiWantsMouse(state->ui) || kon_uiWantsKeyboard(state->ui);
}
@@ -622,6 +947,8 @@ static void shutdown(kon_program_t *program, void *user)
(void)program;
browserFree(&state->side);
browserFree(&state->pick);
+ previewFree(&state->preview);
+ kon_fsFree(&state->packList);
kon_uiDestroy(state->ui);
}