Rewrite parse code to use MD_String8, add test

main
Cameron Murphy Reikes 2 years ago
parent 380a422aa0
commit 197135e14e

@ -365,8 +365,8 @@
"rotation":0, "rotation":0,
"visible":true, "visible":true,
"width":32, "width":32,
"x":1526.33333333334, "x":1954.33333333334,
"y":2427.33333333333 "y":2263.33333333333
}, },
{ {
"class":"", "class":"",

@ -15,7 +15,7 @@ const char *top_of_header = ""
typedef struct typedef struct
{ {
const char *name; // the same as enum name char *name; // the same as enum name
bool takes_argument; bool takes_argument;
} ActionInfo; } ActionInfo;

@ -233,6 +233,35 @@ void do_metadesk_tests()
Log("Testing passed!\n"); 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 #endif
typedef struct Overlap typedef struct Overlap
@ -970,6 +999,7 @@ void init(void)
#ifdef DEVTOOLS #ifdef DEVTOOLS
do_metadesk_tests(); do_metadesk_tests();
do_parsing_tests();
#endif #endif
frame_arena = MD_ArenaAlloc(); frame_arena = MD_ArenaAlloc();
@ -2527,7 +2557,6 @@ void frame(void)
flush_quad_batch(); flush_quad_batch();
sg_end_pass(); sg_end_pass();
sg_commit(); sg_commit();
reset(&scratch);
} }
return; return;
#endif #endif
@ -3257,43 +3286,50 @@ void frame(void)
#endif #endif
#ifdef DESKTOP #ifdef DESKTOP
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
const char *argument = 0; const char *argument = 0;
BUFF(char, 512) dialog_string = {0}; MD_String8List dialog_elems = {0};
Action act = ACT_none; Action act = ACT_none;
it->times_talked_to++; it->times_talked_to++;
if(it->remembered_perceptions.data[it->remembered_perceptions.cur_index-1].was_eavesdropped) 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) if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED)
{ {
assert(it->times_talked_to == 1); assert(it->times_talked_to == 1);
act = ACT_joins_player; act = ACT_joins_player;
printf_buff(&dialog_string, "Joining you..."); MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Joining you...");
} }
else 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) if(true)
{ {
MD_StringJoin join = {0};
MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join);
if (argument) 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 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 }; 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); process_perception(it, p, player, &gs);
MD_ReleaseScratch(scratch);
#undef SAY #undef SAY
#endif #endif
} }

@ -956,150 +956,157 @@ bool char_in_str(char c, const char *str)
return false; 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 = (Perception) { 0 };
out->type = NPCDialog; out->type = NPCDialog;
size_t sentence_length = strlen(sentence_str); MD_String8 action_prefix = MD_S8Lit("ACT_");
MD_u64 act_pos = MD_S8FindSubstring(sentence, action_prefix, 0, 0);
// dialog begins at ACT_ if(act_pos == sentence.size)
const char *to_find = "ACT_";
size_t to_find_len = strlen(to_find);
bool found = false;
while(true)
{ {
if(*sentence_str == '\0') break; error_message = MD_S8Fmt(arena, "Couldn't find beginning of action '%.*s' in sentence", MD_S8VArg(action_prefix));
if(strncmp(sentence_str, to_find, to_find_len) == 0) goto endofparsing;
{
sentence_str += to_find_len;
found = true;
break;
}
sentence_str += 1;
} }
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"); 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));
return to_return; 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 }; 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) 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; found_action = true;
out->npc_action_type = *it; 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); MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")};
out->npc_action_type = ACT_none; MD_String8 possible_actions_str = MD_S8ListJoin(scratch.arena, given_action_strings, &join);
return to_return; 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(end_of_action >= sentence.size)
if (actions[out->npc_action_type].takes_argument) {
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, '('); MD_String8 argument = MD_S8Substring(sentence, beginning_of_arg, end_of_arg);
sentence_str += 1; start_looking_for_quote = end_of_arg + 1;
SmallTextChunk argument = { 0 }; if(out->npc_action_type == ACT_give_item)
sentence_str += get_until(&argument, sentence_str, ")"); {
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; MD_String8 item_str = MD_S8CString(items[*it].enum_name);
bool found = false; MD_S8ListPush(scratch.arena, &possible_item_strings, item_str);
BUFF_ITER(ItemKind, &e->held_items) if(MD_S8Match(item_str, item_name, 0))
{
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)
{ {
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); item_found = true;
return to_return; 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); MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")};
#ifdef DEVTOOLS MD_String8 possible_items_str = MD_S8ListJoin(scratch.arena, possible_item_strings, &join);
// not sure if this should never happen or not, need more sleep... 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));
assert(false); goto endofparsing;
#endif
return to_return;
} }
EXPECT(*sentence_str, ')');
sentence_str += 1;
} }
EXPECT(*sentence_str, ' '); else
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))
{ {
printf_buff(&to_return.error_message, "Dialog string `%s` too big to fit in sentence size %d\n", dialog_str.data, assert(false); // if action takes an argument but we don't handle it, this should be a terrible crash
(int) ARRLEN(out->npc_dialog.data));
return to_return;
} }
}
char next_char = *(sentence_str + 1); if(start_looking_for_quote >= sentence.size)
if(!(next_char == '\0' || next_char == '\n')) {
{ error_message = MD_S8Fmt(arena, "Wanted to start looking for quote for NPC speech, but sentence ended prematurely");
printf_buff(&to_return.error_message, "Expected dialog to end after the last quote, but instead found character '%c'\n", next_char); goto endofparsing;
return to_return; }
}
memcpy(out->npc_dialog.data, dialog_str.data, dialog_str.cur_index); MD_u64 beginning_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), 0, 0);
out->npc_dialog.cur_index = dialog_str.cur_index; MD_u64 end_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), beginning_of_speech + 1, 0);
to_return.succeeded = true; if(beginning_of_speech == sentence.size || end_of_speech == sentence.size)
return to_return; {
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; MD_String8 speech = MD_S8Substring(sentence, beginning_of_speech + 1, end_of_speech);
return to_return;
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;
} }

@ -1,5 +1,5 @@
DONE - Refactor string parsing and chat dumping to use MD_String8 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 - Refactor perceptions to come from multiple places and format correctly
- Perceptions happen in physical space, propagate in physical space. - Perceptions happen in physical space, propagate in physical space.
- Add two more characters, and 4 more items. Make the drama in the town better - Add two more characters, and 4 more items. Make the drama in the town better

Loading…
Cancel
Save