From 197135e14ef2cbcde84102e3d28f7e7edb2e357f Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Wed, 17 May 2023 16:25:20 -0700 Subject: [PATCH] Rewrite parse code to use MD_String8, add test --- assets/new_level.json | 4 +- character_info.h | 2 +- main.c | 56 +++++++++-- makeprompt.h | 213 ++++++++++++++++++++++-------------------- todo.txt | 2 +- 5 files changed, 160 insertions(+), 117 deletions(-) diff --git a/assets/new_level.json b/assets/new_level.json index 7bf463d..f8d05cf 100644 --- a/assets/new_level.json +++ b/assets/new_level.json @@ -365,8 +365,8 @@ "rotation":0, "visible":true, "width":32, - "x":1526.33333333334, - "y":2427.33333333333 + "x":1954.33333333334, + "y":2263.33333333333 }, { "class":"", diff --git a/character_info.h b/character_info.h index ca75547..88c9ff3 100644 --- a/character_info.h +++ b/character_info.h @@ -15,7 +15,7 @@ const char *top_of_header = "" typedef struct { - const char *name; // the same as enum name + char *name; // the same as enum name bool takes_argument; } ActionInfo; diff --git a/main.c b/main.c index 64c0e07..cd6b57c 100644 --- a/main.c +++ b/main.c @@ -233,6 +233,35 @@ void do_metadesk_tests() Log("Testing passed!\n"); } +void do_parsing_tests() +{ + Log("Testing chatgpt parsing...\n"); + + MD_ArenaTemp scratch = MD_GetScratch(0, 0); + + Entity e = {0}; + e.npc_kind = NPC_TheBlacksmith; + e.exists = true; + Perception p = {0}; + MD_String8 error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &p); + assert(error.size > 0); + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \""), &p); + assert(error.size > 0); + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Cha \""), &p); + assert(error.size > 0); + + BUFF_APPEND(&e.held_items, ITEM_Chalice); + + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice \""), &p); + assert(error.size > 0); + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &p); + assert(error.size == 0); + assert(p.type == NPCDialog); + assert(p.npc_action_type == ACT_give_item); + assert(p.given_item == ITEM_Chalice); + + MD_ReleaseScratch(scratch); +} #endif typedef struct Overlap @@ -970,6 +999,7 @@ void init(void) #ifdef DEVTOOLS do_metadesk_tests(); + do_parsing_tests(); #endif frame_arena = MD_ArenaAlloc(); @@ -2527,7 +2557,6 @@ void frame(void) flush_quad_batch(); sg_end_pass(); sg_commit(); - reset(&scratch); } return; #endif @@ -3257,43 +3286,50 @@ void frame(void) #endif #ifdef DESKTOP + MD_ArenaTemp scratch = MD_GetScratch(0, 0); + const char *argument = 0; - BUFF(char, 512) dialog_string = {0}; + MD_String8List dialog_elems = {0}; Action act = ACT_none; it->times_talked_to++; if(it->remembered_perceptions.data[it->remembered_perceptions.cur_index-1].was_eavesdropped) { - printf_buff(&dialog_string, "Responding to eavesdropped: "); + MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Responding to eavesdropped: "); } if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED) { assert(it->times_talked_to == 1); act = ACT_joins_player; - printf_buff(&dialog_string, "Joining you..."); + MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Joining you..."); } else { - printf_buff(&dialog_string, "%d times talked", it->times_talked_to); + MD_S8ListPushFmt(scratch.arena, &dialog_elems, "%d times talked", it->times_talked_to); } - BUFF(char, 1024) mocked_ai_response = { 0 }; + MD_String8 mocked_ai_response = {0}; if(true) { + MD_StringJoin join = {0}; + MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join); if (argument) { - printf_buff(&mocked_ai_response, "ACT_%s(%s) \"%s\"", actions[act].name, argument, dialog_string.data); + mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s(%s) \"%.*s\"", actions[act].name, argument, MD_S8VArg(dialog)); } else { - printf_buff(&mocked_ai_response, "ACT_%s \"%s\"", actions[act].name, dialog_string.data); + mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s \"%.*s\"", actions[act].name, MD_S8VArg(dialog)); } } Perception p = { 0 }; - ChatgptParse parsed = parse_chatgpt_response(it, mocked_ai_response.data, &p); - assert(parsed.succeeded); + + MD_String8 error_message = parse_chatgpt_response(scratch.arena, it, mocked_ai_response, &p); + assert(error_message.size == 0); process_perception(it, p, player, &gs); + + MD_ReleaseScratch(scratch); #undef SAY #endif } diff --git a/makeprompt.h b/makeprompt.h index 171df8c..75ea5d6 100644 --- a/makeprompt.h +++ b/makeprompt.h @@ -956,150 +956,157 @@ bool char_in_str(char c, const char *str) return false; } -typedef struct -{ - bool succeeded; - Sentence error_message; -} ChatgptParse; -ChatgptParse parse_chatgpt_response(Entity *it, char *sentence_str, Perception *out) +// if returned string has size greater than 0, it's the error message. Allocated +// on arena passed into it +MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentence, Perception *out) { - ChatgptParse to_return = {0}; + MD_ArenaTemp scratch = MD_GetScratch(&arena, 1); + + MD_String8 error_message = {0}; *out = (Perception) { 0 }; out->type = NPCDialog; - size_t sentence_length = strlen(sentence_str); - - // dialog begins at ACT_ - const char *to_find = "ACT_"; - size_t to_find_len = strlen(to_find); - bool found = false; - while(true) + MD_String8 action_prefix = MD_S8Lit("ACT_"); + MD_u64 act_pos = MD_S8FindSubstring(sentence, action_prefix, 0, 0); + if(act_pos == sentence.size) { - if(*sentence_str == '\0') break; - if(strncmp(sentence_str, to_find, to_find_len) == 0) - { - sentence_str += to_find_len; - found = true; - break; - } - sentence_str += 1; + error_message = MD_S8Fmt(arena, "Couldn't find beginning of action '%.*s' in sentence", MD_S8VArg(action_prefix)); + goto endofparsing; } - if(!found) + MD_u64 beginning_of_action = act_pos + action_prefix.size; + + MD_u64 parenth = MD_S8FindSubstring(sentence, MD_S8Lit("("), 0, 0); + MD_u64 space = MD_S8FindSubstring(sentence, MD_S8Lit(" "), 0, 0); + + MD_u64 end_of_action = parenth < space ? parenth : space; + if(end_of_action == sentence.size) { - printf_buff(&to_return.error_message, "Couldn't find action beginning with 'ACT_'.\n"); - return to_return; + error_message = MD_S8Fmt(arena, "'%.*s' prefix doesn't end with a ' ' or a '(', like how 'ACT_none ' or 'ACT_give_item(ITEM_sandwich) does.", MD_S8VArg(action_prefix)); + goto endofparsing; } + MD_String8 given_action_string = MD_S8Substring(sentence, beginning_of_action, end_of_action); - SmallTextChunk action_string = { 0 }; - sentence_str += get_until(&action_string, sentence_str, "( "); - - bool found_action = false; AvailableActions available = { 0 }; - fill_available_actions(it, &available); + fill_available_actions(e, &available); + bool found_action = false; + MD_String8List given_action_strings = {0}; BUFF_ITER(Action, &available) { - if (strcmp(actions[*it].name, action_string.data) == 0) + MD_String8 action_str = MD_S8CString(actions[*it].name); + MD_S8ListPush(scratch.arena, &given_action_strings, action_str); + if(MD_S8Match(action_str, given_action_string, 0)) { found_action = true; out->npc_action_type = *it; } } - if (!found_action) + if(!found_action) { - printf_buff(&to_return.error_message, "Could not find action associated with parsed 'ACT_' string `%s`\n", action_string.data); - out->npc_action_type = ACT_none; - return to_return; + MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")}; + MD_String8 possible_actions_str = MD_S8ListJoin(scratch.arena, given_action_strings, &join); + error_message = MD_S8Fmt(arena, "Action string given is '%.*s', but available actions are: [%.*s]", MD_S8VArg(given_action_string), MD_S8VArg(possible_actions_str)); + goto endofparsing; } - else + + MD_u64 start_looking_for_quote = end_of_action; + + if(actions[out->npc_action_type].takes_argument) { - SmallTextChunk dialog_str = { 0 }; - if (actions[out->npc_action_type].takes_argument) + if(end_of_action >= sentence.size) + { + error_message = MD_S8Fmt(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but sentence ended prematurely", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->npc_action_type].name))); + goto endofparsing; + } + char should_be_paren = sentence.str[end_of_action]; + if(should_be_paren != '(') { -#define EXPECT(chr, val) if (chr != val) { printf_buff(&to_return.error_message, "Improperly formatted sentence, expected character '%c' but got '%c'\n", val, chr); return to_return; } + error_message = MD_S8Fmt(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but found character '%c'", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->npc_action_type].name)), should_be_paren); + goto endofparsing; + } + MD_u64 beginning_of_arg = end_of_action; + MD_u64 end_of_arg = MD_S8FindSubstring(sentence, MD_S8Lit(")"), beginning_of_arg, 0); + if(end_of_arg == sentence.size) + { + error_message = MD_S8Fmt(arena, "Expected ')' to close the action string's argument, but couldn't find one"); + goto endofparsing; + } - EXPECT(*sentence_str, '('); - sentence_str += 1; + MD_String8 argument = MD_S8Substring(sentence, beginning_of_arg, end_of_arg); + start_looking_for_quote = end_of_arg + 1; - SmallTextChunk argument = { 0 }; - sentence_str += get_until(&argument, sentence_str, ")"); + if(out->npc_action_type == ACT_give_item) + { + MD_String8 item_prefix = MD_S8Lit("ITEM_"); + MD_u64 item_prefix_begin = MD_S8FindSubstring(argument, item_prefix, 0, 0); + if(item_prefix_begin == argument.size) + { + error_message = MD_S8Fmt(arena, "Expected prefix 'ITEM_' before the give_item action, but found '%.*s' instead", MD_S8VArg(argument)); + goto endofparsing; + } + MD_u64 item_name_begin = item_prefix_begin + item_prefix.size; + MD_u64 item_name_end = argument.size; - if (out->npc_action_type == ACT_give_item) + MD_String8 item_name = MD_S8Substring(argument, item_name_begin, item_name_end); + + bool item_found = false; + MD_String8List possible_item_strings = {0}; + BUFF_ITER(ItemKind, &e->held_items) { - Entity *e = it; - bool found = false; - BUFF_ITER(ItemKind, &e->held_items) - { - const char *without_item_prefix = &argument.data[0]; - EXPECT(*without_item_prefix, 'I'); - without_item_prefix += 1; - EXPECT(*without_item_prefix, 'T'); - without_item_prefix += 1; - EXPECT(*without_item_prefix, 'E'); - without_item_prefix += 1; - EXPECT(*without_item_prefix, 'M'); - without_item_prefix += 1; - EXPECT(*without_item_prefix, '_'); - without_item_prefix += 1; - if (strcmp(items[*it].enum_name, without_item_prefix) == 0) - { - out->given_item = *it; - if (found) - { - Log("Duplicate item enum name? Really weird...\n"); - } - found = true; - } - } - if (!found) + MD_String8 item_str = MD_S8CString(items[*it].enum_name); + MD_S8ListPush(scratch.arena, &possible_item_strings, item_str); + if(MD_S8Match(item_str, item_name, 0)) { - printf_buff(&to_return.error_message, "Couldn't find item in the inventory of the NPC to give. You said `%s`, but you have [%s] in your inventory \n", argument.data, item_string(it).data); - return to_return; + item_found = true; + out->given_item = *it; } } - else + + if(!item_found) { - printf_buff(&to_return.error_message, "Don't know how to handle argument in action of type `%s`\n", actions[out->npc_action_type].name); -#ifdef DEVTOOLS - // not sure if this should never happen or not, need more sleep... - assert(false); -#endif - return to_return; + MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")}; + MD_String8 possible_items_str = MD_S8ListJoin(scratch.arena, possible_item_strings, &join); + error_message = MD_S8Fmt(arena, "Item string given is '%.*s', but available items to give are: [%.*s]", MD_S8VArg(item_name), MD_S8VArg(possible_items_str)); + goto endofparsing; } - EXPECT(*sentence_str, ')'); - sentence_str += 1; } - EXPECT(*sentence_str, ' '); - sentence_str += 1; - EXPECT(*sentence_str, '"'); - sentence_str += 1; - - sentence_str += get_until(&dialog_str, sentence_str, "\"\n"); - if (dialog_str.cur_index >= ARRLEN(out->npc_dialog.data)) + else { - printf_buff(&to_return.error_message, "Dialog string `%s` too big to fit in sentence size %d\n", dialog_str.data, - (int) ARRLEN(out->npc_dialog.data)); - return to_return; + assert(false); // if action takes an argument but we don't handle it, this should be a terrible crash } + } - char next_char = *(sentence_str + 1); - if(!(next_char == '\0' || next_char == '\n')) - { - printf_buff(&to_return.error_message, "Expected dialog to end after the last quote, but instead found character '%c'\n", next_char); - return to_return; - } + if(start_looking_for_quote >= sentence.size) + { + error_message = MD_S8Fmt(arena, "Wanted to start looking for quote for NPC speech, but sentence ended prematurely"); + goto endofparsing; + } - memcpy(out->npc_dialog.data, dialog_str.data, dialog_str.cur_index); - out->npc_dialog.cur_index = dialog_str.cur_index; + MD_u64 beginning_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), 0, 0); + MD_u64 end_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), beginning_of_speech + 1, 0); - to_return.succeeded = true; - return to_return; + if(beginning_of_speech == sentence.size || end_of_speech == sentence.size) + { + error_message = MD_S8Fmt(arena, "Expected dialog enclosed by two quotes (i.e \"My name is greg\") after the action, but couldn't find anything!"); + goto endofparsing; } - to_return.succeeded = false; - return to_return; + MD_String8 speech = MD_S8Substring(sentence, beginning_of_speech + 1, end_of_speech); + + if(speech.size >= ARRLEN(out->npc_dialog.data)) + { + error_message = MD_S8Fmt(arena, "The speech given is %llu bytes big, but the maximum allowed is %llu bytes.", speech.size, ARRLEN(out->npc_dialog.data)); + goto endofparsing; + } + + memcpy(out->npc_dialog.data, speech.str, speech.size); + out->npc_dialog.cur_index = (int)speech.size; + +endofparsing: + MD_ReleaseScratch(scratch); + return error_message; } diff --git a/todo.txt b/todo.txt index 2dc953a..2cf8eed 100644 --- a/todo.txt +++ b/todo.txt @@ -1,5 +1,5 @@ DONE - Refactor string parsing and chat dumping to use MD_String8 - - Delete old perception types and sources, like from an enemy or player item changed +DONE - Delete old perception types and sources, like from an enemy or player item changed - Refactor perceptions to come from multiple places and format correctly - Perceptions happen in physical space, propagate in physical space. - Add two more characters, and 4 more items. Make the drama in the town better