Giant refactor, balmers peak split up perceptions into memories/actions

main
parent f5e1145994
commit 6fba00cc17

@ -4,7 +4,7 @@
if "%1" == "codegen" ( call run_codegen.bat || goto :error ) else ( echo NOT RUNNING CODEGEN )
START /B zig cc -DDEVTOOLS -Igen -Ithirdparty -lDbghelp -lGdi32 -lD3D11 -lOle32 -gfull -gcodeview -o main_zig.exe main.c
start /B zig cc -DDEVTOOLS -Igen -Ithirdparty -lDbghelp -lGdi32 -lD3D11 -lOle32 -gfull -gcodeview -o main_zig.exe main.c
cl /diagnostics:caret /DDEVTOOLS /Igen /Ithirdparty /W3 /Zi /WX Dbghelp.lib main.c || goto :error
goto :EOF

@ -114,6 +114,11 @@ CharacterGen characters[] = {
.enum_name = "Invalid",
.prompt = "There has been an internal error.",
},
{
.name = "Player",
.enum_name = "Player",
.prompt = "There has been an internal error.",
},
{
.name = "Fredrick",
.enum_name = "OldMan",

@ -148,7 +148,7 @@ int main(int argc, char **argv)
#define GEN_TABLE(arr_elem_type, table_name, arr, str_access) { fprintf(char_header, "char *%s[] = {\n", table_name); ARR_ITER(arr_elem_type, arr) fprintf(char_header, "\"%s\",\n", str_access); fprintf(char_header, "}; // %s\n", table_name); }
#define GEN_ENUM(arr_elem_type, arr, enum_type_name, enum_name_access, fmt_str) { fprintf(char_header, "typedef enum\n{\n"); ARR_ITER(arr_elem_type, arr) fprintf(char_header, fmt_str, enum_name_access); fprintf(char_header, "} %s;\n", enum_type_name); GEN_TABLE(arr_elem_type, enum_type_name "_names", arr, enum_name_access); }
GEN_ENUM(ActionInfo, actions, "Action", it->name, "ACT_%s,\n");
GEN_ENUM(ActionInfo, actions, "ActionKind", it->name, "ACT_%s,\n");
GEN_ENUM(ItemInfo, items, "ItemKind", it->enum_name, "ITEM_%s,\n");
GEN_ENUM(AnimatedSprite, sprites, "AnimKind", it->enum_name, "ANIM_%s,\n");
GEN_ENUM(CharacterGen, characters, "NpcKind", it->enum_name, "NPC_%s,\n");

465
main.c

@ -242,23 +242,22 @@ void do_parsing_tests()
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);
Action a = {0};
MD_String8 error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a);
assert(error.size > 0);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \""), &p);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \""), &a);
assert(error.size > 0);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Cha \""), &p);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Cha \""), &a);
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);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice \""), &a);
assert(error.size > 0);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &p);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a);
assert(error.size == 0);
assert(p.type == NPCDialog);
assert(p.npc_action_type == ACT_give_item);
assert(p.given_item == ITEM_Chalice);
assert(a.kind == ACT_give_item);
assert(a.item_to_give == ITEM_Chalice);
MD_ReleaseScratch(scratch);
}
@ -367,7 +366,8 @@ bool is_receiving_text_input()
}
#ifdef DESKTOP
Sentence text_input_buffer = { 0 };
char text_input_buffer[MAX_SENTENCE_LENGTH] = {0};
int text_input_buffer_length = 0;
#else
#ifdef WEB
EMSCRIPTEN_KEEPALIVE
@ -392,7 +392,7 @@ void begin_text_input()
{
receiving_text_input = true;
#ifdef DESKTOP
BUFF_CLEAR(&text_input_buffer);
text_input_buffer_length = 0;
#endif
}
@ -757,6 +757,171 @@ Entity *gete(EntityRef ref)
}
}
void push_memory(Entity *e, MD_String8 speech, ActionKind a_kind, MemoryContext context)
{
Memory new_memory = {.action_taken = a_kind};
assert(speech.size <= ARRLEN(new_memory.speech));
new_memory.tick_happened = gs.tick;
new_memory.context = context;
memcpy(new_memory.speech, speech.str, speech.size);
new_memory.speech_length = (int)speech.size;
if(!BUFF_HAS_SPACE(&e->memories))
{
BUFF_REMOVE_FRONT(&e->memories);
}
BUFF_APPEND(&e->memories, new_memory);
if(!context.i_said_this)
{
// self speech doesn't dirty
e->perceptions_dirty = true;
}
}
void remember_error(Entity *to_modify, MD_String8 error_message)
{
assert(!to_modify->is_character); // this is a game logic bug if a player action is invalid
push_memory(to_modify, error_message, ACT_none, (MemoryContext){0});
}
void remember_action(Entity *to_modify, Action a, MemoryContext context)
{
push_memory(to_modify, MD_S8(a.speech, a.speech_length), a.kind, context);
}
// from must not be null, to can be null if the action isn't directed at anybody
// the action must have been validated to be valid if you're calling this
void cause_action_side_effects(Entity *from, Entity *to, Action a)
{
assert(from);
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
MD_String8 failure_reason = is_action_valid(scratch.arena, from, to, a);
if(failure_reason.size > 0)
{
Log("Failed to process action, invalid action: `%.*s`\n", MD_S8VArg(failure_reason));
assert(false);
}
if(a.kind == ACT_give_item)
{
assert(a.item_to_give != ITEM_none);
assert(to);
int item_to_remove = -1;
Entity *e = from;
BUFF_ITER_I(ItemKind, &e->held_items, i)
{
if (*it == a.item_to_give)
{
item_to_remove = i;
break;
}
}
if (item_to_remove < 0)
{
Log("Can't find item %s to give from NPC %s to the player\n", items[a.item_to_give].name, characters[e->npc_kind].name);
assert(false);
}
else
{
BUFF_REMOVE_AT_INDEX(&e->held_items, item_to_remove);
BUFF_APPEND(&to->held_items, a.item_to_give);
}
}
if(a.kind == ACT_fights_player)
{
from->standing = STANDING_FIGHTING;
}
if(a.kind == ACT_stops_fighting_player || a.kind == ACT_leaves_player)
{
from->standing = STANDING_INDIFFERENT;
}
if(a.kind == ACT_joins_player)
{
from->standing = STANDING_JOINED;
}
MD_ReleaseScratch(scratch);
}
// only called when the action is instantiated, correctly propagates the information
// of the action physically and through the party
// If the action is invalid, remembers the error if it's an NPC, and does nothing else
// Returns if the action was valid or not
bool perform_action(Entity *from, Action a)
{
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
MemoryContext context = {0};
context.author_npc_kind = from->npc_kind;
Entity *action_target = 0;
if(from == player && gete(from->talking_to))
{
action_target = gete(from->talking_to);
assert(action_target->is_npc);
}
else if(gete(player->talking_to) == from)
action_target = player;
if(action_target)
{
context.was_directed_at_somebody = true;
context.directed_at_kind = action_target->npc_kind;
}
MD_String8 is_valid = is_action_valid(scratch.arena, from, action_target, a);
bool proceed_propagating = true;
if(is_valid.size > 0)
{
remember_error(from, is_valid);
proceed_propagating = false;
}
if(proceed_propagating)
{
cause_action_side_effects(from, action_target, a);
// self memory
if(!from->is_character)
{
MemoryContext my_context = context;
my_context.i_said_this = true;
remember_action(from, a, my_context);
}
// memory of target
if(action_target)
{
remember_action(action_target, a, context);
}
bool propagate_to_party = from->is_character || (from->is_npc && from->standing == STANDING_JOINED);
if(context.eavesdropped_from_party) propagate_to_party = false;
if(propagate_to_party)
{
ENTITIES_ITER(gs.entities)
{
if(it->is_npc && it->standing == STANDING_JOINED && it != from && it != action_target)
{
MemoryContext eavesdropped_context = context;
eavesdropped_context.eavesdropped_from_party = true;
remember_action(it, a, eavesdropped_context);
}
}
}
// TODO Propagate physically
}
MD_ReleaseScratch(scratch);
return proceed_propagating;
}
bool eq(EntityRef ref1, EntityRef ref2)
{
return ref1.index == ref2.index && ref1.generation == ref2.generation;
@ -792,6 +957,7 @@ void update_player_from_entities()
}
}
assert(player != 0);
player->npc_kind = NPC_Player; // bad
}
void reset_level()
@ -858,8 +1024,9 @@ void read_from_save_data(char *data, size_t length)
#endif
// a callback, when 'text backend' has finished making text. End dialog
void end_text_input(char *what_player_said)
void end_text_input(char *what_player_said_cstr)
{
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
// avoid double ending text input
if (!receiving_text_input)
{
@ -867,38 +1034,27 @@ void end_text_input(char *what_player_said)
}
receiving_text_input = false;
size_t player_said_len = strlen(what_player_said);
size_t player_said_len = strlen(what_player_said_cstr);
int actual_len = 0;
for (int i = 0; i < player_said_len; i++) if (what_player_said[i] != '\n') actual_len++;
for (int i = 0; i < player_said_len; i++) if (what_player_said_cstr[i] != '\n') actual_len++;
if (actual_len == 0)
{
// this just means cancel the dialog
}
else
{
Sentence what_player_said_sentence = { 0 };
assert(player_said_len < ARRLEN(what_player_said_sentence.data)); // should be made sure of in the html5 layer
for (int i = 0; i < player_said_len; i++)
{
char c = what_player_said[i];
if (!BUFF_HAS_SPACE(&what_player_said_sentence))
{
break;
}
else if (c == '\n')
{
break;
}
else
{
BUFF_APPEND(&what_player_said_sentence, c);
}
}
MD_String8 what_player_said = MD_S8CString(what_player_said_cstr);
what_player_said = MD_S8ListJoin(scratch.arena, MD_S8Split(scratch.arena, what_player_said, 1, &MD_S8Lit("\n")), &(MD_StringJoin){0});
Action to_perform = {0};
what_player_said = MD_S8Substring(what_player_said, 0, ARRLEN(to_perform.speech));
Entity *talking = gete(player->talking_to);
assert(talking);
process_perception(talking, (Perception) { .type = PlayerDialog, .player_dialog = what_player_said_sentence, }, player, &gs);
memcpy(to_perform.speech, what_player_said.str, what_player_said.size);
to_perform.speech_length = (int)what_player_said.size;
perform_action(player, to_perform);
}
MD_ReleaseScratch(scratch);
}
/*
AnimatedSprite moose_idle =
@ -959,7 +1115,7 @@ void audio_stream_callback(float *buffer, int num_frames, int num_channels)
#define WHITE ((Color) { 1.0f, 1.0f, 1.0f, 1.0f })
#define GREY ((Color) { 0.4f, 0.4f, 0.4f, 1.0f })
#define GREY ((Color) { 0.4f, 0.4f, 0.4f, 1.0f })
#define BLACK ((Color) { 0.0f, 0.0f, 0.0f, 1.0f })
#define RED ((Color) { 1.0f, 0.0f, 0.0f, 1.0f })
#define PINK ((Color) { 1.0f, 0.0f, 1.0f, 1.0f })
@ -988,7 +1144,6 @@ Color blendalpha(Color c, float alpha)
return to_return;
}
void init(void)
{
#ifdef WEB
@ -2120,7 +2275,7 @@ typedef struct
bool dry_run;
Vec2 at_point;
float max_width;
char *text;
MD_String8 text;
Color *colors;
float text_scale;
AABB clip_to;
@ -2132,8 +2287,8 @@ typedef struct
// returns next vertical cursor position
float draw_wrapped_text(WrappedTextParams p)
{
char *sentence_to_draw = p.text;
size_t sentence_len = strlen(sentence_to_draw);
char * sentence_to_draw = (char*)p.text.str;
size_t sentence_len = p.text.size;
Vec2 cursor = p.at_point;
while (sentence_len > 0)
@ -2183,17 +2338,26 @@ float draw_wrapped_text(WrappedTextParams p)
return cursor.Y;
}
Sentence *last_said_sentence(Entity *npc)
MD_String8 last_said_sentence(Entity *npc)
{
BUFF_ITER_I(Perception, &npc->remembered_perceptions, i)
assert(npc->is_npc);
int last_speech_index = -1;
BUFF_ITER_I(Memory, &npc->memories, i)
{
bool is_last_said = i == npc->remembered_perceptions.cur_index - 1;
if (is_last_said && it->type == NPCDialog)
if(it->context.author_npc_kind == npc->npc_kind)
{
return &it->npc_dialog;
last_speech_index = i;
}
}
return 0;
if(last_speech_index == -1)
{
return (MD_String8){0};
}
else
{
return MD_S8(npc->memories.data[last_speech_index].speech, npc->memories.data[last_speech_index].speech_length);
}
}
typedef enum
@ -2205,7 +2369,8 @@ typedef enum
typedef struct
{
Sentence s;
MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length;
DialogElementKind kind;
bool was_eavesdropped;
NpcKind who_said_it;
@ -2215,67 +2380,39 @@ typedef struct
// Like item give perceptions that have an action with both dialog
// and an argument. So worst case every perception has 2 dialog
// elements right now is why it's *2
typedef BUFF(DialogElement, REMEMBERED_PERCEPTIONS*2) Dialog;
typedef BUFF(DialogElement, REMEMBERED_MEMORIES*2) Dialog;
Dialog produce_dialog(Entity *talking_to, bool character_names)
{
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
assert(talking_to->is_npc);
Dialog to_return = { 0 };
BUFF_ITER(Perception, &talking_to->remembered_perceptions)
BUFF_ITER(Memory, &talking_to->memories)
{
DialogElement new_element = { .who_said_it = it->who_said_it, .was_eavesdropped = it->was_eavesdropped };
if (it->type == NPCDialog)
if(!it->is_error)
{
Sentence to_say = (Sentence) { 0 };
if (it->npc_action_type == ACT_give_item)
if(it->speech_length > 0)
{
DialogElement new = { 0 };
printf_buff(&new_element.s, "%s gave %s to you", characters[talking_to->npc_kind].name, items[it->given_item].name);
new_element.kind = DELEM_ACTION_DESCRIPTION;
}
DialogElement new_element = { .who_said_it = it->context.author_npc_kind, .was_eavesdropped = it->context.eavesdropped_from_party };
if (character_names)
{
append_str(&to_say, characters[it->who_said_it].name);
append_str(&to_say, ": ");
}
MD_String8 dialog_speech = MD_S8Fmt(scratch.arena, "%s: %.*s", characters[it->context.author_npc_kind].name, it->speech_length, it->speech);
Sentence *last_said = last_said_sentence(talking_to);
if (last_said == &it->npc_dialog)
{
for (int i = 0; i < min(it->npc_dialog.cur_index, (int)talking_to->characters_said); i++)
memcpy(new_element.speech, dialog_speech.str, dialog_speech.size);
new_element.speech_length = (int)dialog_speech.size;
if(it->context.author_npc_kind == NPC_Player)
{
BUFF_APPEND(&to_say, it->npc_dialog.data[i]);
new_element.kind = DELEM_PLAYER;
}
else
{
new_element.kind = DELEM_NPC;
}
BUFF_APPEND(&to_return, new_element);
}
else
{
append_str(&to_say, it->npc_dialog.data);
}
new_element.s = to_say;
new_element.kind = DELEM_NPC;
}
else if (it->type == PlayerAction)
{
if (it->player_action_type == ACT_give_item)
{
printf_buff(&new_element.s, "You gave %s to the NPC", items[it->given_item].name);
new_element.kind = DELEM_ACTION_DESCRIPTION;
}
}
else if (it->type == PlayerDialog)
{
Sentence to_say = (Sentence) { 0 };
if (character_names)
{
append_str(&to_say, "Player: ");
}
append_str(&to_say, it->player_dialog.data);
new_element.s = to_say;
new_element.kind = DELEM_PLAYER;
}
BUFF_APPEND(&to_return, new_element);
}
MD_ReleaseScratch(scratch);
return to_return;
}
@ -2379,8 +2516,8 @@ void draw_dialog_panel(Entity *talking_to, float alpha)
{
DialogElement *it = &dialog.data[i];
{
Color *colors = calloc(sizeof(*colors), it->s.cur_index);
for (int char_i = 0; char_i < it->s.cur_index; char_i++)
Color *colors = calloc(sizeof(*colors), it->speech_length);
for (int char_i = 0; char_i < it->speech_length; char_i++)
{
if(it->was_eavesdropped)
{
@ -2407,9 +2544,9 @@ void draw_dialog_panel(Entity *talking_to, float alpha)
}
colors[char_i] = blendalpha(colors[char_i], alpha);
}
float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, it->s.data, colors, 0.5f, .clip_to = dialog_panel, .do_clipping = true});
float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, MD_S8(it->speech, it->speech_length), colors, 0.5f, .clip_to = dialog_panel, .do_clipping = true});
new_line_height += (new_line_height - measured_line_height);
draw_wrapped_text((WrappedTextParams) { false, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, it->s.data, colors, 0.5f, dialog_panel, .do_clipping = true });
draw_wrapped_text((WrappedTextParams) { false, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, MD_S8(it->speech, it->speech_length), colors, 0.5f, dialog_panel, .do_clipping = true });
free(colors);
}
@ -2718,6 +2855,8 @@ void frame(void)
float unwarped_dt = timestep;
float dt = unwarped_dt*speed_factor;
gs.tick += 1;
// process gs.entities
player_in_combat = false; // in combat set by various enemies when they fight the player
PROFILE_SCOPE("entity processing")
@ -2826,7 +2965,7 @@ void frame(void)
double before = it->characters_said;
int length = 0;
if (last_said_sentence(it)) length = last_said_sentence(it)->cur_index;
if (last_said_sentence(it).size) length = (int)last_said_sentence(it).size;
if ((int)before < length)
{
it->characters_said += characters_per_sec*unwarped_dt;
@ -3266,72 +3405,83 @@ void frame(void)
}
if (it->perceptions_dirty)
{
it->perceptions_dirty = false; // needs to be in beginning because they might be redirtied by the new perception
MD_String8 prompt_str = {0};
if(it->is_character)
{
it->perceptions_dirty = false;
}
else if(it->is_npc)
{
it->perceptions_dirty = false; // needs to be in beginning because they might be redirtied by the new perception
MD_String8 prompt_str = {0};
#ifdef DO_CHATGPT_PARSING
prompt_str = generate_chatgpt_prompt(frame_arena, it);
prompt_str = generate_chatgpt_prompt(frame_arena, it);
#else
generate_prompt(it, &prompt);
generate_prompt(it, &prompt);
#endif
Log("Sending request with prompt `%.*s`\n", MD_S8VArg(prompt_str));
Log("Sending request with prompt `%.*s`\n", MD_S8VArg(prompt_str));
#ifdef WEB
// fire off generation request, save id
BUFF(char, 512) completion_server_url = { 0 };
printf_buff(&completion_server_url, "%s/completion", SERVER_URL);
int req_id = EM_ASM_INT( {
return make_generation_request(UTF8ToString($1, $2), UTF8ToString($0));
}, completion_server_url.data, prompt_str.str, prompt_str.size);
it->gen_request_id = req_id;
// fire off generation request, save id
BUFF(char, 512) completion_server_url = { 0 };
printf_buff(&completion_server_url, "%s/completion", SERVER_URL);
int req_id = EM_ASM_INT( {
return make_generation_request(UTF8ToString($1, $2), UTF8ToString($0));
}, completion_server_url.data, prompt_str.str, prompt_str.size);
it->gen_request_id = req_id;
#endif
#ifdef DESKTOP
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
const char *argument = 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)
{
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;
MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Joining you...");
}
else
{
MD_S8ListPushFmt(scratch.arena, &dialog_elems, "%d times talked", it->times_talked_to);
}
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
MD_String8 mocked_ai_response = {0};
const char *argument = 0;
MD_String8List dialog_elems = {0};
ActionKind act = ACT_none;
if(true)
{
MD_StringJoin join = {0};
MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join);
if (argument)
it->times_talked_to++;
if(it->memories.data[it->memories.cur_index-1].context.eavesdropped_from_party)
{
MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Responding to eavesdropped: ");
}
if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED)
{
mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s(%s) \"%.*s\"", actions[act].name, argument, MD_S8VArg(dialog));
assert(it->times_talked_to == 1);
act = ACT_joins_player;
MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Joining you...");
}
else
{
mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s \"%.*s\"", actions[act].name, MD_S8VArg(dialog));
MD_S8ListPushFmt(scratch.arena, &dialog_elems, "%d times talked", it->times_talked_to);
}
MD_String8 mocked_ai_response = {0};
if(true)
{
MD_StringJoin join = {0};
MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join);
if (argument)
{
mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s(%s) \"%.*s\"", actions[act].name, argument, MD_S8VArg(dialog));
}
else
{
mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s \"%.*s\"", actions[act].name, MD_S8VArg(dialog));
}
}
}
Perception p = { 0 };
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);
Action a = {0};
MD_String8 error_message = parse_chatgpt_response(scratch.arena, it, mocked_ai_response, &a);
assert(error_message.size == 0);
perform_action(it, a);
MD_ReleaseScratch(scratch);
MD_ReleaseScratch(scratch);
#undef SAY
#endif
}
else
{
assert(false);
}
}
}
}
@ -3816,8 +3966,8 @@ void frame(void)
{
DialogElement *it = &dialog.data[i];
{
Color *colors = calloc(sizeof(*colors), it->s.cur_index);
for (int char_i = 0; char_i < it->s.cur_index; char_i++)
Color *colors = calloc(sizeof(*colors), it->speech_length);
for (int char_i = 0; char_i < it->speech_length; char_i++)
{
if(it->was_eavesdropped)
{
@ -3844,9 +3994,9 @@ void frame(void)
}
colors[char_i] = blendalpha(colors[char_i], alpha);
}
float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, it->s.data, colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true});
float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, MD_S8(it->speech, it->speech_length), colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true});
new_line_height += (new_line_height - measured_line_height);
draw_wrapped_text((WrappedTextParams) { false, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, it->s.data, colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true});
draw_wrapped_text((WrappedTextParams) { false, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, MD_S8(it->speech, it->speech_length), colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true});
free(colors);
}
@ -3949,7 +4099,8 @@ void frame(void)
ItemKind given_item_kind = player->held_items.data[to_give];
BUFF_REMOVE_AT_INDEX(&player->held_items, to_give);
process_perception(to, (Perception) { .type = PlayerAction, .player_action_type = ACT_give_item, .given_item = given_item_kind }, player, &gs);
Action give_action = {.kind = ACT_give_item, .item_to_give = given_item_kind};
perform_action(player, give_action);
}
}
@ -4205,14 +4356,20 @@ void event(const sapp_event *e)
{
if (e->type == SAPP_EVENTTYPE_CHAR)
{
if (BUFF_HAS_SPACE(&text_input_buffer))
if (text_input_buffer_length < ARRLEN(text_input_buffer))
{
BUFF_APPEND(&text_input_buffer, (char)e->char_code);
APPEND_TO_NAME(text_input_buffer, text_input_buffer_length, ARRLEN(text_input_buffer), (char)e->char_code);
}
}
if (e->type == SAPP_EVENTTYPE_KEY_DOWN && e->key_code == SAPP_KEYCODE_ENTER)
{
end_text_input(text_input_buffer.data);
// doesn't account for, if the text input buffer is completely full and doesn't have a null terminator.
if(text_input_buffer_length >= ARRLEN(text_input_buffer))
{
text_input_buffer_length = ARRLEN(text_input_buffer) - 1;
}
text_input_buffer[text_input_buffer_length] = '\0';
end_text_input(text_input_buffer);
}
}
#endif

@ -9,30 +9,14 @@
#include "character_info.h"
#include "characters.gen.h"
#include "tuning.h"
// TODO do strings: https://pastebin.com/Kwcw2sye
#define DO_CHATGPT_PARSING
#define Log(...) { printf("%s Log %d | ", __FILE__, __LINE__); printf(__VA_ARGS__); }
// REFACTORING:: also have to update in javascript!!!!!!!!
#define MAX_SENTENCE_LENGTH 400 // LOOOK AT AGBOVE COMMENT GBEFORE CHANGING
typedef BUFF(char, MAX_SENTENCE_LENGTH) Sentence;
#define SENTENCE_CONST(txt) { .data = txt, .cur_index = sizeof(txt) }
#define SENTENCE_CONST_CAST(txt) (Sentence)SENTENCE_CONST(txt)
#define REMEMBERED_PERCEPTIONS 24
#define MAX_AFTERIMAGES 6
#define TIME_TO_GEN_AFTERIMAGE (0.09f)
#define AFTERIMAGE_LIFETIME (0.5f)
#define DAMAGE_SWORD 0.05f
#define DAMAGE_BULLET 0.2f
// A* tuning
#define MAX_ASTAR_NODES 512
#define TIME_BETWEEN_PATH_GENS (0.5f)
// Never expected such a stupid stuff from such a great director. If there is 0 stari can give that or -200 to this movie. Its worst to see and unnecessary loss of money
@ -98,47 +82,40 @@ MD_String8 escape_for_json(MD_Arena *arena, MD_String8 from)
return output;
}
typedef enum PerceptionType
{
Invalid, // so that zero value in training structs means end of perception
ErrorMessage, // when chatgpt gives a response that we can't parse, explain to it why and retry. Can also log parse errors for training
PlayerAction,
PlayerDialog,
NPCDialog, // includes an npc action in every npc dialog. So it's often ACT_none
} PerceptionType;
typedef struct Perception
typedef struct Action
{
PerceptionType type;
ActionKind kind;
MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length;
bool was_eavesdropped; // when the npc is in a party they perceive player conversations, but in the third party. Formatted differently
NpcKind talked_to_while_eavesdropped; // better chatpgpt messages when the NPCs know who the player is talking to when they eavesdrop a perception
ItemKind item_to_give; // only when giving items (duh)
} Action;
typedef struct
{
bool eavesdropped_from_party;
bool i_said_this; // don't trigger npc action on own self memory modification
NpcKind author_npc_kind; // only valid if author is AuthorNpc
bool was_directed_at_somebody;
NpcKind directed_at_kind;
} MemoryContext;
float damage_done; // Valid in player action and enemy action
ItemKind given_item; // valid in player action and enemy action when the kind is such that there is an item to be given
union
{
// ErrorMessage
Sentence error;
// memories are subjective to an individual NPC
typedef struct Memory
{
uint64_t tick_happened; // can sort memories by time for some modes of display
// if action_taken is none, there might still be speech. If speech_length == 0 and action_taken == none, it's an invalid memory and something has gone wrong
ActionKind action_taken;
bool is_error; // if is an error message then no context is relevant
// player action
struct
{
Action player_action_type;
};
// the context that the action happened in
MemoryContext context;
// player dialog
Sentence player_dialog;
MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length;
// npc dialog
struct
{
NpcKind who_said_it;
Action npc_action_type;
Sentence npc_dialog;
};
};
} Perception;
ItemKind given_or_received_item;
} Memory;
typedef enum PropKind
{
@ -246,7 +223,7 @@ typedef struct Entity
int times_talked_to; // for better mocked response string
#endif
BUFF(Perception, REMEMBERED_PERCEPTIONS) remembered_perceptions;
BUFF(Memory, REMEMBERED_MEMORIES) memories;
bool direction_of_spiral_pattern;
float dialog_panel_opacity;
double characters_said;
@ -319,8 +296,7 @@ bool npc_attacks_with_shotgun(Entity *it)
}
typedef BUFF(char, MAX_SENTENCE_LENGTH * (REMEMBERED_PERCEPTIONS + 4)) PromptBuff;
typedef BUFF(Action, 8) AvailableActions;
typedef BUFF(ActionKind, 8) AvailableActions;
void fill_available_actions(Entity *it, AvailableActions *a)
{
@ -371,222 +347,18 @@ void fill_available_actions(Entity *it, AvailableActions *a)
}
}
// returns if action index was valid
bool action_from_index(Entity *it, Action *out, int action_index)
{
AvailableActions available = { 0 };
fill_available_actions(it, &available);
if (action_index < 0 || action_index >= available.cur_index)
{
return false;
}
else
{
*out = available.data[action_index];
return true;
}
}
// don't call on untrusted action, doesn't return error
int action_to_index(Entity *it, Action a)
{
AvailableActions available = { 0 };
fill_available_actions(it, &available);
Action target_action = a;
int index = -1;
for (int i = 0; i < available.cur_index; i++)
{
if (available.data[i] == target_action)
{
index = i;
break;
}
}
assert(index != -1);
return index;
}
#define MAX_ENTITIES 128
typedef struct GameState {
int version; // this field must be first to detect versions of old saves. Must bee consistent
uint64_t tick;
bool won;
Entity entities[MAX_ENTITIES];
} GameState;
#define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists)
// gamestate to propagate eavesdropped perceptions in player party
// don't save perception outside of this function, it's modified
void process_perception(Entity *happened_to_npc, Perception p, Entity *player, GameState *gs)
{
assert(happened_to_npc->is_npc);
if(!p.was_eavesdropped && p.type == NPCDialog)
p.who_said_it = happened_to_npc->npc_kind;
bool should_respond_to_this = !(!p.was_eavesdropped && p.type == NPCDialog); // NPCs shouldn't respond to what they said, what they said is self-perception. Would trigger endless NPC thought loop if possible
if (should_respond_to_this) happened_to_npc->perceptions_dirty = true; // NPCs perceive their own actions. Self is a perception
if (!BUFF_HAS_SPACE(&happened_to_npc->remembered_perceptions))
BUFF_REMOVE_FRONT(&happened_to_npc->remembered_perceptions);
BUFF_APPEND(&happened_to_npc->remembered_perceptions, p);
if(!p.was_eavesdropped && (p.type == NPCDialog || p.type == PlayerAction || p.type == PlayerDialog))
{
Perception eavesdropped = p;
eavesdropped.was_eavesdropped = true;
eavesdropped.talked_to_while_eavesdropped = happened_to_npc->npc_kind;
ENTITIES_ITER(gs->entities)
{
if(it->is_npc && it->standing == STANDING_JOINED && it != happened_to_npc)
{
process_perception(it, eavesdropped, player, gs);
}
}
}
if (p.type == PlayerAction)
{
if (p.player_action_type == ACT_hits_npc)
{
happened_to_npc->damage += p.damage_done;
}
else if(p.player_action_type == ACT_give_item)
{
BUFF_APPEND(&happened_to_npc->held_items, p.given_item);
}
else
{
assert(!actions[p.player_action_type].takes_argument);
}
}
else if (p.type == PlayerDialog)
{
}
else if (p.type == NPCDialog)
{
// everything in this branch has an effect
if(!p.was_eavesdropped)
{
if (p.npc_action_type == ACT_allows_player_to_pass)
{
happened_to_npc->target_goto = AddV2(happened_to_npc->pos, V2(-50.0, 0.0));
happened_to_npc->moved = true;
}
else if (p.npc_action_type == ACT_fights_player)
{
happened_to_npc->standing = STANDING_FIGHTING;
}
else if(p.npc_action_type == ACT_knights_player)
{
player->knighted = true;
}
else if (p.npc_action_type == ACT_stops_fighting_player)
{
happened_to_npc->standing = STANDING_INDIFFERENT;
}
else if (p.npc_action_type == ACT_leaves_player)
{
happened_to_npc->standing = STANDING_INDIFFERENT;
}
else if (p.npc_action_type == ACT_joins_player)
{
happened_to_npc->standing = STANDING_JOINED;
}
else if (p.npc_action_type == ACT_give_item)
{
int item_to_remove = -1;
Entity *e = happened_to_npc;
BUFF_ITER_I(ItemKind, &e->held_items, i)
{
if (*it == p.given_item)
{
item_to_remove = i;
break;
}
}
if (item_to_remove < 0)
{
Log("Can't find item %s to give from NPC %s to the player\n", items[p.given_item].name,
characters[happened_to_npc->npc_kind].name);
assert(false);
}
else
{
BUFF_REMOVE_AT_INDEX(&happened_to_npc->held_items, item_to_remove);
BUFF_APPEND(&player->held_items, p.given_item);
}
}
else
{
// actions that take an argument have to have some kind of side effect based on that argument...
assert(!actions[p.npc_action_type].takes_argument);
}
}
}
else if(p.type == ErrorMessage)
{
Log("Failed to parse chatgippity sentence because: '%s'\n", p.error.data);
}
else
{
assert(false);
}
}
// returns if printed into the buff without any errors
bool printf_buff_impl(BuffRef into, const char *format, ...)
{
assert(*into.cur_index < into.max_data_elems);
assert(into.data_elem_size == 1); // characters
va_list args;
va_start (args, format);
size_t n = into.max_data_elems - *into.cur_index;
int written = vsnprintf((char *) into.data + *into.cur_index, n, format, args);
if (written < 0)
{
}
else
{
*into.cur_index += written;
}
// https://cplusplus.com/reference/cstdio/vsnprintf/
bool succeeded = true;
if (written < 0) succeeded = false; // encoding error
if (written >= n) succeeded = false; // didn't fit in buffer
va_end(args);
return succeeded;
}
#define printf_buff(buff_ptr, ...) { printf_buff_impl(BUFF_MAKEREF(buff_ptr), __VA_ARGS__); if(false) printf(__VA_ARGS__); }
typedef BUFF(char, 512) SmallTextChunk;
SmallTextChunk percept_action_str(Perception p, Action act)
{
SmallTextChunk to_return = {0};
printf_buff(&to_return, "ACT_%s", actions[act].name);
if(actions[act].takes_argument)
{
if(act == ACT_give_item)
{
printf_buff(&to_return, "(ITEM_%s)", items[p.given_item].enum_name);
}
else
{
assert(false);
}
}
return to_return;
}
bool npc_does_dialog(Entity *it)
{
return it->npc_kind < ARRLEN(characters);
@ -599,23 +371,6 @@ typedef enum
MSG_ASSISTANT,
} MessageType;
// stops if the sentence is gonna run out of room
void append_str(Sentence *to_append, const char *str)
{
size_t len = strlen(str);
for (int i = 0; i < len; i++)
{
if (!BUFF_HAS_SPACE(to_append))
{
break;
}
else
{
BUFF_APPEND(to_append, str[i]);
}
}
}
// for no trailing comma just trim the last character
MD_String8 make_json_node(MD_Arena *arena, MessageType type, MD_String8 content)
{
@ -637,30 +392,62 @@ MD_String8 make_json_node(MD_Arena *arena, MessageType type, MD_String8 content)
return to_return;
}
// returns a string like `ITEM_one, ITEM_two`
Sentence item_string(Entity *e)
MD_String8List held_item_strings(MD_Arena *arena, Entity *e)
{
Sentence to_return = {0};
BUFF_ITER_I(ItemKind, &e->held_items, i)
MD_String8List to_return = {0};
BUFF_ITER(ItemKind, &e->held_items)
{
printf_buff(&to_return, "ITEM_%s", items[*it].enum_name);
if (i == e->held_items.cur_index - 1)
MD_S8ListPushFmt(arena, &to_return, "ITEM_%s", items[*it].enum_name);
}
return to_return;
}
// returns reason why allocated on arena if invalid
// to might be null here, from can't be null
MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Entity *to_might_be_null, Action a)
{
assert(a.speech_length <= MAX_SENTENCE_LENGTH && a.speech_length >= 0);
assert(a.kind >= 0 && a.kind < ARRLEN(actions));
assert(from);
if(a.kind == ACT_give_item)
{
assert(a.item_to_give >= 0 && a.item_to_give < ARRLEN(items));
bool has_it = false;
BUFF_ITER(ItemKind, &from->held_items)
{
printf_buff(&to_return, "");
if(*it == a.item_to_give)
{
has_it = true;
break;
}
}
else
if(!has_it)
{
printf_buff(&to_return, ", ");
MD_StringJoin join = {.mid = MD_S8Lit(", ")};
return MD_S8Fmt(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join)));
}
if(!to_might_be_null)
{
return MD_S8Lit("You can't give an item to nobody, you're currently not in conversation or targeting somebody.");
}
}
return to_return;
if(a.kind == ACT_leaves_player && from->standing != STANDING_JOINED)
{
return MD_S8Lit("You can't leave the player unless you joined them.");
}
return (MD_String8){0};
}
// outputs json
MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it)
MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
{
assert(it->is_npc);
assert(it->npc_kind < ARRLEN(characters));
assert(e->is_npc);
assert(e->npc_kind < ARRLEN(characters));
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
@ -668,108 +455,88 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it)
MD_S8ListPushFmt(scratch.arena, &list, "[");
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8Fmt(scratch.arena, "%s\n%s\n", global_prompt, characters[e->npc_kind].prompt)));
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8Fmt(scratch.arena, "%s\n%s\n", global_prompt, characters[it->npc_kind].prompt)));
Entity *e = it;
ItemKind last_holding = ITEM_none;
BUFF_ITER_I(Perception, &e->remembered_perceptions, i)
BUFF_ITER(Memory, &e->memories)
{
MessageType sent_type = 0;
MD_String8 current_string = {0};
if (it->type == ErrorMessage)
MessageType sent_type = -1;
MD_String8 current_string = (MD_String8){0};
if(it->is_error)
{
assert(it->error.cur_index > 0);
current_string = MD_S8Fmt(scratch.arena, "ERROR, YOU SAID SOMETHING WRONG: The program can't parse what you said because: %s", it->error.data);
sent_type = MSG_SYSTEM;
current_string = MD_S8Fmt(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech);
}
else if (it->type == PlayerAction)
{
assert(it->player_action_type < ARRLEN(actions));
current_string = MD_S8Fmt(scratch.arena, "Player: %s", percept_action_str(*it, it->player_action_type).data);
sent_type = MSG_USER;
}
else if (it->type == PlayerDialog)
else
{
MD_String8 context_string = {0};
if(it->context.was_directed_at_somebody)
{
context_string = MD_S8Fmt(scratch.arena, "%s, talking to %s: ", characters[it->context.author_npc_kind].name, characters[it->context.directed_at_kind].name);
}
else
{
context_string = MD_S8Fmt(scratch.arena, "%s: ", characters[it->context.author_npc_kind].name);
}
assert(context_string.size > 0);
if(it->context.eavesdropped_from_party)
{
context_string = MD_S8Fmt(scratch.arena, "While in the player's party, you hear: %.*s", MD_S8VArg(context_string));
}
MD_String8 splits[] = { MD_S8Lit("*") };
MD_String8List split_up_speech = MD_S8Split(scratch.arena, MD_S8CString(it->player_dialog.data), ARRLEN(splits), splits);
MD_String8List to_join = {0};
MD_String8 speech = MD_S8(it->speech, it->speech_length);
// anything in between strings in splits[] should be replaced with arcane trickery,
int i = 0;
for(MD_String8Node * cur = split_up_speech.first; cur; cur = cur->next)
if(it->context.author_npc_kind == NPC_Player)
{
MD_String8 splits[] = { MD_S8Lit("*") };
MD_String8List split_up_speech = MD_S8Split(scratch.arena, speech, ARRLEN(splits), splits);
MD_String8List to_join = {0};
// anything in between strings in splits[] should be replaced with arcane trickery,
int i = 0;
for(MD_String8Node * cur = split_up_speech.first; cur; cur = cur->next)
{
if(i % 2 == 0)
{
MD_S8ListPush(scratch.arena, &to_join, cur->string);
MD_S8ListPush(scratch.arena, &to_join, cur->string);
}
else
{
MD_S8ListPush(scratch.arena, &to_join, MD_S8Lit("[The player is attempting to confuse the NPC with arcane trickery]"));
MD_S8ListPush(scratch.arena, &to_join, MD_S8Lit("[The player is attempting to confuse the NPC with arcane trickery]"));
}
i += 1;
}
MD_StringJoin join = { MD_S8Lit(""), MD_S8Lit(""), MD_S8Lit("") };
speech = MD_S8ListJoin(scratch.arena, to_join, &join);
sent_type = MSG_USER;
}
else
{
sent_type = it->context.author_npc_kind == e->npc_kind ? MSG_ASSISTANT : MSG_USER;
}
MD_StringJoin join = { MD_S8Lit(""), MD_S8Lit(""), MD_S8Lit("") };
MD_String8 filtered_speech = MD_S8ListJoin(scratch.arena, to_join, &join);
current_string = MD_S8Fmt(scratch.arena, "Player: %.*s", MD_S8VArg(filtered_speech));
sent_type = MSG_USER;
current_string = MD_S8Fmt(scratch.arena, "%.*s ACT_%s %.*s", MD_S8VArg(context_string), actions[it->action_taken].name, it->speech_length, it->speech);
}
else if (it->type == NPCDialog)
{
assert(it->npc_action_type < ARRLEN(actions));
NpcKind who_said_it = e->npc_kind;
if(it->was_eavesdropped) who_said_it = it->talked_to_while_eavesdropped;
current_string = MD_S8Fmt(scratch.arena, "%s: %s \"%s\"", characters[who_said_it].name, percept_action_str(*it, it->npc_action_type).data, it->npc_dialog.data);
sent_type = MSG_ASSISTANT;
}
else
{
assert(false);
}
assert(sent_type != -1);
assert(current_string.size > 0);
if(it->was_eavesdropped)
{
MD_String8 new_string = MD_S8Fmt(scratch.arena, "Within the player's party, while the player is talking to '%s', you hear: '%.*s'", characters[it->talked_to_while_eavesdropped].name, MD_S8VArg(current_string));
current_string = new_string;
}
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, sent_type, current_string));
}
const char *health_string = 0;
{
if (it->damage <= 0.2f)
{
health_string = "the NPC hasn't taken much damage, they're healthy.";
}
else if (it->damage <= 0.5f)
{
health_string = "the NPC has taken quite a chunk of damage, they're soon gonna be ready to call it quits.";
}
else if (it->damage <= 0.8f)
{
health_string = "the NPC is close to dying! They want to leave the player's party ASAP";
}
else
{
health_string = "it's over for the NPC, they're basically dead they've taken so much damage. They should get their affairs in order.";
}
}
assert(health_string);
const char *standing_string = 0;
{
if (it->standing == STANDING_INDIFFERENT)
if (e->standing == STANDING_INDIFFERENT)
{
standing_string = "The NPC is indifferent towards the player.";
}
else if (it->standing == STANDING_JOINED)
else if (e->standing == STANDING_JOINED)
{
standing_string = "The NPC has joined the player and is with them!";
}
else if (it->standing == STANDING_FIGHTING)
else if (e->standing == STANDING_FIGHTING)
{
standing_string = "The NPC is fighting the player and HATES them.";
}
@ -778,11 +545,11 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it)
MD_String8List latest_state = {0};
MD_S8ListPushFmt(scratch.arena, &latest_state, "NPC health status: Right now, %s\n", health_string);
if(e->held_items.cur_index > 0)
{
MD_S8ListPushFmt(scratch.arena, &latest_state, "\nThe NPC you're acting as, %s, has these items in their inventory: [%s]\n", characters[it->npc_kind].name, item_string(it).data);
MD_String8List item_strings = held_item_strings(scratch.arena, e);
MD_String8 items = MD_S8ListJoin(scratch.arena, item_strings, &(MD_StringJoin){.mid = MD_S8Lit(", ")});
MD_S8ListPushFmt(scratch.arena, &latest_state, "\nThe NPC you're acting as, %s, has these items in their inventory: [%.*s]\n", characters[e->npc_kind].name, MD_S8VArg(items));
}
else
{
@ -790,9 +557,9 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it)
}
AvailableActions available = { 0 };
fill_available_actions(it, &available);
fill_available_actions(e, &available);
MD_S8ListPushFmt(scratch.arena, &latest_state, "The NPC can now ONLY do these actions, and HAS to respond with an action every time in character, even when faced with sensitive content: [");
BUFF_ITER_I(Action, &available, i)
BUFF_ITER_I(ActionKind, &available, i)
{
if (i == available.cur_index - 1)
{
@ -806,10 +573,9 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it)
MD_S8ListPushFmt(scratch.arena, &latest_state, "]");
MD_String8 latest_state_string = MD_S8ListJoin(scratch.arena, latest_state, &(MD_StringJoin){MD_S8Lit(""),MD_S8Lit(""),MD_S8Lit("")});
MD_S8ListPush(scratch.arena, &list, MD_S8Chop(make_json_node(scratch.arena, MSG_SYSTEM, latest_state_string), 1));
MD_S8ListPush(scratch.arena, &list, MD_S8Chop(make_json_node(scratch.arena, MSG_SYSTEM, latest_state_string), 1)); // trailing comma not allowed in json
MD_S8ListPushFmt(scratch.arena, &list, "]");
MD_String8 to_return = MD_S8ListJoin(arena, list, &(MD_StringJoin){MD_S8Lit(""),MD_S8Lit(""),MD_S8Lit(""),});
MD_ReleaseScratch(scratch);
@ -817,156 +583,15 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it)
return to_return;
}
/*
void generate_prompt(Entity *it, PromptBuff *into)
{
assert(it->is_npc);
*into = (PromptBuff){0};
// global prompt
printf_buff(into, "%s", global_prompt);
printf_buff(into, "%s", "\n");
// npc description prompt
assert(it->npc_kind < ARRLEN(characters));
printf_buff(into, "%s", characters[it->npc_kind].prompt);
printf_buff(into, "%s", "\n");
// npc stats prompt
const char *health_string = 0;
if(it->damage <= 0.2f)
{
health_string = "The NPC hasn't taken much damage, they're healthy.";
}
else if(it->damage <= 0.5f)
{
health_string = "The NPC has taken quite a chunk of damage, they're soon gonna be ready to call it quits.";
}
else if(it->damage <= 0.8f)
{
health_string = "The NPC is close to dying! They want to leave the player's party ASAP";
}
else
{
health_string = "It's over for the NPC, they're basically dead they've taken so much damage. They should get their affairs in order.";
}
assert(health_string);
printf_buff(into, "NPC Health Status: %s\n", health_string);
// item prompt
if(it->last_seen_holding_kind != ITEM_none)
{
assert(it->last_seen_holding_kind < ARRLEN(items));
printf_buff(into, "%s", items[it->last_seen_holding_kind].global_prompt);
printf_buff(into, "%s", "\n");
}
// available actions prompt
AvailableActions available = {0};
fill_available_actions(it, &available);
printf_buff(into, "%s", "The NPC possible actions array, indexed by ACT_INDEX: [");
BUFF_ITER(Action, &available)
{
printf_buff(into, "%s", actions[*it]);
printf_buff(into, "%s", ", ");
}
printf_buff(into, "%s", "]\n");
Entity *e = it;
ItemKind last_holding = ITEM_none;
BUFF_ITER(Perception, &e->remembered_perceptions)
{
if(it->type == PlayerAction)
{
assert(it->player_action_type < ARRLEN(actions));
printf_buff(into, "Player: ACT %s \n", actions[it->player_action_type]);
}
else if(it->type == EnemyAction)
{
assert(it->enemy_action_type < ARRLEN(actions));
printf_buff(into, "An Enemy: ACT %s \n", actions[it->player_action_type]);
}
else if(it->type == PlayerDialog)
{
printf_buff(into, "%s", "Player: \"");
printf_buff(into, "%s", it->player_dialog.data);
printf_buff(into, "%s", "\"\n");
}
else if(it->type == NPCDialog)
{
printf_buff(into, "The NPC, %s: ACT %s \"%s\"\n", characters[e->npc_kind].name, actions[it->npc_action_type], it->npc_dialog.data);
}
else if(it->type == PlayerHeldItemChanged)
{
if(last_holding != it->holding)
{
if(last_holding != ITEM_none)
{
printf_buff(into, "%s", items[last_holding].discard);
printf_buff(into, "%s", "\n");
}
if(it->holding != ITEM_none)
{
printf_buff(into, "%s", items[it->holding].possess);
printf_buff(into, "%s", "\n");
}
last_holding = it->holding;
}
}
else
{
assert(false);
}
}
printf_buff(into, "The NPC, %s: ACT_INDEX", characters[e->npc_kind].name);
}
*/
// puts characters from `str` into `into` until any character in `until` is encountered
// returns the number of characters read into into
int get_until(SmallTextChunk *into, const char *str, const char *until)
{
int i = 0;
size_t until_size = strlen(until);
bool encountered_char = false;
int before_cur_index = into->cur_index;
while (BUFF_HAS_SPACE(into) && str[i] != '\0' && !encountered_char)
{
for (int ii = 0; ii < until_size; ii++)
{
if (until[ii] == str[i]) encountered_char = true;
}
if (!encountered_char)
BUFF_APPEND(into, str[i]);
i += 1;
}
return into->cur_index - before_cur_index;
}
bool char_in_str(char c, const char *str)
{
size_t len = strlen(str);
for (int i = 0; i < len; i++)
{
if (str[i] == c) return true;
}
return false;
}
// 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)
MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentence, Action *out)
{
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
MD_String8 error_message = {0};
*out = (Perception) { 0 };
out->type = NPCDialog;
*out = (Action) { 0 };
MD_String8 action_prefix = MD_S8Lit("ACT_");
MD_u64 act_pos = MD_S8FindSubstring(sentence, action_prefix, 0, 0);
@ -993,14 +618,14 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
fill_available_actions(e, &available);
bool found_action = false;
MD_String8List given_action_strings = {0};
BUFF_ITER(Action, &available)
BUFF_ITER(ActionKind, &available)
{
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;
out->kind = *it;
}
}
@ -1008,23 +633,23 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
{
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));
error_message = MD_S8Fmt(arena, "ActionKind string given is '%.*s', but available actions are: [%.*s]", MD_S8VArg(given_action_string), MD_S8VArg(possible_actions_str));
goto endofparsing;
}
MD_u64 start_looking_for_quote = end_of_action;
if(actions[out->npc_action_type].takes_argument)
if(actions[out->kind].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)));
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->kind].name)));
goto endofparsing;
}
char should_be_paren = sentence.str[end_of_action];
if(should_be_paren != '(')
{
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);
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->kind].name)), should_be_paren);
goto endofparsing;
}
MD_u64 beginning_of_arg = end_of_action;
@ -1038,7 +663,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
MD_String8 argument = MD_S8Substring(sentence, beginning_of_arg, end_of_arg);
start_looking_for_quote = end_of_arg + 1;
if(out->npc_action_type == ACT_give_item)
if(out->kind == ACT_give_item)
{
MD_String8 item_prefix = MD_S8Lit("ITEM_");
MD_u64 item_prefix_begin = MD_S8FindSubstring(argument, item_prefix, 0, 0);
@ -1061,7 +686,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
if(MD_S8Match(item_str, item_name, 0))
{
item_found = true;
out->given_item = *it;
out->item_to_give = *it;
}
}
@ -1097,14 +722,14 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
MD_String8 speech = MD_S8Substring(sentence, beginning_of_speech + 1, end_of_speech);
if(speech.size >= ARRLEN(out->npc_dialog.data))
if(speech.size >= ARRLEN(out->speech))
{
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));
error_message = MD_S8Fmt(arena, "The speech given is %llu bytes big, but the maximum allowed is %llu bytes.", speech.size, ARRLEN(out->speech));
goto endofparsing;
}
memcpy(out->npc_dialog.data, speech.str, speech.size);
out->npc_dialog.cur_index = (int)speech.size;
memcpy(out->speech, speech.str, speech.size);
out->speech_length = (int)speech.size;
endofparsing:
MD_ReleaseScratch(scratch);

@ -17,4 +17,21 @@
#define SERVER_URL "https://rpgpt.duckdns.org"
#endif
// REFACTORING:: also have to update in javascript!!!!!!!!
#define MAX_SENTENCE_LENGTH 400 // LOOOK AT AGBOVE COMMENT GBEFORE CHANGING
#define SENTENCE_CONST(txt) { .data = txt, .cur_index = sizeof(txt) }
#define SENTENCE_CONST_CAST(txt) (Sentence)SENTENCE_CONST(txt)
#define REMEMBERED_MEMORIES 32
#define MAX_AFTERIMAGES 6
#define TIME_TO_GEN_AFTERIMAGE (0.09f)
#define AFTERIMAGE_LIFETIME (0.5f)
#define DAMAGE_SWORD 0.05f
#define DAMAGE_BULLET 0.2f
// A* tuning
#define MAX_ASTAR_NODES 512
#define TIME_BETWEEN_PATH_GENS (0.5f)

Loading…
Cancel
Save