diff --git a/character_info.h b/character_info.h index cb8f953..654aab2 100644 --- a/character_info.h +++ b/character_info.h @@ -164,3 +164,22 @@ 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."; +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" +; \ No newline at end of file diff --git a/main.c b/main.c index 9497e95..a562a97 100644 --- a/main.c +++ b/main.c @@ -480,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; @@ -572,6 +574,7 @@ 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 prompt) @@ -588,8 +591,7 @@ int make_generation_request(String8 prompt) 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 @@ -620,21 +622,31 @@ 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) { @@ -1131,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; @@ -1827,7 +1840,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); @@ -2013,6 +2026,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) @@ -2022,12 +2036,7 @@ bool perform_action(GameState *gs, Entity *from, Action a) proceed_propagating = false; } - 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}); - StackPush(gs->judgement_memories, new_forever); - } + bool angel_heard_action = false; @@ -2088,6 +2097,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, ¤t_list, make_json_node(scratch.arena, MSG_SYSTEM, S8CString(judgement_system_prompt))); + S8ListPush(scratch.arena, ¤t_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_yes_example))); + S8ListPush(scratch.arena, ¤t_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("yes"))); + S8ListPush(scratch.arena, ¤t_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no_example))); + S8ListPush(scratch.arena, ¤t_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no"))); + S8ListPush(scratch.arena, ¤t_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no2_example))); + S8ListPush(scratch.arena, ¤t_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no"))); + S8ListPush(scratch.arena, ¤t_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; } @@ -5123,6 +5169,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 @@ -5199,12 +5246,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); @@ -5518,9 +5568,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); } @@ -5732,6 +5788,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 { @@ -5804,7 +5861,7 @@ void frame(void) Vec3 light_dir; { float t = clamp01((float)(gs.time / LENGTH_OF_DAY)); - Vec3 sun_vector = V3(2.0f*t - 1.0f, sinf(t*PI32), 0.8f); // where the sun is pointing from + 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)); } @@ -6192,6 +6249,37 @@ 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") @@ -7188,6 +7276,11 @@ void frame(void) } } + if(gs.time > LENGTH_OF_DAY) + { + gs.player->killed = true; + } + // killed screen { static float visible = 0.0f; @@ -7208,12 +7301,12 @@ void frame(void) 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(); } } @@ -7515,33 +7608,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. @@ -7555,6 +7641,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)))) diff --git a/makeprompt.h b/makeprompt.h index 78d5a59..49ef936 100644 --- a/makeprompt.h +++ b/makeprompt.h @@ -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 @@ -307,7 +308,8 @@ typedef struct GameState { bool assigned_objective; GameplayObjective objective; - Memory *judgement_memories; + Memory *judgement_memories_first; + Memory *judgement_memories_last; double time; // in seconds, fraction of length of day int judgement_gen_request; @@ -452,11 +454,19 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it) #define AddFmt(...) PushWithLint(arena, ¤t_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) { -#define HUMAN(kind) S8VArg(npc_to_human_readable(e, kind)) case ACT_none: break; case ACT_join: @@ -481,10 +491,10 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it) 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)); + 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", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind)); + AddFmt("%.*s awarded victory to %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind)); break; } } @@ -499,13 +509,23 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it) 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) + 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) { - speaking_to_you_helper = S8Lit("(Overheard conversation, they aren't speaking directly to you) "); + AddFmt(" (you are %s)", characters[e->npc_kind].name) } - - 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); + AddFmt("\n"); } if (no_longer_wants_to_converse) diff --git a/tuning.h b/tuning.h index 80fc275..755efa8 100644 --- a/tuning.h +++ b/tuning.h @@ -3,7 +3,7 @@ #define RANDOM_SEED 42 #define LEVEL_TILES 150 // width and height of level tiles array -#define LENGTH_OF_DAY (120.0) // double in seconds +#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 @@ -24,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"