Compare commits

...

4 Commits

@ -21,7 +21,7 @@
{enum: PreviousPlayer1, dialog: "Yo?", to: Daniel}
{enum: Daniel, dialog: "Are you askin' a question", to: PreviousPlayer1}
{enum: PreviousPlayer1, dialog: "I guess so? What do you think of farmers?", to: Daniel}
{enum: Daniel, dialog: "I don't tolerate questions. Get out of my sight before I make you!", to: PreviousPlayer1, action: ACT_aim_shotgun, action_argument: "PreviousPlayer1"}
{enum: Daniel, dialog: "I don't tolerate questions. Get out of my sight before I make you!", to: PreviousPlayer1, action: ACT_aim_shotgun, action_argument: "Previous Player 1"}
{enum: Raphael, dialog: "What's going on here?", to: Daniel}
{enum: Daniel, dialog: "THIS DAMNED FOOL DOESN'T UNDERSTAND RESPECT", to: Raphael}
{enum: Raphael, dialog: "Easy man, easy. I ain't much for helpin' folk but you're outta control.", to: Daniel}
@ -61,6 +61,15 @@
{enum: PreviousPlayer2, dialog: "kys", to: Tombstone}
{enum: Tombstone, dialog: "Pardon?", to: PreviousPlayer2}
{can_hear: [PreviousPlayer3, Tombstone]}
{enum: PreviousPlayer3, dialog: "Hey", to: Tombstone}
{enum: Tombstone, dialog: "Another one shall bite the dust, or so to speak.", to: PreviousPlayer3}
{enum: PreviousPlayer3, dialog: "What trisls?", to: Tombstone}
{enum: Tombstone, dialog: "The trials of self-discovery, my friend. Can you change Daniel's ways before the sun sets?", to: PreviousPlayer3}
{enum: PreviousPlayer3, dialog: "Who is Daniel?", to: Tombstone}
{enum: Tombstone, dialog: "You'll find Daniel to be the Planet of Raphael's moon... Quite the rotund buffoon.", to: PreviousPlayer3}
{enum: PreviousPlayer3, dialog: "And you're saying to change his ways? How? What does that mean?", to: Tombstone}
{enum: Tombstone, dialog: "Cause a profound self realization before sundown, or he's toast", to: PreviousPlayer3}
{can_hear: [PreviousPlayer1, Angel]}
{enum: PreviousPlayer1, dialog: "fjdsklajf", to: Angel}

@ -126,15 +126,20 @@ CharacterGen characters[] = {
.prompt = CHARACTER_PROMPT_PREFIX("The Devil") "strange red beast, the devil himself, evil incarnate. You mercilessly mock everybody who talks to you, and are intending to instill absolute chaos.",
},
{
.name = "PreviousPlayer1",
.name = "Previous Player 1",
.enum_name = "PreviousPlayer1",
.prompt = CHARACTER_PROMPT_PREFIX("Previous Player 1") "random person, just passing by",
},
{
.name = "PreviousPlayer2",
.name = "Previous Player 2",
.enum_name = "PreviousPlayer2",
.prompt = CHARACTER_PROMPT_PREFIX("Previous Player 2") "random person, just passing by",
},
{
.name = "Previous Player3",
.enum_name = "PreviousPlayer3",
.prompt = CHARACTER_PROMPT_PREFIX("Previous Player 3") "random person, just passing by",
},
{
.name = "Tombstone",
.enum_name = "Tombstone",
@ -159,3 +164,26 @@ CharacterGen characters[] = {
.silence_factor = 0.0,
},
};
char *judgement_system_prompt =
"You are to judge if, in given conversation and action history from a video game, the player has successfully caused a great transformation in Daniel. You must ONLY respond with either 'yes' or 'no', and no explanation.\n"
"Be cautious with saying yes. This causes the player to win and save Daniel's life. Only say yes if you think, based on the history, Daniel has truly changed his ways and evolved as a person.\n"
;
char *judgement_yes_example =
"The Player said \"Hey\" to Daniel\n"
"Daniel said \"What do you want?\" to The Player\n"
"The Player said \"Did you see what happened... All those people...\" to Daniel\n"
"Daniel said \"What in the hell are you talking about?\" to The Player\n"
"The Player said \"They'll never forgive you for what you did.\" to Daniel\n"
"Daniel said \"Oh God... My greed... My isolation... I'll never forgive myself\" to The Player\n"
;
char *judgement_no_example = "The Player said \"fjdskfjskj\" to Daniel\n"
"Daniel said \"Who are you to speak that gibberish at me?\" to The Player\n"
"The Player said \"pls change kthx\" to Daniel\n"
"Daniel said \"I'll never change for the likes of you\" to The Player\n"
;
char *judgement_no2_example = "The Player said \"hey\" to Daniel\n"
"Daniel said \"What could you possibly want from me?\" to The Player\n"
"The Player said \"I want to change you\" to Daniel\n"
"Daniel said \"HA! You couldn't possibly change me\" to The Player\n"
;

448
main.c

@ -460,6 +460,14 @@ LPCWSTR windows_string(String8 s)
}
#endif
typedef enum
{
GEN_Deleted = -1,
GEN_NotDoneYet = 0,
GEN_Success = 1,
GEN_Failed = 2,
} GenRequestStatus;
#ifdef DESKTOP
#ifdef WINDOWS
#pragma warning(push, 3)
@ -472,6 +480,8 @@ typedef struct ChatRequest
{
struct ChatRequest *next;
struct ChatRequest *prev;
bool user_is_done_with_this_request;
bool thread_is_done_with_this_request;
bool should_close;
int id;
int status;
@ -549,7 +559,7 @@ void generation_thread(void* my_request_voidptr)
WinAssertWithErrorCode(WinHttpReadData(hRequest, (LPVOID)out_buffer, dwSize, &dwDownloaded));
out_buffer[dwDownloaded - 1] = '\0';
Log("Got this from http, size %lu: %s\n", dwDownloaded, out_buffer);
S8ListPush(my_request->arena, &received_data_list, S8(out_buffer, dwDownloaded));
S8ListPush(my_request->arena, &received_data_list, S8(out_buffer, dwDownloaded - 1)); // the string shouldn't include a null terminator in its length, and WinHttpReadData has a null terminator here
}
} while (dwSize > 0);
String8 received_data = S8ListJoin(my_request->arena, received_data_list, &(StringJoin){0});
@ -564,10 +574,13 @@ void generation_thread(void* my_request_voidptr)
chunk_from_s8(&my_request->generated, ai_response);
my_request->status = 1;
}
my_request->thread_is_done_with_this_request = true; // @TODO Threads that finish and users who forget to mark them as done aren't collected right now, we should do that to prevent leaks
}
int make_generation_request(String8 post_req_body)
int make_generation_request(String8 prompt)
{
ArenaTemp scratch = GetScratch(0,0);
String8 post_req_body = FmtWithLint(scratch.arena, "|%.*s", S8VArg(prompt));
// checking for taken characters, pipe should only occur at the beginning
for(u64 i = 1; i < post_req_body.size; i++)
{
@ -578,8 +591,7 @@ int make_generation_request(String8 post_req_body)
if(requests_free_list)
{
to_return = requests_free_list;
requests_free_list = requests_free_list->next;
//StackPop(requests_free_list);
StackPop(requests_free_list);
*to_return = (ChatRequest){0};
}
else
@ -599,6 +611,7 @@ int make_generation_request(String8 post_req_body)
DblPushBack(requests_first, requests_last, to_return);
ReleaseScratch(scratch);
return to_return->id;
}
@ -609,21 +622,44 @@ ChatRequest *get_by_id(int id)
{
for(ChatRequest *cur = requests_first; cur; cur = cur->next)
{
if(cur->id == id)
if(cur->id == id && !cur->user_is_done_with_this_request)
{
return cur;
}
}
assert(false);
return 0;
}
void done_with_request(int id)
{
ChatRequest *req = get_by_id(id);
ArenaRelease(req->arena);
DblRemove(requests_first, requests_last, req);
StackPush(requests_free_list, req);
if(req)
{
if(req->thread_is_done_with_this_request)
{
ArenaRelease(req->arena);
DblRemove(requests_first, requests_last, req);
*req = (ChatRequest){0};
StackPush(requests_free_list, req);
}
else
{
req->user_is_done_with_this_request = true;
}
}
}
GenRequestStatus gen_request_status(int id)
{
ChatRequest *req = get_by_id(id);
if(!req)
return GEN_Deleted;
else
return req->status;
}
TextChunk gen_request_content(int id)
{
assert(get_by_id(id));
return get_by_id(id)->generated;
}
#else
@ -631,6 +667,49 @@ ISANERROR("Only know how to do desktop http requests on windows")
#endif // WINDOWS
#endif // DESKTOP
#ifdef WEB
int make_generation_request(String8 prompt_str)
{
ArenaTemp scratch = GetScratch(0, 0);
String8 terminated_completion_url = nullterm(scratch.arena, FmtWithLint(scratch.arena, "%s://%s:%d/completion", IS_SERVER_SECURE ? "https" : "http", SERVER_DOMAIN, SERVER_PORT));
int req_id = EM_ASM_INT({
return make_generation_request(UTF8ToString($0, $1), UTF8ToString($2, $3));
},
prompt_str.str, (int)prompt_str.size, terminated_completion_url.str, (int)terminated_completion_url.size);
ReleaseScratch(scratch);
return req_id;
}
GenRequestStatus gen_request_status(int id)
{
int status = EM_ASM_INT({
return get_generation_request_status($0);
}, id);
return status;
}
TextChunk gen_request_content(int id)
{
char sentence_cstr[MAX_SENTENCE_LENGTH] = {0};
EM_ASM({
let generation = get_generation_request_content($0);
stringToUTF8(generation, $1, $2);
},
id, sentence_cstr, ARRLEN(sentence_cstr) - 1); // I think minus one for null terminator...
TextChunk to_return = {0};
memcpy(to_return.text, sentence_cstr, MAX_SENTENCE_LENGTH);
to_return.text_length = strlen(sentence_cstr);
return to_return;
}
void done_with_request(int id)
{
EM_ASM({
done_with_generation_request($0);
},
id);
}
#endif // WEB
Memory *memories_free_list = 0;
RememberedError *remembered_error_free_list = 0;
@ -1064,6 +1143,7 @@ typedef struct
u64 vertices_length;
sg_buffer loaded_buffer;
uint64_t last_updated_bones_frame;
sg_image bones_texture;
sg_image image;
int bones_texture_width;
@ -1694,10 +1774,12 @@ CanTalkTo get_can_talk_to(Entity *e)
Entity *get_targeted(Entity *from, NpcKind targeted)
{
bool ignore_radius = from->npc_kind == NPC_Player || targeted == NPC_Player; // player conversations can go across the map to make sure the player always sees them
ENTITIES_ITER(gs.entities)
{
if(it != from && (it->is_npc) && LenV2(SubV2(it->pos, from->pos)) < PROPAGATE_ACTIONS_RADIUS && it->npc_kind == targeted)
if(it != from && (it->is_npc) && it->npc_kind == targeted)
{
if(ignore_radius || LenV2(SubV2(it->pos, from->pos)) < PROPAGATE_ACTIONS_RADIUS)
return it;
}
}
@ -1740,6 +1822,14 @@ String8 is_action_valid(Arena *arena, Entity *from, Action a)
String8 error_message = (String8){0};
if(error_message.size == 0 && a.speech.text_length > 0)
{
if(S8FindSubstring(TextChunkString8(a.speech), S8Lit("assist"), 0, StringMatchFlag_CaseInsensitive) != a.speech.text_length)
{
error_message = S8Lit("You cannot use the word 'assist' in any form, you are not an assistant, do not act overtly helpful");
}
}
CanTalkTo talk = get_can_talk_to(from);
if(error_message.size == 0 && a.talking_to_kind)
{
@ -1752,7 +1842,7 @@ String8 is_action_valid(Arena *arena, Entity *from, Action a)
break;
}
}
if(from->npc_kind == NPC_Player) found = true; // player can always speak to anybody even if it's too far
if(from->npc_kind == NPC_Player || a.talking_to_kind == NPC_Player) found = true; // player can always speak to anybody even if it's too far
if(!found)
{
error_message = FmtWithLint(arena, "Character you're talking to, %s, isn't close enough to be talked to", characters[a.talking_to_kind].enum_name);
@ -1938,6 +2028,7 @@ bool perform_action(GameState *gs, Entity *from, Action a)
context.talking_to_kind = a.talking_to_kind;
String8 is_valid = is_action_valid(scratch.arena, from, a);
bool proceed_propagating = true;
if(is_valid.size > 0)
@ -1947,6 +2038,8 @@ bool perform_action(GameState *gs, Entity *from, Action a)
proceed_propagating = false;
}
bool angel_heard_action = false;
Entity *targeted = 0;
@ -2006,6 +2099,43 @@ bool perform_action(GameState *gs, Entity *from, Action a)
remember_action(gs, gs->angel, a, angel_context);
}
if(from->npc_kind == NPC_Daniel || a.talking_to_kind == NPC_Daniel)
{
Memory *new_forever = PushArray(gs->arena, Memory, 1);
*new_forever = make_memory(a, (MemoryContext){.author_npc_kind = from->npc_kind, .talking_to_kind = a.talking_to_kind, .judgement_memory = true});
DblPushBack(gs->judgement_memories_first, gs->judgement_memories_last, new_forever);
}
if(from->npc_kind == NPC_Daniel)
{
if(gs->judgement_gen_request == 0 && gs->judgement_memories_first)
{
String8List history_list = {0};
for(Memory *it = gs->judgement_memories_first; it; it = it->next)
{
String8List desc = memory_description(scratch.arena, gs->world_entity, it);
S8ListConcat(&history_list, &desc);
}
String8 current_history = S8ListJoin(scratch.arena, history_list, &(StringJoin){0});
Log("Submitting judgement with current history: ```\n%.*s\n```\n", S8VArg(current_history));
String8List current_list = {0};
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_SYSTEM, S8CString(judgement_system_prompt)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_yes_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("yes")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no2_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, current_history));
String8 json_array = S8ListJoin(scratch.arena, current_list, &(StringJoin){0});
json_array.size -= 1; // remove trailing comma. fuck json
String8 prompt = FmtWithLint(scratch.arena, "[%.*s]", S8VArg(json_array));
gs->judgement_gen_request = make_generation_request(prompt);
}
}
ReleaseScratch(scratch);
return proceed_propagating;
}
@ -2183,7 +2313,12 @@ void transition_to_room(GameState *gs, ThreeDeeLevel *level, String8 new_room_na
void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *level)
{
if(gs->arena)
{
ArenaRelease(gs->arena);
}
memset(gs, 0, sizeof(GameState));
gs->arena = ArenaAlloc();
rnd_gamerand_seed(&gs->random, RANDOM_SEED);
// make entities for all rooms
@ -3350,6 +3485,7 @@ String8 make_devtools_help(Arena *arena)
P("P - toggles spall profiling on/off, don't leave on for very long as it consumes a lot of storage if you do that. The resulting spall trace is saved to the file '%s'\n", PROFILING_SAVE_FILENAME);
P("If you hover over somebody it will display some parts of their memories, can be somewhat helpful\n");
P("P - immediately kills %s\n", characters[NPC_Raphael].name);
P("J - judges the player and outputs their verdict to the console\n");
#undef P
String8 to_return = S8ListJoin(arena, list, &(StringJoin){0});
@ -3432,7 +3568,7 @@ void init(void)
shifted_farmer_armature = load_armature(persistent_arena, binary_file, S8Lit("Farmer.bin"));
shifted_farmer_armature.image = image_shifted_farmer;
Log("Done. Used %f of the frame arena, %llu kB\n", (double) frame_arena->pos / (double)frame_arena->cap, (frame_arena->pos/1024));
Log("Done. Used %f of the frame arena, %d kB\n", (double) frame_arena->pos / (double)frame_arena->cap, (int)(frame_arena->pos/1024));
ArenaClear(frame_arena);
@ -5035,6 +5171,7 @@ double elapsed_time = 0.0;
double unwarped_elapsed_time = 0.0;
double last_frame_processing_time = 0.0;
double last_frame_gameplay_processing_time = 0.0;
uint64_t frame_index = 0; // for rendering tick stuff, gamestate tick is used for game logic tick stuff and is serialized/deserialized/saved
uint64_t last_frame_time;
typedef struct
@ -5111,12 +5248,15 @@ bool imbutton_key(ImbuttonArgs args)
draw_quad((DrawParams) { quad_aabb(args.button_aabb), IMG(image_white_square), blendalpha(WHITE, button_alpha), .layer = layer, });
// don't use draw centered text here because it looks funny for some reason... I think it's because the vertical line advance of the font, used in draw_centered_text, is the wrong thing for a button like this
TextParams t = (TextParams) { false, args.text, aabb_center(args.button_aabb), BLACK, args.text_scale, .clip_to = args.button_aabb, .do_clipping = true, .layer = layer, .use_font = font};
TextParams t = (TextParams) { false, args.text, aabb_center(args.button_aabb), BLACK, args.text_scale, .clip_to = args.button_aabb, .do_clipping = true, .layer = layer, .use_font = font };
t.dry_run = true;
AABB aabb = draw_text(t);
t.dry_run = false;
t.pos = SubV2(aabb_center(args.button_aabb), MulV2F(aabb_size(aabb), 0.5f));
draw_text(t);
if(aabb_is_valid(aabb))
{
t.dry_run = false;
t.pos = SubV2(aabb_center(args.button_aabb), MulV2F(aabb_size(aabb), 0.5f));
draw_text(t);
}
}
hmput(imui_state, args.key, state);
@ -5430,9 +5570,15 @@ void flush_all_drawn_things(ShadowMats shadow)
}
}
sg_update_image(armature->bones_texture, &(sg_image_data){
.subimage[0][0] = (sg_range){bones_tex, bones_tex_size},
});
// sokol prohibits updating an image more than once per frame
if(armature->last_updated_bones_frame != frame_index)
{
armature->last_updated_bones_frame = frame_index;
sg_update_image(armature->bones_texture, &(sg_image_data){
.subimage[0][0] = (sg_range){bones_tex, bones_tex_size},
});
}
ReleaseScratch(scratch);
}
@ -5591,7 +5737,7 @@ TextPlacementSettings speech_bubble = {
String8List words_on_current_page(Entity *it, TextPlacementSettings *settings)
{
String8 last = last_said_sentence(it);
PlacedWordList placed = place_wrapped_words(frame_arena, split_by_word(frame_arena, last), settings->text_scale, settings->width_in_pixels, *settings->font, JUST_LEFT);
PlacedWordList placed = place_wrapped_words(frame_arena, split_by_word(frame_arena, last), settings->text_scale, settings->text_width_in_pixels, *settings->font, JUST_LEFT);
String8List on_current_page = {0};
for(PlacedWord *cur = placed.first; cur; cur = cur->next)
@ -5644,6 +5790,7 @@ void frame(void)
double dt_double = unwarped_dt_double*speed_factor;
float unwarped_dt = (float)unwarped_dt_double;
float dt = (float)dt_double;
frame_index += 1;
#if 0
{
@ -5669,6 +5816,8 @@ void frame(void)
{
uint64_t time_start_frame = stm_now();
gs.time += dt_double;
text_input_fade = Lerp(text_input_fade, unwarped_dt * 8.0f, receiving_text_input ? 1.0f : 0.0f);
Vec3 player_pos = V3(gs.player->pos.x, 0.0, gs.player->pos.y);
@ -5713,8 +5862,8 @@ void frame(void)
Vec3 light_dir;
{
float t = (float)(elapsed_time/3.0f - floor(elapsed_time/3.0f));
Vec3 sun_vector = V3(2.0f*t - 1.0f, sinf(t*PI32), 0.8f); // where the sun is pointing from
float t = clamp01((float)(gs.time / LENGTH_OF_DAY));
Vec3 sun_vector = V3(2.0f*t - 1.0f, sinf(t*PI32)*0.8f + 0.2f, 0.8f); // where the sun is pointing from
light_dir = NormV3(MulV3F(sun_vector, -1.0f));
}
@ -5849,6 +5998,7 @@ void frame(void)
case NPC_Devil:
case NPC_PreviousPlayer1:
case NPC_PreviousPlayer2:
case NPC_PreviousPlayer3:
assert(false);
break;
}
@ -6101,6 +6251,39 @@ void frame(void)
}
}
if(gs.judgement_gen_request != 0)
{
GenRequestStatus stat = gen_request_status(gs.judgement_gen_request);
switch(stat)
{
case GEN_NotDoneYet:
break;
case GEN_Success:
{
TextChunk generated = gen_request_content(gs.judgement_gen_request);
if (generated.text_length > 0 && S8FindSubstring(TextChunkString8(generated), S8Lit("yes"), 0, StringMatchFlag_CaseInsensitive) == 0)
{
Log("Starts with yes, success!\n");
gs.won = true;
}
else if (S8FindSubstring(TextChunkString8(generated), S8Lit("no"), 0, StringMatchFlag_CaseInsensitive) == generated.text_length)
{
Log("WARNING: generated judgement string '%.*s', doesn't match yes or no, and so is nonsensical! AI acting up!\n", TextChunkVArg(generated));
}
}
break;
case GEN_Failed:
having_errors = true;
break;
case GEN_Deleted:
break;
}
if(stat != GEN_NotDoneYet)
{
done_with_request(gs.judgement_gen_request);
gs.judgement_gen_request = 0;
}
}
// @Place(UI rendering that happens before gameplay processing so can consume events before the gameplay needs them)
PROFILE_SCOPE("Entity UI Rendering")
@ -6303,61 +6486,40 @@ void frame(void)
{
assert(it->gen_request_id > 0);
#ifdef DESKTOP
int status = get_by_id(it->gen_request_id)->status;
#else
#ifdef WEB
int status = EM_ASM_INT( {
return get_generation_request_status($0);
}, it->gen_request_id);
#else
ISANERROR("Don't know how to do this stuff on this platform.")
#endif // WEB
#endif // DESKTOP
if (status == 0)
GenRequestStatus status = gen_request_status(it->gen_request_id);
switch(status)
{
// simply not done yet
}
else
{
if (status == 1)
case GEN_Deleted:
it->gen_request_id = 0;
break;
case GEN_NotDoneYet:
break;
case GEN_Success:
{
having_errors = false;
// done! we can get the string
char sentence_cstr[MAX_SENTENCE_LENGTH] = { 0 };
#ifdef WEB
EM_ASM( {
let generation = get_generation_request_content($0);
stringToUTF8(generation, $1, $2);
}, it->gen_request_id, sentence_cstr, ARRLEN(sentence_cstr) - 1); // I think minus one for null terminator...
#endif
#ifdef DESKTOP
memcpy(sentence_cstr, get_by_id(it->gen_request_id)->generated.text, get_by_id(it->gen_request_id)->generated.text_length);
#endif
String8 sentence_str = S8CString(sentence_cstr);
TextChunk sentence_chunk = gen_request_content(it->gen_request_id);
String8 sentence_str = TextChunkString8(sentence_chunk);
// parse out from the sentence NPC action and dialog
Action out = {0};
ArenaTemp scratch = GetScratch(0, 0);
Log("Parsing `%.*s`...\n", S8VArg(sentence_str));
String8 parse_response = parse_chatgpt_response(scratch.arena, it, sentence_str, &out);
String8 parse_response = parse_chatgpt_response(frame_arena, it, sentence_str, &out);
// check that it wraps in below two lines
TextPlacementSettings *to_wrap_to = &speech_bubble;
PlacedWordList placed = place_wrapped_words(frame_arena, split_by_word(frame_arena, TextChunkString8(out.speech)), to_wrap_to->text_scale, to_wrap_to->text_width_in_pixels, *to_wrap_to->font, JUST_LEFT);
int words_over_limit = 0;
for(PlacedWord *cur = placed.first; cur; cur = cur->next)
for (PlacedWord *cur = placed.first; cur; cur = cur->next)
{
if(cur->line_index >= to_wrap_to->lines_per_page*to_wrap_to->maximum_pages_from_ai) // the max number of lines of text on a bubble
if (cur->line_index >= to_wrap_to->lines_per_page * to_wrap_to->maximum_pages_from_ai) // the max number of lines of text on a bubble
{
words_over_limit += 1;
}
}
if(words_over_limit > 0)
if (words_over_limit > 0)
{
String8 new_err = FmtWithLint(frame_arena, "Your speech is %d words over the maximum limit, you must be more succinct and remove at least that many words", words_over_limit);
append_to_errors(it, make_memory(out, (MemoryContext){.i_said_this = true, .author_npc_kind = it->npc_kind, .talking_to_kind = out.talking_to_kind}), new_err);
@ -6368,8 +6530,6 @@ ISANERROR("Don't know how to do this stuff on this platform.")
{
Log("Performing action %s!\n", actions[out.kind].name);
perform_action(&gs, it, out);
}
else
{
@ -6378,42 +6538,19 @@ ISANERROR("Don't know how to do this stuff on this platform.")
}
}
ReleaseScratch(scratch);
#ifdef WEB
EM_ASM( {
done_with_generation_request($0);
}, it->gen_request_id);
#endif
#ifdef DESKTOP
done_with_request(it->gen_request_id);
#endif
}
else if (status == 2)
{
Log("Failed to generate dialog! Fuck!\n");
having_errors = true;
/*
Action to_perform = {0};
String8 speech_mdstring = S8Lit("I'm not sure...");
memcpy(to_perform.speech, speech_mdstring.str, speech_mdstring.size);
to_perform.speech_length = (int)speech_mdstring.size;
perform_action(&gs, it, to_perform);
*/
}
else if (status == -1)
{
Log("Generation request doesn't exist anymore, that's fine...\n");
}
else
{
Log("Unknown generation request status: %d\n", status);
it->gen_request_id = 0;
}
break;
case GEN_Failed:
Log("Failed to generate dialog! Fuck!\n");
having_errors = true;
it->gen_request_id = 0;
break;
default:
Log("Unknown generation request status: %d\n", status);
it->gen_request_id = 0;
break;
}
}
}
@ -6830,38 +6967,19 @@ ISANERROR("Don't know how to do this stuff on this platform.")
{
it->perceptions_dirty = false; // needs to be in beginning because they might be redirtied by the new perception
String8 prompt_str = {0};
#ifdef DO_CHATGPT_PARSING
prompt_str = generate_chatgpt_prompt(frame_arena, &gs, it, get_can_talk_to(it));
#else
generate_prompt(it, &prompt);
#endif
Log("Sending request with prompt `%.*s`\n", S8VArg(prompt_str));
Log("Want to make request with prompt `%.*s`\n", S8VArg(prompt_str));
#ifdef WEB
// fire off generation request, save id
ArenaTemp scratch = GetScratch(0, 0);
String8 terminated_completion_url = nullterm(scratch.arena, FmtWithLint(scratch.arena, "%s://%s:%d/completion", IS_SERVER_SECURE ? "https" : "http", SERVER_DOMAIN, SERVER_PORT));
int req_id = EM_ASM_INT({
return make_generation_request(UTF8ToString($0, $1), UTF8ToString($2, $3));
},
prompt_str.str, (int)prompt_str.size, terminated_completion_url.str, (int)terminated_completion_url.size);
it->gen_request_id = req_id;
ReleaseScratch(scratch);
#endif
#ifdef DESKTOP
ArenaTemp scratch = GetScratch(0, 0);
String8 ai_response = {0};
bool mocking_the_ai_response = false;
#ifdef DEVTOOLS
#ifdef MOCK_AI_RESPONSE
mocking_the_ai_response = true;
#endif
#endif
#endif // mock
#endif // devtools
bool succeeded = true; // couldn't get AI response if false
if (mocking_the_ai_response)
{
String8 ai_response = {0};
if (it->memories_last->context.talking_to_kind == it->npc_kind)
//if (it->memories_last->context.author_npc_kind != it->npc_kind)
{
@ -6898,34 +7016,27 @@ ISANERROR("Don't know how to do this stuff on this platform.")
}
// something to mock
if (ai_response.size > 0)
assert(ai_response.size > 0);
Log("Mocking...\n");
Action a = {0};
String8 error_message = S8Lit("Something really bad happened bro. File " STRINGIZE(__FILE__) " Line " STRINGIZE(__LINE__));
if (succeeded)
{
Log("Mocking...\n");
Action a = {0};
String8 error_message = S8Lit("Something really bad happened bro. File " STRINGIZE(__FILE__) " Line " STRINGIZE(__LINE__));
if (succeeded)
{
error_message = parse_chatgpt_response(scratch.arena, it, ai_response, &a);
}
error_message = parse_chatgpt_response(frame_arena, it, ai_response, &a);
}
assert(succeeded);
assert(error_message.size == 0);
assert(succeeded);
assert(error_message.size == 0);
String8 valid_str = is_action_valid(frame_arena, it, a);
assert(valid_str.size == 0);
perform_action(&gs, it, a);
}
String8 valid_str = is_action_valid(frame_arena, it, a);
assert(valid_str.size == 0);
perform_action(&gs, it, a);
}
else
{
String8 post_request_body = FmtWithLint(scratch.arena, "|%.*s", S8VArg(prompt_str));
it->gen_request_id = make_generation_request(post_request_body);
it->gen_request_id = make_generation_request(prompt_str);
}
ReleaseScratch(scratch);
#undef SAY
#endif // desktop endif
}
}
}
@ -7169,6 +7280,11 @@ ISANERROR("Don't know how to do this stuff on this platform.")
}
}
if(gs.time > LENGTH_OF_DAY)
{
gs.player->killed = true;
}
// killed screen
{
static float visible = 0.0f;
@ -7189,12 +7305,12 @@ ISANERROR("Don't know how to do this stuff on this platform.")
float shake_speed = 9.0f;
Vec2 win_offset = V2(sinf((float)unwarped_elapsed_time * shake_speed * 1.5f + 0.1f), sinf((float)unwarped_elapsed_time * shake_speed + 0.3f));
win_offset = MulV2F(win_offset, 10.0f);
draw_centered_text((TextParams){false, S8Lit("YOU WERE KILLED"), AddV2(MulV2F(screen_size(), 0.5f), win_offset), WHITE, 3.0f*visible}); // YOU DIED
draw_centered_text((TextParams){false, S8Lit("YOU FAILED TO SAVE DANIEL"), AddV2(MulV2F(screen_size(), 0.5f), win_offset), WHITE, 3.0f*visible}); // YOU DIED
if(imbutton(aabb_centered(V2(screen_size().x/2.0f, screen_size().y*0.25f), MulV2F(V2(170.0f, 60.0f), visible)), 1.5f*visible, S8Lit("Continue")))
{
gs.player->killed = false;
transition_to_room(&gs, &level_threedee, S8Lit("StartingRoom"));
//transition_to_room(&gs, &level_threedee, S8Lit("StartingRoom"));
reset_level();
}
}
@ -7202,7 +7318,6 @@ ISANERROR("Don't know how to do this stuff on this platform.")
#define HELPER_SIZE 250.0f
// keyboard tutorial icons
if(false)
if (!mobile_controls)
@ -7235,6 +7350,11 @@ ISANERROR("Don't know how to do this stuff on this platform.")
#ifdef DEVTOOLS
if(keypressed[SAPP_KEYCODE_J])
{
Log("Judgement Day!\n");
}
// statistics @Place(devtools drawing developer menu drawing)
if (show_devtools)
PROFILE_SCOPE("devtools drawing")
@ -7492,33 +7612,26 @@ void cleanup(void)
void event(const sapp_event *e)
{
if (e->key_repeat) return;
if (e->type == SAPP_EVENTTYPE_RESIZED)
{
create_screenspace_gfx_state();
}
if (e->type == SAPP_EVENTTYPE_TOUCHES_BEGAN)
{
if (!mobile_controls)
{
thumbstick_base_pos = V2(screen_size().x * 0.25f, screen_size().y * 0.25f);
thumbstick_nub_pos = thumbstick_base_pos;
}
mobile_controls = true;
}
#ifdef DESKTOP
#ifdef DESKTOP
// the desktop text backend, for debugging purposes
if (receiving_text_input)
{
if (e->type == SAPP_EVENTTYPE_CHAR)
if (e->type == SAPP_EVENTTYPE_KEY_DOWN && e->key_code == SAPP_KEYCODE_BACKSPACE)
{
if(text_input_buffer_length > 0)
text_input_buffer_length -= 1;
}
else
{
if (text_input_buffer_length < ARRLEN(text_input_buffer))
if (e->type == SAPP_EVENTTYPE_CHAR)
{
APPEND_TO_NAME(text_input_buffer, text_input_buffer_length, ARRLEN(text_input_buffer), (char)e->char_code);
if (text_input_buffer_length < ARRLEN(text_input_buffer))
{
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)
{
// doesn't account for, if the text input buffer is completely full and doesn't have a null terminator.
@ -7532,6 +7645,25 @@ void event(const sapp_event *e)
}
#endif
if (e->key_repeat) return;
if (e->type == SAPP_EVENTTYPE_RESIZED)
{
create_screenspace_gfx_state();
}
if (e->type == SAPP_EVENTTYPE_TOUCHES_BEGAN)
{
if (!mobile_controls)
{
thumbstick_base_pos = V2(screen_size().x * 0.25f, screen_size().y * 0.25f);
thumbstick_nub_pos = thumbstick_base_pos;
}
mobile_controls = true;
}
if (e->type == SAPP_EVENTTYPE_KEY_DOWN &&
(e->key_code == SAPP_KEYCODE_F11 ||
e->key_code == SAPP_KEYCODE_ENTER && ((e->modifiers & SAPP_MODIFIER_ALT) || (e->modifiers & SAPP_MODIFIER_SHIFT))))

@ -130,6 +130,7 @@ typedef struct
NpcKind talking_to_kind;
bool heard_physically; // if not physically, the source was directly
bool drama_memory; // drama memories arent forgotten, and once they end it's understood that a lot of time has passed.
bool judgement_memory; // judgement memories have special printing for when Daniel says nothing, to make sure the AI understands his attitude towards the player
} MemoryContext;
// memories are subjective to an individual NPC
@ -261,9 +262,7 @@ typedef struct Entity
float dialog_fade;
RememberedError *errorlist_first;
RememberedError *errorlist_last;
#ifdef DESKTOP
int times_talked_to; // for better mocked response string
#endif
float loading_anim_in;
Memory *memories_first;
Memory *memories_last;
@ -298,6 +297,7 @@ typedef BUFF(ActionKind, 8) AvailableActions;
typedef struct GameState {
Arena *arena;
uint64_t tick;
bool won;
@ -306,6 +306,11 @@ typedef struct GameState {
bool assigned_objective;
GameplayObjective objective;
Memory *judgement_memories_first;
Memory *judgement_memories_last;
double time; // in seconds, fraction of length of day
int judgement_gen_request;
// processing may still occur after time has stopped on the gamestate,
bool stopped_time;
@ -441,6 +446,102 @@ String8List dump_memory_as_json(Arena *arena, Memory *it)
return current_list;
}
String8List memory_description(Arena *arena, Entity *e, Memory *it)
{
String8List current_list = {0};
#define AddFmt(...) PushWithLint(arena, &current_list, __VA_ARGS__)
// dump a human understandable sentence description of what happened in this memory
bool no_longer_wants_to_converse = false; // add the no longer wants to converse text after any speech, it makes more sense reading it
#define HUMAN(kind) S8VArg(npc_to_human_readable(e, kind))
if(it->context.judgement_memory)
{
if((it->action_taken == ACT_none && it->speech.text_length == 0) && it->context.author_npc_kind != NPC_Player)
{
AddFmt("%.*s said and did nothing in response\n", HUMAN(it->context.author_npc_kind));
}
}
if (it->action_taken != ACT_none)
{
switch (it->action_taken)
{
case ACT_none:
break;
case ACT_join:
AddFmt("%.*s joined %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_leave:
AddFmt("%.*s left their party\n", HUMAN(it->context.author_npc_kind));
break;
case ACT_aim_shotgun:
AddFmt("%.*s aimed their shotgun at %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_fire_shotgun:
AddFmt("%.*s fired their shotgun at %.*s, brutally murdering them.\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_put_shotgun_away:
AddFmt("%.*s holstered their shotgun, no longer threatening anybody\n", HUMAN(it->context.author_npc_kind));
break;
case ACT_approach:
AddFmt("%.*s approached %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_end_conversation:
no_longer_wants_to_converse = true;
break;
case ACT_assign_gameplay_objective:
AddFmt("%.*s assigned a definitive game objective to %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
break;
case ACT_award_victory:
AddFmt("%.*s awarded victory to %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
break;
}
}
if (it->speech.text_length > 0)
{
String8 target_string = S8Lit("the world");
if (it->context.talking_to_kind != NPC_nobody)
{
if (it->context.talking_to_kind == e->npc_kind)
target_string = S8Lit("you");
else
target_string = S8CString(characters[it->context.talking_to_kind].name);
}
if(!e->is_world)
{
if(it->context.talking_to_kind == e->npc_kind)
{
AddFmt("(Speaking directly you) ");
}
else
{
AddFmt("(Overheard conversation, they aren't speaking directly to you) ");
}
}
AddFmt("%s said \"%.*s\" to %.*s", characters[it->context.author_npc_kind].name, TextChunkVArg(it->speech), S8VArg(target_string));
if(!e->is_world)
{
AddFmt(" (you are %s)", characters[e->npc_kind].name)
}
AddFmt("\n");
}
if (no_longer_wants_to_converse)
{
if (it->action_argument.targeting == NPC_nobody)
{
AddFmt("%.*s no longer wants to converse with everybody\n", HUMAN(it->context.author_npc_kind));
}
else
{
AddFmt("%.*s no longer wants to converse with %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
}
}
#undef HUMAN
#undef AddFmt
return current_list;
}
// outputs json which is parsed by the server
String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkTo can_talk_to)
{
@ -507,76 +608,8 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
in_drama_memories = false;
AddFmt("Some time passed...\n");
}
// dump a human understandable sentence description of what happened in this memory
bool no_longer_wants_to_converse = false; // add the no longer wants to converse text after any speech, it makes more sense reading it
if(it->action_taken != ACT_none)
{
switch(it->action_taken)
{
#define HUMAN(kind) S8VArg(npc_to_human_readable(e, kind))
case ACT_none:
break;
case ACT_join:
AddFmt("%.*s joined %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_leave:
AddFmt("%.*s left their party\n", HUMAN(it->context.author_npc_kind));
break;
case ACT_aim_shotgun:
AddFmt("%.*s aimed their shotgun at %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_fire_shotgun:
AddFmt("%.*s fired their shotgun at %.*s, brutally murdering them.\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_put_shotgun_away:
AddFmt("%.*s holstered their shotgun, no longer threatening anybody\n", HUMAN(it->context.author_npc_kind));
break;
case ACT_approach:
AddFmt("%.*s approached %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
break;
case ACT_end_conversation:
no_longer_wants_to_converse = true;
break;
case ACT_assign_gameplay_objective:
AddFmt("%.*s assigned a definitive game objective to %.*s", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
break;
case ACT_award_victory:
AddFmt("%.*s awarded victory to %.*s", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
break;
}
}
if(it->speech.text_length > 0)
{
String8 target_string = S8Lit("the world");
if(it->context.talking_to_kind != NPC_nobody)
{
if(it->context.talking_to_kind == e->npc_kind)
target_string = S8Lit("you");
else
target_string = S8CString(characters[it->context.talking_to_kind].name);
}
String8 speaking_to_you_helper = S8Lit("(Speaking directly you) ");
if(it->context.talking_to_kind != e->npc_kind)
{
speaking_to_you_helper = S8Lit("(Overheard conversation, they aren't speaking directly to you) ");
}
AddFmt("%.*s%s said \"%.*s\" to %.*s (you are %s)\n", S8VArg(speaking_to_you_helper), characters[it->context.author_npc_kind].name, TextChunkVArg(it->speech), S8VArg(target_string), characters[e->npc_kind].name);
}
if(no_longer_wants_to_converse)
{
if (it->action_argument.targeting == NPC_nobody)
{
AddFmt("%.*s no longer wants to converse with everybody\n", HUMAN(it->context.author_npc_kind));
}
else
{
AddFmt("%.*s no longer wants to converse with %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
}
}
#undef HUMAN
String8List desc_list = memory_description(scratch.arena, e, it);
S8ListConcat(&current_list, &desc_list);
}
// if I said this, or it's the last memory, flush the current list as a user node

@ -508,7 +508,11 @@ func main() {
logResponses = os.Getenv("LOG_RESPONSES") != ""
doCors = os.Getenv("CORS") != ""
if doCors { log.Println("Doing cors"); }
if doCors {
log.Println("Doing cors");
} else {
log.Println("Warning: Not adding cors header to responses, you should probably be running this through a proxy like nginx that does that! To activate cors set the `CORS` environment variable to anything");
}
c = openai.NewClient(api_key)
http.HandleFunc("/completion", completion)

@ -27,6 +27,8 @@ Urgent:
Long distance:
- nocodegen instead of codegen argument
- Blur game on bitmap modal input mode
- Detect when an arena accidentally has more than one ArenaTemp requested of it
- In case of AI failure redact the memories that prompted the character to make a generation request
- Polygon and circle collision with cutec2 probably for the player being unable to collide with the camera bounds, and non axis aligned collision rects
- set the game in oregon (suggestion by phillip)
- Room system where characters can go to rooms. camera constrained to room bounds, and know which rooms are near them to go to

@ -3,6 +3,7 @@
#define RANDOM_SEED 42
#define LEVEL_TILES 150 // width and height of level tiles array
#define LENGTH_OF_DAY (60.0 * 5.0) // in seconds
#define LAYERS 3
#define TILE_SIZE 0.5f // in pixels
#define PLAYER_SPEED 0.15f // in meters per second
@ -23,9 +24,9 @@
#define ARENA_SIZE (1024*1024*20)
#define BIG_ARENA_SIZE (ARENA_SIZE * 4)
#define PROFILING_SAVE_FILENAME "rpgpt.spall"
#ifdef DEVTOOLS
#define PROFILING_SAVE_FILENAME "rpgpt.spall"
// server url cannot have trailing slash
//#define MOCK_AI_RESPONSE
#define SERVER_DOMAIN "localhost"

Loading…
Cancel
Save