diff options
Diffstat (limited to 'game.c')
| -rw-r--r-- | game.c | 860 |
1 files changed, 813 insertions, 47 deletions
@@ -1,4 +1,8 @@ +#include <getopt.h> #include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> #define TB_IMPL #include "termbox2.h" @@ -6,6 +10,9 @@ #define NONSTD_IMPLEMENTATION #include "nonstd.h" +#include "llama.h" +#include "models.h" +#include "vectordb.h" #include "maps.h" #define MIN_W 40 @@ -61,12 +68,38 @@ typedef struct { char input[128]; int input_len; int npc_index; + const char *npc_name; DialogEntry entries[DIALOG_HISTORY_MAX]; int entry_count; } Dialog; +typedef struct { + const ModelConfig *model_cfg; + struct llama_model *model; + struct llama_model *embed_model; + struct llama_context *embed_ctx; + VectorDB *npc_dbs; + int *npc_db_loaded; + int verbose; +} GameRuntime; + +static void llama_log_callback(enum ggml_log_level level, const char *text, void *user_data) { + (void)level; + (void)user_data; + (void)text; +} + static int clamp(int value, int min, int max); +static void show_help(const char *prog) { + printf("Usage: %s [OPTIONS]\n", prog); + printf("Options:\n"); + printf(" -m, --model <name> Specify model to use (default: first model)\n"); + printf(" -e, --embed-model <name> Specify model to use for embeddings\n"); + printf(" -v, --verbose Enable verbose logging\n"); + printf(" -h, --help Show this help message\n"); +} + static void draw_border(int x, int y, int w, int h, uintattr_t fg) { int ix; int iy; @@ -87,7 +120,7 @@ static void draw_border(int x, int y, int w, int h, uintattr_t fg) { } static void draw_border_bg(int x, int y, int w, int h, uintattr_t fg, - uintattr_t bg) { + uintattr_t bg) { int ix; int iy; @@ -107,8 +140,8 @@ static void draw_border_bg(int x, int y, int w, int h, uintattr_t fg, } static void get_layout(int w, int h, int *map_x, int *map_y, int *map_w, - int *map_h, int *side_x, int *side_y, int *side_w, int *side_h, - int *msg1_y, int *msg2_y) { + int *map_h, int *side_x, int *side_y, int *side_w, int *side_h, + int *msg1_y, int *msg2_y) { *map_x = 0; *map_y = 0; *map_w = w - SIDEBAR_W; @@ -226,7 +259,7 @@ static void map_free(Map *map) { } static void update_camera(const Map *map, int view_w, int view_h, - const Player *player, int *cam_x, int *cam_y) { + const Player *player, int *cam_x, int *cam_y) { int max_cam_x; int max_cam_y; int margin_x; @@ -267,7 +300,7 @@ static void update_camera(const Map *map, int view_w, int view_h, } static void draw_map(const Map *map, int map_x, int map_y, int view_w, - int view_h, const Player *player, int cam_x, int cam_y) { + int view_h, const Player *player, int cam_x, int cam_y) { int ix; int iy; @@ -296,7 +329,7 @@ static void draw_map(const Map *map, int map_x, int map_y, int view_w, } if (player->x >= cam_x && player->x < cam_x + view_w && player->y >= cam_y - && player->y < cam_y + view_h) { + && player->y < cam_y + view_h) { int sx = map_x + (player->x - cam_x); int sy = map_y + (player->y - cam_y); tb_set_cell(sx, sy, '@', COLOR_GREEN_256 | TB_BOLD, TB_DEFAULT); @@ -324,7 +357,7 @@ static void draw_progress_bar(int x, int y, int w, int value, int max) { filled = (inner_w * value) / max; tb_set_cell(x, y, '[', COLOR_WHITE_256, TB_DEFAULT); for (ix = 0; ix < inner_w; ix++) { - uintattr_t fg = ix < filled ? COLOR_GREEN_256 : COLOR_WHITE_256; + uintattr_t fg = ix < filled ? COLOR_GREEN_256 : COLOR_WHITE_256; uint32_t ch = ix < filled ? '=' : ' '; tb_set_cell(x + 1 + ix, y, ch, fg, TB_DEFAULT); } @@ -389,31 +422,127 @@ static void update_status(const char *message) { status_msg = message ? message : ""; } -static void copy_truncated(char *dst, size_t dst_size, const char *src, int max_chars) { - int i = 0; - if (dst_size == 0) { - return; +static int draw_wrapped(int x, int y, int max_lines, int box_w, uintattr_t fg, + uintattr_t bg, const char *prefix, const char *text) { + if (max_lines <= 0 || box_w <= 0 || text == NULL) { + return 0; + } + int lines = 0; + int prefix_len = prefix ? (int)strlen(prefix) : 0; + if (prefix_len < 0) { + prefix_len = 0; + } + int avail = box_w - 4 - prefix_len; + if (avail < 1) { + return 0; + } + char pad[64]; + int pad_len = prefix_len < (int)sizeof(pad) - 1 ? prefix_len : (int)sizeof(pad) - 1; + for (int i = 0; i < pad_len; i++) { + pad[i] = ' '; + } + pad[pad_len] = '\0'; + const char *p = text; + while (*p != '\0' && lines < max_lines) { + while (*p == ' ') { + p++; + } + int line_len = 0; + int last_space = -1; + for (int i = 0; i < avail && p[i] != '\0'; i++) { + if (p[i] == '\n') { + line_len = i; + break; + } + if (p[i] == ' ') { + last_space = i; + } + line_len = i + 1; + } + if (line_len == 0) { + break; + } + int cut = line_len; + if (cut == avail && p[cut] != '\0' && last_space > 0) { + cut = last_space; + } + char buf[512]; + int copy_len = cut < (int)sizeof(buf) - 1 ? cut : (int)sizeof(buf) - 1; + memcpy(buf, p, (size_t)copy_len); + buf[copy_len] = '\0'; + while (copy_len > 0 && buf[copy_len - 1] == ' ') { + buf[copy_len - 1] = '\0'; + copy_len--; + } + const char *line_prefix = (lines == 0) ? (prefix ? prefix : "") : pad; + tb_printf(x, y + lines, fg, bg, "%s%s", line_prefix, buf); + lines++; + p += cut; + if (*p == '\n') { + p++; + } + } + return lines; +} + +static int count_wrapped_lines(int box_w, const char *prefix, const char *text) { + if (box_w <= 0 || text == NULL) { + return 0; + } + int prefix_len = prefix ? (int)strlen(prefix) : 0; + if (prefix_len < 0) { + prefix_len = 0; } - if (max_chars < 0) { - max_chars = 0; + int avail = box_w - 4 - prefix_len; + if (avail < 1) { + return 0; } - while (i < max_chars && src[i] != '\0' && i < (int)dst_size - 1) { - dst[i] = src[i]; - i++; + int lines = 0; + const char *p = text; + while (*p != '\0') { + while (*p == ' ') { + p++; + } + int line_len = 0; + int last_space = -1; + for (int i = 0; i < avail && p[i] != '\0'; i++) { + if (p[i] == '\n') { + line_len = i; + break; + } + if (p[i] == ' ') { + last_space = i; + } + line_len = i + 1; + } + if (line_len == 0) { + break; + } + int cut = line_len; + if (cut == avail && p[cut] != '\0' && last_space > 0) { + cut = last_space; + } + lines++; + p += cut; + if (*p == '\n') { + p++; + } } - dst[i] = '\0'; + return lines; } -static void dialog_open(Dialog *dialog, int npc_index) { +static void dialog_open(Dialog *dialog, int npc_index, const char *npc_name) { dialog->open = 1; dialog->input_len = 0; dialog->input[0] = '\0'; dialog->npc_index = npc_index; + dialog->npc_name = npc_name; } static void dialog_close(Dialog *dialog) { dialog->open = 0; dialog->npc_index = -1; + dialog->npc_name = NULL; } static void dialog_append(Dialog *dialog, uint32_t ch) { @@ -435,19 +564,455 @@ static void dialog_backspace(Dialog *dialog) { dialog->input[dialog->input_len] = '\0'; } -static void dialog_submit(Dialog *dialog, const GameMap *game_map) { +static void trim_leading(char **text) { + while (**text == ' ' || **text == '\t' || **text == '\n' || **text == '\r') { + (*text)++; + } +} + +static void trim_leading_punct(char **text) { + while (**text == '"' || **text == '\'' || **text == '`') { + (*text)++; + trim_leading(text); + } +} + +static void trim_trailing(char *text) { + size_t len = strlen(text); + while (len > 0) { + char ch = text[len - 1]; + if (ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') { + break; + } + text[len - 1] = '\0'; + len--; + } +} + +static void strip_any_prefix(char **text, const char *prefix) { + if (strncasecmp(*text, prefix, strlen(prefix)) == 0) { + *text += strlen(prefix); + trim_leading(text); + } +} + + +static char *sanitize_reply(char *reply, const char *name) { + if (reply == NULL) { + return NULL; + } + char *start = reply; + trim_leading(&start); + trim_leading_punct(&start); + strip_any_prefix(&start, "Answer:"); + strip_any_prefix(&start, "NPC:"); + strip_any_prefix(&start, "Context:"); + strip_any_prefix(&start, "System:"); + if (strncmp(start, "<context>", 9) == 0) { + start += 9; + trim_leading(&start); + } + char *reminder = strstr(start, "<system-reminder>"); + if (reminder) { + *reminder = '\0'; + } + char *system_tag = strstr(start, "<system"); + if (system_tag) { + *system_tag = '\0'; + } + char *tag = strstr(start, "<|"); + if (tag) { + *tag = '\0'; + } + char *eos = strstr(start, "</s>"); + if (eos) { + *eos = '\0'; + } + char *hash = strstr(start, "###"); + if (hash) { + *hash = '\0'; + } + if (name && name[0] != '\0') { + size_t name_len = strlen(name); + for (;;) { + if (strncasecmp(start, name, name_len) != 0) { + break; + } + start += name_len; + while (*start == ':' || *start == '-' || *start == ',') { + start++; + } + trim_leading(&start); + trim_leading_punct(&start); + } + } + if (start != reply) { + memmove(reply, start, strlen(start) + 1); + } + trim_trailing(reply); + return reply; +} + +static int find_substr_offset(const char *buf, int n, const char *needle) { + int needle_len = (int)strlen(needle); + if (needle_len <= 0 || n <= 0 || needle_len > n) { + return -1; + } + for (int i = 0; i + needle_len <= n; i++) { + int match = 1; + for (int j = 0; j < needle_len; j++) { + if (buf[i + j] != needle[j]) { + match = 0; + break; + } + } + if (match) { + return i; + } + } + return -1; +} + +static int find_stop_offset(const char *buf, int n) { + int stop_at = n; + for (int i = 0; i < n; i++) { + if (buf[i] == '\n') { + stop_at = i; + break; + } + } + int off = find_substr_offset(buf, n, "</s>"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + off = find_substr_offset(buf, n, "<system-reminder>"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + off = find_substr_offset(buf, n, "<system"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + off = find_substr_offset(buf, n, "<|"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + off = find_substr_offset(buf, n, "###"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + off = find_substr_offset(buf, n, "System:"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + off = find_substr_offset(buf, n, "User:"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + off = find_substr_offset(buf, n, "Assistant:"); + if (off >= 0 && off < stop_at) { + stop_at = off; + } + return stop_at; +} + +static void append_prompt_context(stringb *sb, const char *npc_name, const char *context, + const char *question) { + sb_append_cstr(sb, "Context:\n"); + if (npc_name && npc_name[0] != '\0') { + sb_append_cstr(sb, "NPC Name: "); + sb_append_cstr(sb, npc_name); + sb_append_cstr(sb, "\n"); + } + if (context && context[0] != '\0') { + sb_append_cstr(sb, context); + } + sb_append_cstr(sb, "\nQuestion:\n"); + sb_append_cstr(sb, question ? question : ""); +} + +static char *build_prompt(const ModelConfig *cfg, const char *system, const char *npc_name, + const char *context, const char *question) { + stringb full = {0}; + sb_init(&full, 0); + + switch (cfg->prompt_style) { + case PROMPT_STYLE_T5: + sb_append_cstr(&full, "instruction: "); + sb_append_cstr(&full, system ? system : ""); + sb_append_cstr(&full, "\nquestion: "); + sb_append_cstr(&full, question ? question : ""); + sb_append_cstr(&full, "\ncontext:\n"); + if (npc_name && npc_name[0] != '\0') { + sb_append_cstr(&full, "NPC Name: "); + sb_append_cstr(&full, npc_name); + sb_append_cstr(&full, "\n"); + } + if (context && context[0] != '\0') { + sb_append_cstr(&full, context); + } + sb_append_cstr(&full, "\nanswer:"); + break; + case PROMPT_STYLE_CHAT: + sb_append_cstr(&full, "System:\n"); + sb_append_cstr(&full, system ? system : ""); + sb_append_cstr(&full, "\nUser:\n"); + append_prompt_context(&full, npc_name, context, question); + sb_append_cstr(&full, "\nAssistant:"); + break; + case PROMPT_STYLE_PLAIN: + default: + sb_append_cstr(&full, "System:\n"); + sb_append_cstr(&full, system ? system : ""); + sb_append_cstr(&full, "\n"); + append_prompt_context(&full, npc_name, context, question); + sb_append_cstr(&full, "\nAnswer:"); + break; + } + + return full.data; +} + +static char *generate_npc_reply(const GameRuntime *runtime, const GameMap *game_map, + int npc_index, const char *prompt) { + if (runtime == NULL || prompt == NULL) { + return NULL; + } + const char *fallback = "Demo reply: The old ruins are north of here."; + const char *npc_name = NULL; + if (game_map && npc_index >= 0 && npc_index < 10) { + const char *npc_reply = game_map->npcs[npc_index].reply; + npc_name = game_map->npcs[npc_index].name; + if (npc_reply && npc_reply[0] != '\0') { + fallback = npc_reply; + } + } + + if (runtime->model == NULL || runtime->model_cfg == NULL || runtime->embed_ctx == NULL + || runtime->npc_dbs == NULL || runtime->npc_db_loaded == NULL) { + return strdup(fallback); + } + if (npc_index < 0 || npc_index >= 10 || runtime->npc_db_loaded[npc_index] == 0) { + return strdup(fallback); + } + + VectorDB *db = &runtime->npc_dbs[npc_index]; + float query[VDB_EMBED_SIZE]; + int results[5]; + for (int i = 0; i < 5; i++) { + results[i] = -1; + } + vdb_embed_query(db, prompt, query); + vdb_search(db, query, 5, results); + + size_t context_cap = 1024; + size_t context_len = 0; + char *context = (char *)malloc(context_cap); + if (context == NULL) { + return strdup(fallback); + } + context[0] = '\0'; + if (runtime->verbose) { + fprintf(stderr, "[npc] question: %s\n", prompt); + } + for (int i = 0; i < 5; i++) { + if (results[i] < 0) { + continue; + } + const char *text = db->docs[results[i]].text; + if (runtime->verbose) { + fprintf(stderr, "[npc] context[%d]: %s\n", i, text); + } + char header[32]; + int header_len = snprintf(header, sizeof(header), "Snippet %d:\n", i + 1); + size_t text_len = strlen(text); + size_t need = context_len + (size_t)header_len + text_len + 2; + if (need > context_cap) { + while (need > context_cap) { + context_cap *= 2; + } + char *next = (char *)realloc(context, context_cap); + if (next == NULL) { + free(context); + return strdup(fallback); + } + context = next; + } + if (header_len > 0) { + memcpy(context + context_len, header, (size_t)header_len); + context_len += (size_t)header_len; + } + memcpy(context + context_len, text, text_len); + context_len += text_len; + context[context_len++] = '\n'; + context[context_len] = '\0'; + } + + const char *system_prompt = "You are a helpful NPC. Speak in first person. " + "Use only the provided context. If the context does not contain the answer, say \"I don't know.\" " + "If asked your name, answer with the NPC Name from the context. " + "Do not mention context, system messages, or prompts. Reply with one short sentence."; + + char *full_prompt = build_prompt(runtime->model_cfg, system_prompt, npc_name, context, prompt); + if (full_prompt == NULL) { + free(context); + return strdup(fallback); + } + free(context); + + if (runtime->verbose) { + printf(">> %s\n", full_prompt); + } + + const struct llama_vocab *vocab = llama_model_get_vocab(runtime->model); + int n_prompt = -llama_tokenize(vocab, full_prompt, strlen(full_prompt), NULL, 0, true, true); + llama_token *prompt_tokens = (llama_token *)malloc((size_t)n_prompt * sizeof(llama_token)); + if (prompt_tokens == NULL) { + free(full_prompt); + return strdup(fallback); + } + if (llama_tokenize(vocab, full_prompt, strlen(full_prompt), prompt_tokens, n_prompt, true, true) < 0) { + free(full_prompt); + free(prompt_tokens); + return strdup(fallback); + } + + struct llama_context_params ctx_params = llama_context_default_params(); + ctx_params.n_ctx = runtime->model_cfg->n_ctx; + ctx_params.n_batch = runtime->model_cfg->n_batch; + ctx_params.embeddings = false; + + struct llama_context *ctx = llama_init_from_model(runtime->model, ctx_params); + if (ctx == NULL) { + free(full_prompt); + free(prompt_tokens); + return strdup(fallback); + } + + struct llama_sampler_chain_params sparams = llama_sampler_chain_default_params(); + struct llama_sampler *smpl = llama_sampler_chain_init(sparams); + if (runtime->model_cfg->top_k > 0) { + llama_sampler_chain_add(smpl, llama_sampler_init_top_k(runtime->model_cfg->top_k)); + } + if (runtime->model_cfg->top_p > 0.0f && runtime->model_cfg->top_p < 1.0f) { + llama_sampler_chain_add(smpl, llama_sampler_init_top_p(runtime->model_cfg->top_p, 1)); + } + if (runtime->model_cfg->min_p > 0.0f) { + llama_sampler_chain_add(smpl, llama_sampler_init_min_p(runtime->model_cfg->min_p, 1)); + } + llama_sampler_chain_add(smpl, llama_sampler_init_penalties( + runtime->model_cfg->repeat_last_n, + runtime->model_cfg->repeat_penalty, + runtime->model_cfg->freq_penalty, + runtime->model_cfg->presence_penalty)); + llama_sampler_chain_add(smpl, llama_sampler_init_temp(runtime->model_cfg->temperature)); + llama_sampler_chain_add(smpl, llama_sampler_init_dist(runtime->model_cfg->seed)); + + struct llama_batch batch = llama_batch_get_one(prompt_tokens, n_prompt); + + if (llama_model_has_encoder(runtime->model)) { + if (llama_encode(ctx, batch)) { + llama_sampler_free(smpl); + free(full_prompt); + free(prompt_tokens); + llama_free(ctx); + return strdup(fallback); + } + llama_token decoder_start = llama_model_decoder_start_token(runtime->model); + if (decoder_start == LLAMA_TOKEN_NULL) { + decoder_start = llama_vocab_bos(vocab); + } + batch = llama_batch_get_one(&decoder_start, 1); + } + + int n_pos = 0; + llama_token new_token_id; + size_t out_cap = 256; + size_t out_len = 0; + char *out = (char *)malloc(out_cap); + if (out == NULL) { + llama_sampler_free(smpl); + free(full_prompt); + free(prompt_tokens); + llama_free(ctx); + return strdup(fallback); + } + out[0] = '\0'; + int n_predict = runtime->model_cfg->n_predict > 0 ? runtime->model_cfg->n_predict : 64; + if (n_predict > 64) { + n_predict = 64; + } + while (n_pos + batch.n_tokens < n_prompt + n_predict) { + if (llama_decode(ctx, batch)) { + break; + } + n_pos += batch.n_tokens; + new_token_id = llama_sampler_sample(smpl, ctx, -1); + if (llama_vocab_is_eog(vocab, new_token_id)) { + break; + } + char buf[128]; + int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true); + if (n < 0) { + break; + } + int stop_at = find_stop_offset(buf, n); + if (out_len == 0 && stop_at == 0 && n > 0 && buf[0] == '\n') { + batch = llama_batch_get_one(&new_token_id, 1); + continue; + } + if (out_len + (size_t)stop_at + 1 > out_cap) { + while (out_len + (size_t)stop_at + 1 > out_cap) { + out_cap *= 2; + } + char *next = (char *)realloc(out, out_cap); + if (next == NULL) { + break; + } + out = next; + } + memcpy(out + out_len, buf, (size_t)stop_at); + out_len += (size_t)stop_at; + out[out_len] = '\0'; + if (stop_at != n) { + break; + } + batch = llama_batch_get_one(&new_token_id, 1); + } + + llama_sampler_free(smpl); + free(full_prompt); + free(prompt_tokens); + llama_free(ctx); + + if (out_len == 0) { + free(out); + return strdup(fallback); + } + return out; +} + +static void dialog_submit(Dialog *dialog, const GameMap *game_map, const GameRuntime *runtime) { if (dialog->input_len == 0) { return; } { - const char *demo = "Demo reply: The old ruins are north of here."; - const char *reply = demo; + const char *npc_name = NULL; + char *reply = generate_npc_reply(runtime, game_map, dialog->npc_index, dialog->input); + const char *fallback = ""; if (game_map && dialog->npc_index >= 0 && dialog->npc_index < 10) { - const char *npc_reply = game_map->npcs[dialog->npc_index].reply; - if (npc_reply && npc_reply[0] != '\0') { - reply = npc_reply; + npc_name = game_map->npcs[dialog->npc_index].name; + fallback = game_map->npcs[dialog->npc_index].reply; + if (fallback == NULL) { + fallback = ""; } } + reply = sanitize_reply(reply, npc_name); + if (reply == NULL || reply[0] == '\0') { + free(reply); + reply = NULL; + } + const char *reply_text = reply != NULL ? reply : fallback; if (dialog->entry_count >= DIALOG_HISTORY_MAX) { for (int i = 1; i < DIALOG_HISTORY_MAX; i++) { dialog->entries[i - 1] = dialog->entries[i]; @@ -457,8 +1022,9 @@ static void dialog_submit(Dialog *dialog, const GameMap *game_map) { snprintf(dialog->entries[dialog->entry_count].prompt, sizeof(dialog->entries[dialog->entry_count].prompt), "%s", dialog->input); snprintf(dialog->entries[dialog->entry_count].response, - sizeof(dialog->entries[dialog->entry_count].response), "%s", reply); + sizeof(dialog->entries[dialog->entry_count].response), "%s", reply_text); dialog->entry_count++; + free(reply); } dialog->input_len = 0; dialog->input[0] = '\0'; @@ -479,7 +1045,7 @@ static void update_npc_status(const GameMap *game_map, int npc_index) { } static void render(const Map *map, const Player *player, int *cam_x, - int *cam_y, int *out_view_w, int *out_view_h, const Dialog *dialog) { + int *cam_y, int *out_view_w, int *out_view_h, const Dialog *dialog) { int w; int h; int map_x; @@ -594,23 +1160,49 @@ static void render(const Map *map, const Player *player, int *cam_x, if (max_text < 0) { max_text = 0; } - int max_entries = max_lines / 2; - int start = dialog->entry_count - max_entries; + int start = dialog->entry_count; if (start < 0) { start = 0; } + int used_lines = 0; + for (int i = dialog->entry_count - 1; i >= 0; i--) { + const char *prompt_text = dialog->entries[i].prompt; + const char *response_text = dialog->entries[i].response; + const char *name = dialog->npc_name && dialog->npc_name[0] != '\0' ? dialog->npc_name : "NPC"; + char prefix_you[16]; + char prefix_npc[64]; + snprintf(prefix_you, sizeof(prefix_you), "You: "); + snprintf(prefix_npc, sizeof(prefix_npc), "%s: ", name); + int need = count_wrapped_lines(box_w, prefix_you, prompt_text) + + count_wrapped_lines(box_w, prefix_npc, response_text); + if (used_lines + need > max_lines && used_lines > 0) { + break; + } + used_lines += need; + start = i; + if (used_lines >= max_lines) { + break; + } + } for (int i = start; i < dialog->entry_count && line + 1 <= max_lines; i++) { - char prompt_buf[128]; - char response_buf[256]; - copy_truncated(prompt_buf, sizeof(prompt_buf), dialog->entries[i].prompt, max_text); - copy_truncated(response_buf, sizeof(response_buf), dialog->entries[i].response, max_text); - if (line < max_lines) { - tb_printf(box_x + 2, log_y + line, COLOR_WHITE_256, 19, "You: %s", prompt_buf); - line++; + const char *prompt_text = dialog->entries[i].prompt; + const char *response_text = dialog->entries[i].response; + const char *name = dialog->npc_name && dialog->npc_name[0] != '\0' ? dialog->npc_name : "NPC"; + char prefix_you[16]; + char prefix_npc[64]; + snprintf(prefix_you, sizeof(prefix_you), "You: "); + snprintf(prefix_npc, sizeof(prefix_npc), "%s: ", name); + int used = draw_wrapped(box_x + 2, log_y + line, max_lines - line, box_w, + COLOR_WHITE_256, 19, prefix_you, prompt_text); + line += used; + if (line >= max_lines) { + break; } - if (line < max_lines) { - tb_printf(box_x + 2, log_y + line, COLOR_GREEN_256, 19, "NPC: %s", response_buf); - line++; + used = draw_wrapped(box_x + 2, log_y + line, max_lines - line, box_w, + COLOR_GREEN_256, 19, prefix_npc, response_text); + line += used; + if (line >= max_lines) { + break; } } @@ -641,17 +1233,71 @@ static int clamp(int value, int min, int max) { return value; } -int main(void) { +int main(int argc, char **argv) { + const char *model_name = NULL; + const char *embed_model_name = NULL; + const ModelConfig *model_cfg = NULL; + struct llama_model *embed_model = NULL; + struct llama_model *gen_model = NULL; + struct llama_context *embed_ctx = NULL; + int tb_ready = 0; + int llama_ready = 0; + int exit_code = 0; + int verbose = 0; + + static struct option long_options[] = { + {"model", required_argument, 0, 'm'}, + {"embed-model", required_argument, 0, 'e'}, + {"verbose", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + int option_index = 0; + while ((opt = getopt_long(argc, argv, "m:e:vh", long_options, &option_index)) != -1) { + switch (opt) { + case 'm': + model_name = optarg; + break; + case 'e': + embed_model_name = optarg; + break; + case 'v': + verbose = 1; + break; + case 'h': + show_help(argv[0]); + return 0; + default: + fprintf(stderr, "Usage: %s [-m model] [-v] [-h]\n", argv[0]); + return 1; + } + } + + if (model_name != NULL) { + model_cfg = get_model_by_name(model_name); + if (model_cfg == NULL) { + fprintf(stderr, "Unknown model '%s'\n", model_name); + return 1; + } + } else { + model_cfg = &models[0]; + } + Player player = {0}; array(GameMap) maps; GameMap map1 = {0}; GameMap *current_map = NULL; + VectorDB *npc_dbs = NULL; + int *npc_db_loaded = NULL; int running = 1; int view_w = 0; int view_h = 0; int cam_x = 0; int cam_y = 0; Dialog dialog = {0}; + GameRuntime runtime = {0}; player_init(&player); array_init(maps); @@ -660,10 +1306,96 @@ int main(void) { current_map = &maps.data[0]; map_init(¤t_map->map, current_map->data, current_map->len); + if (verbose == 0) { + llama_log_set(llama_log_callback, NULL); + } + + npc_dbs = (VectorDB *)calloc(10, sizeof(VectorDB)); + npc_db_loaded = (int *)calloc(10, sizeof(int)); + if (npc_dbs == NULL || npc_db_loaded == NULL) { + fprintf(stderr, "Failed to allocate NPC vector databases\n"); + exit_code = 1; + goto cleanup; + } + + llama_backend_init(); + ggml_backend_load_all(); + llama_ready = 1; + const ModelConfig *embed_cfg = NULL; + if (embed_model_name != NULL) { + embed_cfg = get_model_by_name(embed_model_name); + if (embed_cfg == NULL) { + fprintf(stderr, "Unknown embedding model '%s'\n", embed_model_name); + exit_code = 1; + goto cleanup; + } + } else if (model_cfg->embed_model_name != NULL) { + embed_cfg = get_model_by_name(model_cfg->embed_model_name); + } + if (embed_cfg == NULL) { + embed_cfg = model_cfg; + } + + struct llama_model_params gen_params = llama_model_default_params(); + gen_params.n_gpu_layers = model_cfg->n_gpu_layers; + gen_params.use_mmap = model_cfg->use_mmap; + gen_model = llama_model_load_from_file(model_cfg->filepath, gen_params); + if (gen_model == NULL) { + fprintf(stderr, "Unable to load generation model\n"); + exit_code = 1; + goto cleanup; + } + + struct llama_model_params embed_params = llama_model_default_params(); + embed_params.n_gpu_layers = embed_cfg->n_gpu_layers; + embed_params.use_mmap = embed_cfg->use_mmap; + embed_model = llama_model_load_from_file(embed_cfg->filepath, embed_params); + if (embed_model == NULL) { + fprintf(stderr, "Unable to load embedding model\n"); + exit_code = 1; + goto cleanup; + } + + struct llama_context_params cparams = llama_context_default_params(); + cparams.n_ctx = embed_cfg->n_ctx; + cparams.n_batch = embed_cfg->n_batch; + cparams.embeddings = true; + embed_ctx = llama_init_from_model(embed_model, cparams); + if (embed_ctx == NULL) { + fprintf(stderr, "Failed to create embedding context\n"); + exit_code = 1; + goto cleanup; + } + + for (int i = 0; i < 10; i++) { + const char *vdb_path = current_map->npcs[i].vdb_path; + if (vdb_path == NULL || vdb_path[0] == '\0') { + continue; + } + vdb_init(&npc_dbs[i], embed_ctx); + VectorDBErrorCode vdb_rc = vdb_load(&npc_dbs[i], vdb_path); + if (vdb_rc != VDB_SUCCESS) { + fprintf(stderr, "Failed to load vector database %s: %s\n", vdb_path, vdb_error(vdb_rc)); + vdb_free(&npc_dbs[i]); + continue; + } + npc_db_loaded[i] = 1; + } + + runtime.model_cfg = model_cfg; + runtime.model = gen_model; + runtime.embed_model = embed_model; + runtime.embed_ctx = embed_ctx; + runtime.npc_dbs = npc_dbs; + runtime.npc_db_loaded = npc_db_loaded; + runtime.verbose = verbose; + if (tb_init() != TB_OK) { fprintf(stderr, "Failed to init termbox.\n"); - return 1; + exit_code = 1; + goto cleanup; } + tb_ready = 1; tb_set_input_mode(TB_INPUT_ESC); tb_set_output_mode(TB_OUTPUT_256); @@ -678,7 +1410,7 @@ int main(void) { if (ev.key == TB_KEY_ESC) { dialog_close(&dialog); } else if (ev.key == TB_KEY_ENTER) { - dialog_submit(&dialog, current_map); + dialog_submit(&dialog, current_map, &runtime); } else if (ev.key == TB_KEY_BACKSPACE || ev.key == TB_KEY_BACKSPACE2) { dialog_backspace(&dialog); } else if (ev.ch) { @@ -692,7 +1424,10 @@ int main(void) { u32 target = map_get(¤t_map->map, player.x, next_y); int npc_index = npc_index_from_tile(target); if (target == 'N' || npc_index >= 0) { - dialog_open(&dialog, npc_index); + const char *npc_name = current_map && npc_index >= 0 && npc_index < 10 + ? current_map->npcs[npc_index].name + : NULL; + dialog_open(&dialog, npc_index, npc_name); update_npc_status(current_map, npc_index); } else if (map_is_walkable(¤t_map->map, player.x, next_y)) { player.y = next_y; @@ -702,7 +1437,10 @@ int main(void) { u32 target = map_get(¤t_map->map, player.x, next_y); int npc_index = npc_index_from_tile(target); if (target == 'N' || npc_index >= 0) { - dialog_open(&dialog, npc_index); + const char *npc_name = current_map && npc_index >= 0 && npc_index < 10 + ? current_map->npcs[npc_index].name + : NULL; + dialog_open(&dialog, npc_index, npc_name); update_npc_status(current_map, npc_index); } else if (map_is_walkable(¤t_map->map, player.x, next_y)) { player.y = next_y; @@ -712,7 +1450,10 @@ int main(void) { u32 target = map_get(¤t_map->map, next_x, player.y); int npc_index = npc_index_from_tile(target); if (target == 'N' || npc_index >= 0) { - dialog_open(&dialog, npc_index); + const char *npc_name = current_map && npc_index >= 0 && npc_index < 10 + ? current_map->npcs[npc_index].name + : NULL; + dialog_open(&dialog, npc_index, npc_name); update_npc_status(current_map, npc_index); } else if (map_is_walkable(¤t_map->map, next_x, player.y)) { player.x = next_x; @@ -722,7 +1463,10 @@ int main(void) { u32 target = map_get(¤t_map->map, next_x, player.y); int npc_index = npc_index_from_tile(target); if (target == 'N' || npc_index >= 0) { - dialog_open(&dialog, npc_index); + const char *npc_name = current_map && npc_index >= 0 && npc_index < 10 + ? current_map->npcs[npc_index].name + : NULL; + dialog_open(&dialog, npc_index, npc_name); update_npc_status(current_map, npc_index); } else if (map_is_walkable(¤t_map->map, next_x, player.y)) { player.x = next_x; @@ -742,11 +1486,33 @@ int main(void) { } } +cleanup: player_free(&player); for (size_t i = 0; i < maps.length; i++) { map_free(&maps.data[i].map); } array_free(maps); - tb_shutdown(); - return 0; + if (tb_ready) { + tb_shutdown(); + } + for (int i = 0; i < 10; i++) { + if (npc_db_loaded && npc_db_loaded[i]) { + vdb_free(&npc_dbs[i]); + } + } + free(npc_db_loaded); + free(npc_dbs); + if (embed_ctx != NULL) { + llama_free(embed_ctx); + } + if (embed_model != NULL) { + llama_model_free(embed_model); + } + if (gen_model != NULL) { + llama_model_free(gen_model); + } + if (llama_ready) { + llama_backend_free(); + } + return exit_code; } |
