You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
758 lines
20 KiB
C
758 lines
20 KiB
C
#pragma once
|
|
|
|
#include "buff.h"
|
|
#include "HandmadeMath.h" // vector types in entity struct definition
|
|
#include "better_assert.h"
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <stdlib.h> // atoi
|
|
#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__); }
|
|
|
|
|
|
// 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
|
|
|
|
typedef BUFF(char, 1024 * 10) Escaped;
|
|
|
|
bool character_valid(char c)
|
|
{
|
|
return c <= 126 && c >= 32;
|
|
}
|
|
|
|
MD_String8 escape_for_json(MD_Arena *arena, MD_String8 from)
|
|
{
|
|
MD_u64 output_size = 0;
|
|
#define SHOULD_ESCAPE(c) (c == '"' || c == '\n')
|
|
for (int i = 0; i < from.size; i++)
|
|
{
|
|
char c = from.str[i];
|
|
if (SHOULD_ESCAPE(c))
|
|
{
|
|
output_size += 2;
|
|
}
|
|
else
|
|
{
|
|
if (!character_valid(c))
|
|
{
|
|
// replaces with question mark
|
|
Log("Unknown character code %d\n", c);
|
|
}
|
|
output_size += 1;
|
|
}
|
|
}
|
|
|
|
MD_String8 output = {
|
|
.str = MD_ArenaPush(arena, output_size),
|
|
.size = output_size,
|
|
};
|
|
MD_u64 output_cursor = 0;
|
|
|
|
for(MD_u64 i = 0; i < from.size; i++)
|
|
{
|
|
#define APPEND(elem) APPEND_TO_NAME(output.str, output_cursor, output.size, elem);
|
|
assert(output_cursor < output.size);
|
|
if(SHOULD_ESCAPE(from.str[i]))
|
|
{
|
|
if(from.str[i] == '\n')
|
|
{
|
|
APPEND('\\');
|
|
APPEND('n');
|
|
}
|
|
else
|
|
{
|
|
APPEND('\\');
|
|
APPEND(from.str[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
APPEND(from.str[i]);
|
|
}
|
|
#undef APPEND
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
ItemKind item_to_give;
|
|
} ActionArgument;
|
|
|
|
typedef struct Action
|
|
{
|
|
ActionKind kind;
|
|
ActionArgument argument;
|
|
MD_u8 speech[MAX_SENTENCE_LENGTH];
|
|
int speech_length;
|
|
|
|
} 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;
|
|
|
|
// 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;
|
|
ActionArgument action_argument;
|
|
|
|
bool is_error; // if is an error message then no context is relevant
|
|
|
|
// the context that the action happened in
|
|
MemoryContext context;
|
|
|
|
MD_u8 speech[MAX_SENTENCE_LENGTH];
|
|
int speech_length;
|
|
|
|
ItemKind given_or_received_item;
|
|
} Memory;
|
|
|
|
typedef enum PropKind
|
|
{
|
|
TREE0,
|
|
TREE1,
|
|
TREE2,
|
|
ROCK0,
|
|
} PropKind;
|
|
|
|
typedef struct EntityRef
|
|
{
|
|
int index;
|
|
int generation;
|
|
} EntityRef;
|
|
|
|
typedef enum CharacterState
|
|
{
|
|
CHARACTER_WALKING,
|
|
CHARACTER_IDLE,
|
|
CHARACTER_TALKING,
|
|
} CharacterState;
|
|
|
|
typedef enum
|
|
{
|
|
STANDING_INDIFFERENT,
|
|
STANDING_JOINED,
|
|
STANDING_FIGHTING,
|
|
} NPCPlayerStanding;
|
|
|
|
|
|
typedef Vec4 Color;
|
|
|
|
typedef struct
|
|
{
|
|
AnimKind anim;
|
|
double elapsed_time;
|
|
bool flipped;
|
|
Vec2 pos;
|
|
Color tint;
|
|
bool no_shadow;
|
|
} DrawnAnimatedSprite;
|
|
|
|
typedef struct
|
|
{
|
|
DrawnAnimatedSprite drawn;
|
|
float alive_for;
|
|
} PlayerAfterImage;
|
|
|
|
typedef BUFF(Vec2, MAX_ASTAR_NODES) AStarPath;
|
|
|
|
typedef struct
|
|
{
|
|
bool exists;
|
|
int generation;
|
|
double elapsed_time;
|
|
|
|
AStarPath path;
|
|
} PathCache;
|
|
|
|
typedef struct
|
|
{
|
|
int generation;
|
|
int index;
|
|
} PathCacheHandle;
|
|
|
|
typedef struct
|
|
{
|
|
bool is_reference;
|
|
EntityRef ref;
|
|
Vec2 pos;
|
|
} Target;
|
|
|
|
typedef struct Entity
|
|
{
|
|
bool exists;
|
|
bool destroy;
|
|
int generation;
|
|
|
|
// fields for all gs.entities
|
|
Vec2 pos;
|
|
Vec2 vel; // only used sometimes, like in old man and bullet
|
|
float damage; // at 1.0, dead! zero initialized
|
|
bool facing_left;
|
|
double dead_time;
|
|
bool dead;
|
|
|
|
// npcs and player
|
|
BUFF(ItemKind, 32) held_items;
|
|
|
|
// props
|
|
bool is_prop;
|
|
PropKind prop_kind;
|
|
|
|
// items
|
|
bool is_item;
|
|
bool held_by_player;
|
|
ItemKind item_kind;
|
|
|
|
// npcs
|
|
bool is_npc;
|
|
bool being_hovered;
|
|
bool perceptions_dirty;
|
|
|
|
#ifdef DESKTOP
|
|
int times_talked_to; // for better mocked response string
|
|
#endif
|
|
|
|
BUFF(Memory, REMEMBERED_MEMORIES) memories;
|
|
bool direction_of_spiral_pattern;
|
|
float dialog_panel_opacity;
|
|
double characters_said;
|
|
NPCPlayerStanding standing;
|
|
NpcKind npc_kind;
|
|
PathCacheHandle cached_path;
|
|
#ifdef WEB
|
|
int gen_request_id;
|
|
#endif
|
|
bool walking;
|
|
double shotgun_timer;
|
|
bool moved;
|
|
Vec2 target_goto;
|
|
// only for skeleton npc
|
|
double swing_timer;
|
|
|
|
// character
|
|
bool is_character;
|
|
bool knighted;
|
|
bool in_conversation_mode;
|
|
Vec2 to_throw_direction;
|
|
|
|
BUFF(Vec2, 8) position_history; // so npcs can follow behind the player
|
|
|
|
CharacterState state;
|
|
EntityRef talking_to;
|
|
bool is_rolling; // can only roll in idle or walk states
|
|
double time_not_rolling; // for cooldown for roll, so you can't just hold it and be invincible
|
|
|
|
// so doesn't change animations while time is stopped
|
|
AnimKind cur_animation;
|
|
float anim_change_timer;
|
|
} Entity;
|
|
|
|
bool npc_is_knight_sprite(Entity *it)
|
|
{
|
|
return it->is_npc && (it->npc_kind == NPC_TheGuard || it->npc_kind == NPC_Edeline || it->npc_kind == NPC_TheKing ||
|
|
it->npc_kind == NPC_TheBlacksmith
|
|
|| it->npc_kind == NPC_Red
|
|
|| it->npc_kind == NPC_Blue
|
|
|| it->npc_kind == NPC_Davis
|
|
);
|
|
}
|
|
|
|
bool npc_is_skeleton(Entity *it)
|
|
{
|
|
return it->is_npc && (it->npc_kind == NPC_MikeSkeleton);
|
|
}
|
|
|
|
float entity_max_damage(Entity *e)
|
|
{
|
|
if (e->is_npc && npc_is_skeleton(e))
|
|
{
|
|
return 2.0f;
|
|
}
|
|
else
|
|
{
|
|
return 1.0f;
|
|
}
|
|
}
|
|
|
|
bool npc_attacks_with_sword(Entity *it)
|
|
{
|
|
return npc_is_skeleton(it);
|
|
}
|
|
|
|
bool npc_attacks_with_shotgun(Entity *it)
|
|
{
|
|
return it->is_npc && (it->npc_kind == NPC_OldMan);
|
|
}
|
|
|
|
|
|
typedef BUFF(ActionKind, 8) AvailableActions;
|
|
|
|
void fill_available_actions(Entity *it, AvailableActions *a)
|
|
{
|
|
*a = (AvailableActions) { 0 };
|
|
BUFF_APPEND(a, ACT_none);
|
|
|
|
if(it->held_items.cur_index > 0)
|
|
{
|
|
BUFF_APPEND(a, ACT_give_item);
|
|
}
|
|
|
|
if (it->npc_kind == NPC_TheKing)
|
|
{
|
|
BUFF_APPEND(a, ACT_knights_player);
|
|
}
|
|
|
|
if (it->npc_kind == NPC_GodRock)
|
|
{
|
|
BUFF_APPEND(a, ACT_heals_player);
|
|
}
|
|
else
|
|
{
|
|
if (it->standing == STANDING_INDIFFERENT)
|
|
{
|
|
BUFF_APPEND(a, ACT_fights_player);
|
|
BUFF_APPEND(a, ACT_joins_player);
|
|
}
|
|
else if (it->standing == STANDING_JOINED)
|
|
{
|
|
BUFF_APPEND(a, ACT_leaves_player);
|
|
BUFF_APPEND(a, ACT_fights_player);
|
|
}
|
|
else if (it->standing == STANDING_FIGHTING)
|
|
{
|
|
BUFF_APPEND(a, ACT_stops_fighting_player);
|
|
}
|
|
if (npc_is_knight_sprite(it))
|
|
{
|
|
BUFF_APPEND(a, ACT_strikes_air);
|
|
}
|
|
if (it->npc_kind == NPC_TheGuard)
|
|
{
|
|
if (!it->moved)
|
|
{
|
|
BUFF_APPEND(a, ACT_allows_player_to_pass);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#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)
|
|
|
|
bool npc_does_dialog(Entity *it)
|
|
{
|
|
return it->npc_kind < ARRLEN(characters);
|
|
}
|
|
|
|
typedef enum
|
|
{
|
|
MSG_SYSTEM,
|
|
MSG_USER,
|
|
MSG_ASSISTANT,
|
|
} MessageType;
|
|
|
|
// for no trailing comma just trim the last character
|
|
MD_String8 make_json_node(MD_Arena *arena, MessageType type, MD_String8 content)
|
|
{
|
|
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
|
|
|
|
const char *type_str = 0;
|
|
if (type == MSG_SYSTEM)
|
|
type_str = "system";
|
|
else if (type == MSG_USER)
|
|
type_str = "user";
|
|
else if (type == MSG_ASSISTANT)
|
|
type_str = "assistant";
|
|
assert(type_str);
|
|
|
|
MD_String8 escaped = escape_for_json(scratch.arena, content);
|
|
MD_String8 to_return = MD_S8Fmt(arena, "{\"type\": \"%s\", \"content\": \"%.*s\"},", type_str, MD_S8VArg(escaped));
|
|
MD_ReleaseScratch(scratch);
|
|
|
|
return to_return;
|
|
}
|
|
|
|
MD_String8List held_item_strings(MD_Arena *arena, Entity *e)
|
|
{
|
|
MD_String8List to_return = {0};
|
|
BUFF_ITER(ItemKind, &e->held_items)
|
|
{
|
|
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.argument.item_to_give >= 0 && a.argument.item_to_give < ARRLEN(items));
|
|
bool has_it = false;
|
|
BUFF_ITER(ItemKind, &from->held_items)
|
|
{
|
|
if(*it == a.argument.item_to_give)
|
|
{
|
|
has_it = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!has_it)
|
|
{
|
|
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.argument.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.");
|
|
}
|
|
}
|
|
|
|
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 *e)
|
|
{
|
|
assert(e->is_npc);
|
|
assert(e->npc_kind < ARRLEN(characters));
|
|
|
|
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
|
|
|
|
MD_String8List list = {0};
|
|
|
|
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)));
|
|
|
|
ItemKind last_holding = ITEM_none;
|
|
BUFF_ITER(Memory, &e->memories)
|
|
{
|
|
MessageType sent_type = -1;
|
|
MD_String8 current_string = (MD_String8){0};
|
|
|
|
if(it->is_error)
|
|
{
|
|
sent_type = MSG_SYSTEM;
|
|
current_string = MD_S8Fmt(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech);
|
|
}
|
|
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 speech = MD_S8(it->speech, it->speech_length);
|
|
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
}
|
|
|
|
if(actions[it->action_taken].takes_argument)
|
|
{
|
|
if(it->action_taken == ACT_give_item)
|
|
{
|
|
current_string = MD_S8Fmt(scratch.arena, "%.*s ACT_%s(ITEM_%s) \"%.*s\"", MD_S8VArg(context_string), actions[it->action_taken].name, items[it->action_argument.item_to_give].enum_name, it->speech_length, it->speech);
|
|
}
|
|
else
|
|
{
|
|
assert(false); // don't know how to serialize this action with argument into text
|
|
}
|
|
}
|
|
else
|
|
{
|
|
current_string = MD_S8Fmt(scratch.arena, "%.*s ACT_%s \"%.*s\"", MD_S8VArg(context_string), actions[it->action_taken].name, it->speech_length, it->speech);
|
|
}
|
|
}
|
|
|
|
assert(sent_type != -1);
|
|
assert(current_string.size > 0);
|
|
|
|
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, sent_type, current_string));
|
|
}
|
|
const char *standing_string = 0;
|
|
{
|
|
if (e->standing == STANDING_INDIFFERENT)
|
|
{
|
|
standing_string = "The NPC is indifferent towards the player.";
|
|
}
|
|
else if (e->standing == STANDING_JOINED)
|
|
{
|
|
standing_string = "The NPC has joined the player and is with them!";
|
|
}
|
|
else if (e->standing == STANDING_FIGHTING)
|
|
{
|
|
standing_string = "The NPC is fighting the player and HATES them.";
|
|
}
|
|
}
|
|
assert(standing_string);
|
|
|
|
MD_String8List latest_state = {0};
|
|
|
|
if(e->held_items.cur_index > 0)
|
|
{
|
|
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
|
|
{
|
|
MD_S8ListPushFmt(scratch.arena, &latest_state, "\nThe NPC doesn't have any items.\n");
|
|
}
|
|
|
|
AvailableActions available = { 0 };
|
|
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(ActionKind, &available, i)
|
|
{
|
|
if (i == available.cur_index - 1)
|
|
{
|
|
MD_S8ListPushFmt(scratch.arena, &latest_state, "ACT_%s", actions[*it].name);
|
|
}
|
|
else
|
|
{
|
|
MD_S8ListPushFmt(scratch.arena, &latest_state, "ACT_%s, ", actions[*it].name);
|
|
}
|
|
}
|
|
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)); // 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);
|
|
|
|
return to_return;
|
|
}
|
|
|
|
// 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, Action *out)
|
|
{
|
|
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
|
|
|
|
MD_String8 error_message = {0};
|
|
|
|
*out = (Action) { 0 };
|
|
|
|
MD_String8 action_prefix = MD_S8Lit("ACT_");
|
|
MD_u64 act_pos = MD_S8FindSubstring(sentence, action_prefix, 0, 0);
|
|
if(act_pos == sentence.size)
|
|
{
|
|
error_message = MD_S8Fmt(arena, "Couldn't find beginning of action '%.*s' in sentence", MD_S8VArg(action_prefix));
|
|
goto endofparsing;
|
|
}
|
|
|
|
MD_u64 beginning_of_action = act_pos + action_prefix.size;
|
|
|
|
MD_u64 parenth = MD_S8FindSubstring(sentence, MD_S8Lit("("), beginning_of_action, 0);
|
|
MD_u64 space = MD_S8FindSubstring(sentence, MD_S8Lit(" "), beginning_of_action, 0);
|
|
|
|
MD_u64 end_of_action = parenth < space ? parenth : space;
|
|
if(end_of_action == sentence.size)
|
|
{
|
|
error_message = MD_S8Fmt(arena, "'%.*s' prefix doesn't end with a ' ' or a '(', like how 'ACT_none ' or 'ACT_give_item(ITEM_sandwich) does.", MD_S8VArg(action_prefix));
|
|
goto endofparsing;
|
|
}
|
|
MD_String8 given_action_string = MD_S8Substring(sentence, beginning_of_action, end_of_action);
|
|
|
|
AvailableActions available = { 0 };
|
|
fill_available_actions(e, &available);
|
|
bool found_action = false;
|
|
MD_String8List given_action_strings = {0};
|
|
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->kind = *it;
|
|
}
|
|
}
|
|
|
|
if(!found_action)
|
|
{
|
|
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, "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->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->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->kind].name)), should_be_paren);
|
|
goto endofparsing;
|
|
}
|
|
MD_u64 beginning_of_arg = end_of_action;
|
|
MD_u64 end_of_arg = MD_S8FindSubstring(sentence, MD_S8Lit(")"), beginning_of_arg, 0);
|
|
if(end_of_arg == sentence.size)
|
|
{
|
|
error_message = MD_S8Fmt(arena, "Expected ')' to close the action string's argument, but couldn't find one");
|
|
goto endofparsing;
|
|
}
|
|
|
|
MD_String8 argument = MD_S8Substring(sentence, beginning_of_arg, end_of_arg);
|
|
start_looking_for_quote = end_of_arg + 1;
|
|
|
|
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);
|
|
if(item_prefix_begin == argument.size)
|
|
{
|
|
error_message = MD_S8Fmt(arena, "Expected prefix 'ITEM_' before the give_item action, but found '%.*s' instead", MD_S8VArg(argument));
|
|
goto endofparsing;
|
|
}
|
|
MD_u64 item_name_begin = item_prefix_begin + item_prefix.size;
|
|
MD_u64 item_name_end = argument.size;
|
|
|
|
MD_String8 item_name = MD_S8Substring(argument, item_name_begin, item_name_end);
|
|
|
|
bool item_found = false;
|
|
MD_String8List possible_item_strings = {0};
|
|
BUFF_ITER(ItemKind, &e->held_items)
|
|
{
|
|
MD_String8 item_str = MD_S8CString(items[*it].enum_name);
|
|
MD_S8ListPush(scratch.arena, &possible_item_strings, item_str);
|
|
if(MD_S8Match(item_str, item_name, 0))
|
|
{
|
|
item_found = true;
|
|
out->argument.item_to_give = *it;
|
|
}
|
|
}
|
|
|
|
if(!item_found)
|
|
{
|
|
MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")};
|
|
MD_String8 possible_items_str = MD_S8ListJoin(scratch.arena, possible_item_strings, &join);
|
|
error_message = MD_S8Fmt(arena, "Item string given is '%.*s', but available items to give are: [%.*s]", MD_S8VArg(item_name), MD_S8VArg(possible_items_str));
|
|
goto endofparsing;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
assert(false); // if action takes an argument but we don't handle it, this should be a terrible crash
|
|
}
|
|
}
|
|
|
|
if(start_looking_for_quote >= sentence.size)
|
|
{
|
|
error_message = MD_S8Fmt(arena, "Wanted to start looking for quote for NPC speech, but sentence ended prematurely");
|
|
goto endofparsing;
|
|
}
|
|
|
|
MD_u64 beginning_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), 0, 0);
|
|
MD_u64 end_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), beginning_of_speech + 1, 0);
|
|
|
|
if(beginning_of_speech == sentence.size || end_of_speech == sentence.size)
|
|
{
|
|
error_message = MD_S8Fmt(arena, "Expected dialog enclosed by two quotes (i.e \"My name is greg\") after the action, but couldn't find anything!");
|
|
goto endofparsing;
|
|
}
|
|
|
|
MD_String8 speech = MD_S8Substring(sentence, beginning_of_speech + 1, end_of_speech);
|
|
|
|
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->speech));
|
|
goto endofparsing;
|
|
}
|
|
|
|
memcpy(out->speech, speech.str, speech.size);
|
|
out->speech_length = (int)speech.size;
|
|
|
|
endofparsing:
|
|
MD_ReleaseScratch(scratch);
|
|
return error_message;
|
|
}
|