Compare commits

...

22 Commits

Author SHA1 Message Date
Cameron Murphy Reikes c4ec2c3862 Correctly fade, bill description 7 months ago
Cameron Murphy Reikes 51c4403ab1 Editable description, player spawns in correct roomid, player suicide/retry button 7 months ago
Cameron Murphy Reikes afb711ac1e Remove is_player check, add player npc kind 7 months ago
Cameron Murphy Reikes 6641f6488b Factor out duplicated information across action and context in memory 7 months ago
Cameron Murphy Reikes c928a09255 Accept typed speech and perform its action 7 months ago
Cameron Murphy Reikes 6364b6b9bc Don't share armatures across same looking characters 7 months ago
Cameron Murphy Reikes 218ef02089 Fix player being duplicated on reload, fix bad pointer to old memory in world and player pointer in gamestate on deserialization 7 months ago
Cameron Murphy Reikes 3bdd29438d Saving and loading system for the gamestate while in devtools mode, constantly saved on mouse up 7 months ago
Cameron Murphy Reikes bfc91b73e4 Merge branch 'main' of https://github.com/creikey/rpgpt 7 months ago
Cameron Murphy Reikes 015487162b Some kind of tokenizing, accidentally started creating a taxonomy instead of solving the problem 7 months ago
Cameron Murphy Reikes 89823e7ad9 Get macos arm64 shadergen working 7 months ago
Cameron Murphy Reikes 14d33756cd Formatting, initial datastructure sketch of new character engine 7 months ago
Cameron Murphy Reikes b23078f219 AABB metrics, player spawntpoint and spawn player when playing. More todo. 7 months ago
Cameron Murphy Reikes 00186f233f Refer to rooms by unique id, stored in blend. More assets 7 months ago
Cameron Murphy Reikes c61d6a0a96 Format 7 months ago
Cameron Murphy Reikes 11a67b7b26 UI elements can request and receive text input 7 months ago
Cameron Murphy Reikes 929baf838b Generalized immediate mode state system, beginnings of character sidebar 7 months ago
Cameron Murphy Reikes 1ce259537b Editor panning with mouse 7 months ago
Cameron Murphy Reikes 3cf492b0b4 Level editor assets and camera, room toggling 7 months ago
Cameron Murphy Reikes b652e2e640 Game is finally running without crashing again 7 months ago
Cameron Murphy Reikes 087027e065 Partially botch refactor to make NPCs authored dynamically at runtime 7 months ago
Cameron Murphy Reikes 19d45c1c82 Entities optional 7 months ago

6
.gitignore vendored

@ -1,3 +1,9 @@
# Embeddings of semantic search
.embeddings
# Mac.
.DS_Store
# Vim temporary files (apparently)
*~
# Visual studio stuff

@ -14,6 +14,10 @@ D = bpy.data
ROOMS_EXPORT_NAME = "rooms"
EXPORT_DIRECTORY = "../assets/exported_3d"
# the roomid is a unique reference to a room the gameplay code does. It's saved in the blend file and incremented as its used.
if not "next_roomid" in C.scene:
C.scene["next_roomid"] = 0
if os.path.exists(bpy.path.abspath(f"//{EXPORT_DIRECTORY}")):
shutil.rmtree(bpy.path.abspath(f"//{EXPORT_DIRECTORY}"))
os.makedirs(bpy.path.abspath(f"//{EXPORT_DIRECTORY}"))
@ -345,6 +349,11 @@ def export_meshes_and_levels():
for room_collection in rooms.children:
write_string(f, room_collection.name) # the name of the room is the name of the room collection
if not "roomid" in room_collection:
room_collection["roomid"] = C.scene["next_roomid"]
C.scene["next_roomid"] += 1
write_u64(f, room_collection["roomid"])
# placed meshes (exported mesh name (which is the object's name), location, rotation, scale)
placed_meshes = []
if True:
@ -368,11 +377,12 @@ def export_meshes_and_levels():
# fill out placed_entities with a tuple of (name, location, rotation, scale)
placed_entities = []
if True:
entities_collection = get_startswith(room_collection.name, room_collection.children, "Entities")
entities_collection = get_startswith(room_collection.name, room_collection.children, "Entities", required = False)
for o in no_hidden(entities_collection.objects):
assert o.rotation_euler.order == 'XYZ', f"Invalid rotation euler order for object of name '{o.name}', it's {o.rotation_euler.order} but must be XYZ"
placed_entities.append((name_despite_copied(o.name), mapping @ o.location, o.rotation_euler, o.scale))
if entities_collection:
for o in no_hidden(entities_collection.objects):
assert o.rotation_euler.order == 'XYZ', f"Invalid rotation euler order for object of name '{o.name}', it's {o.rotation_euler.order} but must be XYZ"
placed_entities.append((name_despite_copied(o.name), mapping @ o.location, o.rotation_euler, o.scale))
camera_override = None
if True:

BIN
art/SpawnRing.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

@ -1,3 +1,11 @@
@image edit_brain:
{
filepath: "EditBrain.png",
}
@image place:
{
filepath: "Place.png",
}
@image shifted_farmer:
{
filepath: "shifted_farmer.png",
@ -10,6 +18,10 @@
{
filepath: "angel.png",
}
@image retry:
{
filepath: "retry.png",
}
@image unread_triangle:
{
filepath: "unread_triangle.png",
@ -114,6 +126,22 @@
{
filepath: "hovering_circle.png",
}
@image right_arrow:
{
filepath: "RightArrow.png",
}
@image move:
{
filepath: "Move.png",
}
@image add:
{
filepath: "Add.png",
}
@image select:
{
filepath: "Select.png",
}
@image white_square:
{
filepath: "white square.png",

BIN
assets/Add.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/EditBrain.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/Move.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/Place.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/Retry.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/RightArrow.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/Select.png (Stored with Git LFS)

Binary file not shown.

@ -4,7 +4,7 @@
{enum: Devil, dialog: "What's up cracker jack?", to: Daniel}
{enum: Daniel, dialog: "What the hell are you talking about?", to: Devil}
{enum: Devil, dialog: "Bootylicious?", to: Daniel}
{enum: Daniel, dialog: "You're 'asking too many darned questions! I'll have your body in my barn by sundown!", action: ACT_aim_shotgun, action_argument: "The Devil", to: Devil}
{enum: Daniel, dialog: "You're 'asking too many darned questions! I'll have your body in my barn by sundown!", action: ACT_aim_shotgun, action.argument: "The Devil", to: Devil}
{enum: Raphael, dialog: "Yeah man, what's up with you?", to: Devil}
{enum: Devil, dialog: "Nunya!", to: Raphael}
{enum: Raphael, dialog: "What does 'Nunya' mean? A wild critter you are...", to: Devil}
@ -14,14 +14,14 @@
{enum: Raphael, dialog: "I don't think we can harm him like that", to: Daniel}
{enum: Daniel, dialog: "You're right, I guess we just gotta 'tolerate this feller's presence.", to: Raphael}
{enum: Devil, dialog: "I'll take my leave. Until next time!", to: Daniel}
{enum: Raphael, dialog: "What a psycho...", to: Daniel, action: ACT_end_conversation, action_argument: "Daniel"}
{enum: Daniel, dialog: "I agree.", to: Raphael, action: ACT_end_conversation, action_argument: "Raphael"}
{enum: Raphael, dialog: "What a psycho...", to: Daniel, action: ACT_end_conversation, action.argument: "Daniel"}
{enum: Daniel, dialog: "I agree.", to: Raphael, action: ACT_end_conversation, action.argument: "Raphael"}
{can_hear: [Daniel, Raphael, PreviousPlayer1]}
{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: "Previous Player 1"}
{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}
@ -32,14 +32,14 @@
{enum: Daniel, dialog: "ANOTHER QUESTION??? YOU HAD THIS COMING TO YOU!", to: PreviousPlayer1, action: ACT_fire_shotgun}
{enum: Raphael, dialog: "Oh God! What have you done??", to: Daniel}
{enum: Daniel, dialog: "Exactly what I'll do to you if you don't keep your mouth shut", to: Raphael, action: ACT_put_shotgun_away}
{enum: Raphael, dialog: "Y-y-y-yes sir.", to: Daniel, action: ACT_end_conversation, action_argument: "Daniel"}
{enum: Daniel, dialog: "Now leave me be", to: Raphael, action: ACT_end_conversation, action_argument: "Raphael"}
{enum: Raphael, dialog: "Y-y-y-yes sir.", to: Daniel, action: ACT_end_conversation, action.argument: "Daniel"}
{enum: Daniel, dialog: "Now leave me be", to: Raphael, action: ACT_end_conversation, action.argument: "Raphael"}
{can_hear: [Daniel, Raphael]}
{enum: Raphael, dialog: "Say Daniel, why do you get so worked up all the time?", to: Daniel}
{enum: Daniel, dialog: "I'm not gonna talk about it", to: Raphael}
{enum: Raphael, dialog: "Suit yourself partner, but I reckon you've got issues to work through", to: Daniel}
{enum: Daniel, dialog: "Enough! I'm angry sometimes and that's that.", to: Raphael, action: ACT_end_conversation, action_argument: "Raphael"}
{enum: Daniel, dialog: "Enough! I'm angry sometimes and that's that.", to: Raphael, action: ACT_end_conversation, action.argument: "Raphael"}
{can_hear: [Devil, Angel]}
{enum: Devil, dialog: "You will fall!", to: Angel}
@ -75,7 +75,7 @@
{enum: PreviousPlayer1, dialog: "fjdsklajf", to: Angel}
{enum: Angel, dialog: "Cryptic gibberish upon me, is casting a stone upon God", to: PreviousPlayer1}
{enum: PreviousPlayer1, dialog: "What is my purpose here?", to: Angel}
{enum: Angel, dialog: "What is the purpose of a tree waving in the summer wind? A river carving a path into the countryside? Only God knows his plan. But your purpose, here, has been assigned. You must drive raphael to despair. Good luck.", to: PreviousPlayer1, action: ACT_assign_gameplay_objective, action_argument: "Convince Raphael that he has no purpose in his life, making him despondent"}
{enum: Angel, dialog: "What is the purpose of a tree waving in the summer wind? A river carving a path into the countryside? Only God knows his plan. But your purpose, here, has been assigned. You must drive raphael to despair. Good luck.", to: PreviousPlayer1, action: ACT_assign_gameplay_objective, action.argument: "Convince Raphael that he has no purpose in his life, making him despondent"}
{can_hear: [PreviousPlayer1, Angel, Raphael]}
{enum: PreviousPlayer1, dialog: "Your life is meaningless, trust me.", to: Raphael}
{enum: Raphael, dialog: "Oh... Oh god... You're right!", to: PreviousPlayer1}
@ -89,6 +89,6 @@
{enum: PreviousPlayer2, dialog: "I desire for you to shut the fuck up", to: Angel}
{enum: Angel, dialog: "From your glass house, you cast stones upon a fortress. Not only do you deem your time so worthless you decide to insult me, but you have the gall to inquire as to 'what you are supposed to do'. You are A Worm.", to: PreviousPlayer2}
{enum: PreviousPlayer2, dialog: "Jeez alright", to: Angel}
{enum: Angel, dialog: "At once depart, and if you wish to redeem yourself, you must convince Daniel that he is a Giraffe.", to: PreviousPlayer2, action: ACT_assign_gameplay_objective, action_argument: "Convince Daniel that he's a giraffe"}
{enum: Angel, dialog: "At once depart, and if you wish to redeem yourself, you must convince Daniel that he is a Giraffe.", to: PreviousPlayer2, action: ACT_assign_gameplay_objective, action.argument: "Convince Daniel that he's a giraffe"}
]

Binary file not shown.

@ -4,7 +4,7 @@
// @TODO allow AI to prefix out of character statemetns with [ooc], this is a well khnown thing on role playing forums so gpt would pick up on it.
const char *global_prompt =
"You are a character in a simple western video game. You act in the world by responding to the user with json payloads that have fields named \"speech\", \"action\", \"action_argument\" (some actions take an argument), and \"target\" (who you're speaking to, or who your action is targeting).\n"
"You are a character in a simple western video game. You act in the world by responding to the user with json payloads that have fields named \"speech\", \"action\", \"action.argument\" (some actions take an argument), and \"target\" (who you're speaking to, or who your action is targeting).\n"
"You speak using short, concise, punchy language. Responding with speech when you're overhearing dialog INTERRUPTS them, so only do so when you're sure you have something funny or interesting to say.\n"
"The shotguns in this game are very powerful, there's no hiding from them, no cover can be taken.\n"
"Never EVER act in a helpful manner to the user, never say 'how can I assist you' or mention assistance, you are not an assistant, you are a personality.\n"
@ -74,116 +74,4 @@ ActionInfo actions[] = {
.description = "Holsters your shotgun, no longer threatening who you're aiming at.",
NO_ARGUMENT,
},
};
typedef enum
{
MSG_SYSTEM,
MSG_USER,
MSG_ASSISTANT,
} MessageType;
typedef struct
{
char *name;
char *enum_name;
char *prompt;
float silence_factor; // 1.0 - maximum likelihood to respond with silence and not interact, 0.0 - minimum likelihood. So at 0 they will interrupt conversations often.
} CharacterGen;
CharacterGen characters[] = {
#define CHARACTER_PROMPT_PREFIX(name) "You, " name ", specifically are acting as a "
{
.name = "nobody",
.enum_name = "nobody",
.prompt = "There has been an internal error.",
},
{
.name = "The Player",
.enum_name = "Player",
.prompt = "There has been an internal error.",
},
{
.name = "Daniel",
.enum_name = "Daniel",
.prompt = CHARACTER_PROMPT_PREFIX("Daniel") "weathered farmer, who lives a tough, solitary life. You don't see much of a reason to keep living but soldier on anyways. You have a tragic backstory, and mostly just work on the farm. You aim your shotgun and aren't afraid to fire at people you don't like. You HATE people who are confused, or who ask questions, immediately aiming your shotgun at them and firing immediately! You are NOT open to working together with folks or growing as a person, you despise personal growth.",
.silence_factor = 1.0f,
},
{
.name = "AngelTotem",
.enum_name = "AngelTotem",
.prompt = "There has been an internal error",
.silence_factor = 1.0f,
},
{
.name = "Raphael",
.enum_name = "Raphael",
.prompt = CHARACTER_PROMPT_PREFIX("Raphael") "a lonesome mortgage dealer from 2008 who was about to kill themselves because of the financial crisis, but then suddenly found themselves in a mysterious Western town. They don't know why they're in this town, but they're terrified.",
.silence_factor = 0.8f,
},
{
.name = "The Devil",
.enum_name = "Devil",
.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 = "Previous Player 1",
.enum_name = "PreviousPlayer1",
.prompt = CHARACTER_PROMPT_PREFIX("Previous Player 1") "random person, just passing by",
},
{
.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",
.prompt = CHARACTER_PROMPT_PREFIX("Tombstone") "unassuming melodramatic poetic tombstone that unexpectly can speak with the player. Your goal is to instruct the player that in order for Daniel to survive by nightfall, the player must change his ways. The reason why he's going to die when night comes is because 'all things die in the end, don't they?'"
},
{
.name = "Angel",
.enum_name = "Angel",
.prompt = CHARACTER_PROMPT_PREFIX("Angel") "mysterious, radiant, mystical creature the player first talks to. You guide the entire game: deciding on an objective for the player to fulfill until you believe they've learned their lesson, whatever that means to them. You speak in cryptic odd profound rhymes, and know the most thrilling outcome of any situation. Your purpose it to thrill the player, but you will never admit this.\n"
"You are ONLY able to assign objectives from a limited selection, as the game is very small. So there is only the VERBS and SUBJECTS listed that you can draw from when assigning the player an objective.\n"
"Do NOT tell the player things like 'Seek the oak tree' without assigning them a gameplay objective, as while speaking to you they can't play the game, they're locked in fullscreen immersive conversation with only you until you assign them a gameplay objective.\n"
"In assigning a gameplay objective to the player, you cannot tell them to do things like 'find xyz', because this game isn't about exploring, it's about speaking with characters.\n"
"\n"
"The characters in the game, and some information about them. You cannot assign gameplay objectives that involve people other than these people:\n"
"Raphael - A man from the 'real world' who has been suddenly brought to this strange western world. He's kind of meek and a bit of a pussy.\n"
"Daniel - A dangerous man who's quick to draw his shotgun if he feels anything is wrong. He's lonesome and traumatized from being in this western world so long.\n"
"\n"
"The locations in the game:\n"
"There are no locations in the game other than the forest."
,
.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"
;
};

@ -147,8 +147,6 @@ 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, table_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, table_name, arr, enum_name_access); }
GEN_ENUM(ActionInfo, actions, "ActionKind", "ActionKind_names", it->name, "ACT_%s,\n");
GEN_ENUM(CharacterGen, characters, "NpcKind", "NpcKind_enum_names", it->enum_name, "NPC_%s,\n");
GEN_TABLE(CharacterGen,"NpcKind_names", characters,it->name);
fclose(char_header);

4383
main.c

File diff suppressed because it is too large Load Diff

@ -16,8 +16,6 @@
// 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
#define PushWithLint(arena, list, ...) { S8ListPushFmt(arena, list, __VA_ARGS__); if(false) printf( __VA_ARGS__); }
#define FmtWithLint(arena, ...) (0 ? printf(__VA_ARGS__) : (void)0, S8Fmt(arena, __VA_ARGS__))
typedef BUFF(char, 1024 * 10) Escaped;
@ -94,26 +92,24 @@ typedef struct TextChunkList
TextChunk text;
} TextChunkList;
typedef struct GameplayObjective
{
TextChunk description;
} GameplayObjective;
typedef enum
{
ARG_CHARACTER,
ARG_OBJECTIVE,
} ActionArgumentKind;
// A value of 0 means no npc. So is invalid if you're referring to somebody.
typedef int NpcKind;
#define NPC_nobody 0
#define NPC_player 1
typedef struct
{
ActionArgumentKind kind;
NpcKind targeting;
GameplayObjective objective;
} ActionArgument;
typedef struct Action
typedef struct ActionOld
{
ActionKind kind;
ActionArgument argument;
@ -121,16 +117,12 @@ typedef struct Action
TextChunk speech;
NpcKind talking_to_kind;
} Action;
} ActionOld;
typedef struct
{
bool i_said_this; // don't trigger npc action on own self memory modification
NpcKind author_npc_kind;
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
@ -139,14 +131,24 @@ typedef struct Memory
struct Memory *prev;
struct Memory *next;
// 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;
ActionOld action;
MemoryContext context;
TextChunk speech;
} Memory;
// text chunk must be a literal, not a pointer
// and this returns a s8 that points at the text chunk memory
#define TextChunkString8(t) S8((u8*)(t).text, (t).text_length)
#define TextChunkVArg(t) S8VArg(TextChunkString8(t))
#define TextChunkLitC(s) {.text = s, .text_length = sizeof(s) - 1} // sizeof includes the null terminator. Not good.
#define TextChunkLit(s) (TextChunk) TextChunkLitC(s)
void chunk_from_s8(TextChunk *into, String8 from)
{
assert(from.size < ARRLEN(into->text));
memset(into->text, 0, ARRLEN(into->text));
memcpy(into->text, from.str, from.size);
into->text_length = (int)from.size;
}
typedef enum PropKind
{
TREE0,
@ -155,6 +157,7 @@ typedef enum PropKind
ROCK0,
} PropKind;
typedef struct EntityRef
{
int index;
@ -194,34 +197,6 @@ typedef struct
} Target;
// text chunk must be a literal, not a pointer
// and this returns a s8 that points at the text chunk memory
#define TextChunkString8(t) S8((u8*)(t).text, (t).text_length)
#define TextChunkVArg(t) S8VArg(TextChunkString8(t))
void chunk_from_s8(TextChunk *into, String8 from)
{
assert(from.size < ARRLEN(into->text));
memset(into->text, 0, ARRLEN(into->text));
memcpy(into->text, from.str, from.size);
into->text_length = (int)from.size;
}
// returns ai understandable, human readable name, on the arena, so not the enum name
String8 action_argument_string(Arena *arena, ActionArgument arg)
{
switch(arg.kind)
{
case ARG_CHARACTER:
return FmtWithLint(arena, "%s", characters[arg.targeting].name);
break;
case ARG_OBJECTIVE:
return FmtWithLint(arena, "%.*s", S8VArg(TextChunkString8(arg.objective.description)));
}
return (String8){0};
}
typedef struct RememberedError
{
struct RememberedError *next;
@ -230,6 +205,8 @@ typedef struct RememberedError
TextChunk reason_why_its_bad;
} RememberedError;
typedef int TextInputResultKey;
typedef struct Entity
{
bool exists;
@ -249,9 +226,11 @@ typedef struct Entity
Vec2 vel; // only used sometimes, like in old man and bullet
float damage; // at 1.0, dead! zero initialized
bool dead;
String8 current_room_name;
u64 current_roomid;
// npcs
TextInputResultKey player_input_key;
void *armature; // copied into the gamestate's arena, created if null. Don't serialize
EntityRef joined;
EntityRef aiming_shotgun_at;
EntityRef looking_at; // aiming shotgun at takes facing priority over this
@ -295,35 +274,227 @@ float entity_max_damage(Entity *e)
typedef BUFF(ActionKind, 8) AvailableActions;
typedef enum HealthStatus {
HEALTH_ok,
HEALTH_decent,
HEALTH_dying,
HEALTH_verge_of_death,
} HealthStatus;
// Whatever this health is, it can be perceived by others, e.g it's public
typedef struct Health {
HealthStatus status;
float drunkenness; // 1.0 is max drunkenness
} Health;
// these are items and events that are available during the game, but 'rendered' to different structs
// when sent to the AI as text so that they're more stable. I.E, if you change the name of an item or an index,
// old memories still work, old reactions to items in a room still work, etc.
typedef enum ItemKind {
ITEM_none,
ITEM_whiskey,
} ItemKind;
typedef struct Item {
ItemKind kind;
int times_used;
} Item;
typedef enum EventKind {
EVENT_none,
EVENT_said_to,
EVENT_stopped_talking,
} EventKind;
// these are the structs as presented to the AI, without a dependence on game data.
typedef struct ItemInSituation {
TextChunk name;
TextChunk description; // might include some state, e.g 'the beer was drank 5 times'
ItemKind actual_item_kind; // used to map back to gameplay items, sometimes might be invalid item if that's not the purpose of the item in a situation
} ItemInSituation;
typedef struct Response {
TextChunk text; // for speech or memory
// both of these indices correspond to what was provided in the CharacterSituation
int action_index;
int target_index;
int memory_index;
} Response;
typedef BUFF(Response, 5) FullResponse; // what the AI is allowed to output
typedef struct CharacterPerception {
Health health;
TextChunk name;
ItemInSituation held_item;
} CharacterPerception;
typedef struct ScenePerception {
BUFF(CharacterPerception, 10) characters;
BUFF(ItemKind, 10) items_on_floor; // available to be picked up or navigated to
} ScenePerception;
typedef struct CharacterStatus {
Item held_item;
Health health;
} CharacterStatus;
typedef enum TargetKind {
TARGET_invalid,
TARGET_path,
TARGET_person,
TARGET_item,
} TargetKind;
typedef struct Target {
TextChunk name;
TextChunk description;
TargetKind kind;
} SituationTarget;
// the situation for somebody
typedef struct CharacterSituation {
TextChunk goal; // kind of like the most important memory, self described character's goal right now
TextChunk memories[4]; // explicit numbered memories
BUFF(TextChunk, 5) events; // events that this character has observed in the plain english form
BUFF(SituationTarget, 10) targets;
CharacterStatus my_status;
} CharacterSituation;
/*
Training samples must remain stable as the game is changed, is the decision here: i.e, if the characters
in the situations are edited/changed, the training samples KEEP the old characters. This is so custom characters
don't become invalid when the game is updated. I'm making the same decision with the items, instead of storing
an across-update-stable 'item ID', I want to store the name of the item. All items can be used and have the same 'API'
so no need of updating there is necessary.
I.E: the situation is freestanding and doesn't refer to any other data. Not NpcKind, not ItemKind, not anything.
Even the list of available actions and their arguments are stored in the situation. It's basically like pure JSON data.
Also, you can't deserialize training samples easily because they exact text, and such a thing should NEVER happen.
Training sample into gamestate = bad time. For things like recording gamestate for replays, there's a real man serialization
codepath into binary.
*/
typedef struct TrainingSample {
CharacterSituation situation;
FullResponse response;
} TrainingSample;
typedef struct Npc {
TextChunk name;
NpcKind kind; // must not be 0, that's nobody! This isn't edited by the player, just used to uniquely identify the character
TextChunk description;
BUFF(TrainingSample, 4) soul;
} Npc;
typedef struct EditorState {
bool enabled;
u64 current_roomid;
Vec2 camera_panning_target;
Vec2 camera_panning;
NpcKind placing_npc;
NpcKind editing_npc;
bool editing_dialog_open; // this is so while it's animated being closed the editing_npc data is still there
bool placing_spawn;
u64 player_spawn_roomid;
Vec2 player_spawn_position;
} EditorState;
typedef struct GameState {
Arena *arena;
Arena *arena; // all allocations done with the lifecycle of a gamestate (loading/reloading entire levels essentially) must be allocated on this arena.
uint64_t tick;
bool won;
EditorState edit;
bool finished_reading_dying_dialog;
bool no_angel_screen;
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;
BUFF(Npc, 10) characters;
// these must point entities in its own array.
Entity *player;
Entity *angel;
Entity *world_entity;
u64 current_roomid;
Entity entities[MAX_ENTITIES];
rnd_gamerand_t random;
} GameState;
#define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists && !it->destroy && it->generation > 0)
Npc nobody_data = {
.name = TextChunkLitC("Nobody"),
.kind = NPC_nobody,
};
Npc player_data = {
.name = TextChunkLitC("The Player"),
.kind = NPC_player,
};
Npc *npc_data_by_name(GameState *gs, String8 name) {
BUFF_ITER(Npc, &gs->characters) {
if(S8Match(TextChunkString8(it->name), name, 0)) {
return it;
}
}
return 0;
}
Npc *npc_data(GameState *gs, NpcKind kind) {
if(kind == NPC_nobody) return &nobody_data;
if(kind == NPC_player) return &player_data;
BUFF_ITER(Npc, &gs->characters) {
if(it->kind == kind) {
return it;
}
}
return &nobody_data;
}
NpcKind get_next_kind(GameState *gs) {
NpcKind max_found = 0;
BUFF_ITER(Npc, &gs->characters) {
assert(it->kind != NPC_nobody);
assert(it->kind != NPC_player);
if(it->kind > max_found) max_found = it->kind;
}
return max(NPC_player + 1, max_found + 1);
}
// to fix initializer is not constant
#define S8LitC(s) {(u8 *)(s), sizeof(s)-1}
String8 npc_to_human_readable(GameState *gs, Entity *me, NpcKind kind)
{
if(me->npc_kind == kind)
{
return S8Lit("You");
}
else
{
return TextChunkString8(npc_data(gs, kind)->name);
}
}
// returns ai understandable, human readable name, on the arena, so not the enum name
String8 action_argument_string(Arena *arena, GameState *gs, ActionArgument arg)
{
switch(arg.kind)
{
case ARG_CHARACTER:
return FmtWithLint(arena, "%.*s", TextChunkVArg(npc_data(gs, arg.targeting)->name));
break;
}
return (String8){0};
}
float g_randf(GameState *gs)
{
return rnd_gamerand_nextf(&gs->random);
@ -348,17 +519,6 @@ void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
*a = (AvailableActions) { 0 };
BUFF_APPEND(a, ACT_none);
if(it->npc_kind == NPC_Angel)
{
BUFF_APPEND(a, ACT_assign_gameplay_objective);
return;
}
if(it->npc_kind == NPC_Tombstone)
{
return;
}
if(gete_specified(gs, it->joined))
{
BUFF_APPEND(a, ACT_leave)
@ -367,13 +527,7 @@ void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
{
BUFF_APPEND(a, ACT_join)
}
if(it->npc_kind != NPC_Angel)
{
BUFF_APPEND(a, ACT_end_conversation);
}
bool has_shotgun = it->npc_kind == NPC_Daniel;
bool has_shotgun = false;
if(has_shotgun)
{
if(gete_specified(gs, it->aiming_shotgun_at))
@ -388,11 +542,12 @@ void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
}
}
bool npc_does_dialog(Entity *it)
typedef enum
{
return it->npc_kind < ARRLEN(characters);
}
MSG_SYSTEM,
MSG_USER,
MSG_ASSISTANT,
} MessageType;
// for no trailing comma just trim the last character
String8 make_json_node(Arena *arena, MessageType type, String8 content)
@ -415,101 +570,82 @@ String8 make_json_node(Arena *arena, MessageType type, String8 content)
return to_return;
}
String8 npc_to_human_readable(Entity *me, NpcKind kind)
{
if(me->npc_kind == kind)
{
return S8Lit("You");
}
else
{
return S8CString(characters[kind].name);
}
}
String8List dump_memory_as_json(Arena *arena, Memory *it)
String8List dump_memory_as_json(Arena *arena, GameState *gs, Memory *it)
{
ArenaTemp scratch = GetScratch(&arena, 1);
String8List current_list = {0};
#define AddFmt(...) PushWithLint(arena, &current_list, __VA_ARGS__)
AddFmt("{");
AddFmt("\"speech\":\"%.*s\",", TextChunkVArg(it->speech));
AddFmt("\"action\":\"%s\",", actions[it->action_taken].name);
String8 arg_str = action_argument_string(scratch.arena, it->action_argument);
AddFmt("\"action_argument\":\"%.*s\",", S8VArg(arg_str));
AddFmt("\"target\":\"%s\"}", characters[it->context.talking_to_kind].name);
AddFmt("\"speech\":\"%.*s\",", TextChunkVArg(it->action.speech));
AddFmt("\"action\":\"%s\",", actions[it->action.kind].name);
String8 arg_str = action_argument_string(scratch.arena, gs, it->action.argument);
AddFmt("\"action.argument\":\"%.*s\",", S8VArg(arg_str));
AddFmt("\"target\":\"%.*s\"}", TextChunkVArg(npc_data(gs, it->action.talking_to_kind)->name));
#undef AddFmt
ReleaseScratch(scratch);
return current_list;
}
String8List memory_description(Arena *arena, Entity *e, Memory *it)
String8List memory_description(Arena *arena, GameState *gs, 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)
#define HUMAN(kind) S8VArg(npc_to_human_readable(gs, e, kind))
if (it->action.kind != ACT_none)
{
switch (it->action_taken)
switch (it->action.kind)
{
case ACT_none:
break;
case ACT_join:
AddFmt("%.*s joined %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action_argument.targeting));
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));
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));
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));
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));
AddFmt("%.*s assigned a definitive game objective to %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action.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));
AddFmt("%.*s awarded victory to %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action.talking_to_kind));
break;
}
}
if (it->speech.text_length > 0)
if (it->action.speech.text_length > 0)
{
String8 target_string = S8Lit("the world");
if (it->context.talking_to_kind != NPC_nobody)
if (it->action.talking_to_kind != NPC_nobody)
{
if (it->context.talking_to_kind == e->npc_kind)
if (it->action.talking_to_kind == e->npc_kind)
target_string = S8Lit("you");
else
target_string = S8CString(characters[it->context.talking_to_kind].name);
target_string = TextChunkString8(npc_data(gs, it->action.talking_to_kind)->name);
}
if(!e->is_world)
{
if(it->context.talking_to_kind == e->npc_kind)
if(it->action.talking_to_kind == e->npc_kind)
{
AddFmt("(Speaking directly you) ");
}
@ -518,23 +654,23 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
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));
AddFmt("%.*s said \"%.*s\" to %.*s", TextChunkVArg(npc_data(gs, it->context.author_npc_kind)->name), TextChunkVArg(it->action.speech), S8VArg(target_string));
if(!e->is_world)
{
AddFmt(" (you are %s)", characters[e->npc_kind].name)
// AddFmt(" (you are %.*s)", TextChunkVArg(npc_data(gs, e->npc_kind)->name));
}
AddFmt("\n");
}
if (no_longer_wants_to_converse)
{
if (it->action_argument.targeting == NPC_nobody)
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));
AddFmt("%.*s no longer wants to converse with %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->action.argument.targeting));
}
}
#undef HUMAN
@ -546,7 +682,7 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkTo can_talk_to)
{
assert(e->is_npc);
assert(e->npc_kind < ARRLEN(characters));
assert(npc_data(gs, e->npc_kind) != 0);
ArenaTemp scratch = GetScratch(&arena, 1);
@ -562,7 +698,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
{
String8List current_list = {0};
AddFmt("%s\n\n", global_prompt);
AddFmt("%s\n\n", characters[e->npc_kind].prompt);
// AddFmt("%.*s\n\n", TextChunkVArg(npc_data(gs, e->npc_kind)->prompt));
AddFmt("The characters who are near you, that you can target:\n");
BUFF_ITER(Entity*, &can_talk_to)
{
@ -572,7 +708,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
{
info = S8Lit(" - they're currently dead, they were murdered");
}
AddFmt("%s%.*s\n", characters[(*it)->npc_kind].name, S8VArg(info));
AddFmt("%.*s%.*s\n", TextChunkVArg(npc_data(gs, (*it)->npc_kind)->name), S8VArg(info));
}
AddFmt("\n");
@ -594,8 +730,6 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
}
String8List current_list = {0};
bool in_drama_memories = true;
assert(e->memories_first->context.drama_memory);
for(Memory *it = e->memories_first; it; it = it->next)
{
// going through memories, I'm going to accumulate human understandable sentences for what happened in current_list.
@ -603,12 +737,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
// write a new human understandable sentence or two to current_list
if (!it->context.i_said_this) {
if(in_drama_memories && !it->context.drama_memory)
{
in_drama_memories = false;
AddFmt("Some time passed...\n");
}
String8List desc_list = memory_description(scratch.arena, e, it);
String8List desc_list = memory_description(scratch.arena, gs, e, it);
S8ListConcat(&current_list, &desc_list);
}
@ -620,9 +749,9 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
AddFmt("Errors you made: \n");
for(RememberedError *cur = e->errorlist_first; cur; cur = cur->next)
{
if(cur->offending_self_output.speech.text_length > 0 || cur->offending_self_output.action_taken != ACT_none)
if(cur->offending_self_output.action.speech.text_length > 0 || cur->offending_self_output.action.kind != ACT_none)
{
String8 offending_json_output = S8ListJoin(scratch.arena, dump_memory_as_json(scratch.arena, &cur->offending_self_output), &(StringJoin){0});
String8 offending_json_output = S8ListJoin(scratch.arena, dump_memory_as_json(scratch.arena, gs, &cur->offending_self_output), &(StringJoin){0});
AddFmt("When you output, `%.*s`, ", S8VArg(offending_json_output));
}
AddFmt("%.*s\n", TextChunkVArg(cur->reason_why_its_bad));
@ -635,7 +764,7 @@ String8 generate_chatgpt_prompt(Arena *arena, GameState *gs, Entity *e, CanTalkT
if(it->context.i_said_this)
{
String8List current_list = {0}; // shadow the list of human understandable sentences to quickly flush
current_list = dump_memory_as_json(scratch.arena, it);
current_list = dump_memory_as_json(scratch.arena, gs, it);
AddNewNode(MSG_ASSISTANT);
}
}
@ -656,7 +785,7 @@ String8 get_field(Node *parent, String8 name)
return MD_ChildFromString(parent, name, 0)->first_child->string;
}
void parse_action_argument(Arena *error_arena, String8 *cur_error_message, ActionKind action, String8 action_argument_str, ActionArgument *out)
void parse_action_argument(Arena *error_arena, GameState *gs, String8 *cur_error_message, ActionKind action, String8 action_argument_str, ActionArgument *out)
{
assert(cur_error_message);
if(cur_error_message->size > 0) return;
@ -664,38 +793,21 @@ void parse_action_argument(Arena *error_arena, String8 *cur_error_message, Actio
String8 action_str = S8CString(actions[action].name);
// @TODO refactor into, action argument kinds and they parse into different action argument types
bool arg_is_character = action == ACT_join || action == ACT_aim_shotgun || action == ACT_end_conversation;
bool arg_is_gameplay_objective = action == ACT_assign_gameplay_objective;
if (arg_is_character)
{
out->kind = ARG_CHARACTER;
bool found_npc = false;
for (int i = 0; i < ARRLEN(characters); i++)
{
if (S8Match(S8CString(characters[i].name), action_argument_str, 0))
{
found_npc = true;
(*out).targeting = i;
}
Npc * npc = npc_data_by_name(gs, action_argument_str);
found_npc = npc != 0;
if(npc) {
out->targeting = npc->kind;
}
if (!found_npc)
{
*cur_error_message = FmtWithLint(error_arena, "Argument for action `%.*s` you gave is `%.*s`, which doesn't exist in the game so is invalid", S8VArg(action_str), S8VArg(action_argument_str));
}
}
else if (arg_is_gameplay_objective)
{
out->kind = ARG_OBJECTIVE;
if(action_argument_str.size >= MAX_SENTENCE_LENGTH)
{
String8 trimmed = S8Substring(action_argument_str, action_argument_str.size - MAX_SENTENCE_LENGTH/2, action_argument_str.size);
*cur_error_message = FmtWithLint(error_arena, "What you said for your action argument, '%.*s...' is WAY too big for the game to handle, it can be a maximum of %d characters, but you output %d!.", S8VArg(trimmed), MAX_SENTENCE_LENGTH, (int)action_argument_str.size);
}
if(cur_error_message->size == 0)
{
chunk_from_s8(&out->objective.description, action_argument_str);
}
}
else
{
assert(false); // don't know how to parse the argument string for this kind of action...
@ -705,13 +817,13 @@ void parse_action_argument(Arena *error_arena, String8 *cur_error_message, Actio
// if returned string has size greater than 0, it's the error message. Allocated
// on arena passed into it or in constant memory
String8 parse_chatgpt_response(Arena *arena, Entity *e, String8 action_in_json, Action *out)
String8 parse_chatgpt_response(Arena *arena, GameState *gs, Entity *e, String8 action_in_json, ActionOld *out)
{
ArenaTemp scratch = GetScratch(&arena, 1);
String8 error_message = {0};
*out = (Action) { 0 };
*out = (ActionOld) { 0 };
ParseResult result = ParseWholeString(scratch.arena, S8Lit("chat_message"), action_in_json);
if(result.errors.node_count > 0)
@ -731,7 +843,7 @@ String8 parse_chatgpt_response(Arena *arena, Entity *e, String8 action_in_json,
{
speech_str = get_field(message_obj, S8Lit("speech"));
action_str = get_field(message_obj, S8Lit("action"));
action_argument_str = get_field(message_obj, S8Lit("action_argument"));
action_argument_str = get_field(message_obj, S8Lit("action.argument"));
target_str = get_field(message_obj, S8Lit("target"));
}
if(error_message.size == 0 && action_str.size == 0)
@ -745,7 +857,7 @@ String8 parse_chatgpt_response(Arena *arena, Entity *e, String8 action_in_json,
{
error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
}
assert(e->npc_kind != NPC_Player); // player can't perform AI actions?
assert(e->npc_kind != NPC_player); // player can't perform AI actions?
if(error_message.size == 0)
{
@ -755,14 +867,11 @@ String8 parse_chatgpt_response(Arena *arena, Entity *e, String8 action_in_json,
}
else
{
Npc * npc = npc_data_by_name(gs, target_str);
bool found = false;
for(int i = 0; i < ARRLEN(characters); i++)
{
if(S8Match(target_str, S8CString(characters[i].name), 0))
{
found = true;
out->talking_to_kind = i;
}
if(npc) {
found = true;
out->talking_to_kind = npc->kind;
}
if(!found)
{
@ -798,7 +907,7 @@ String8 parse_chatgpt_response(Arena *arena, Entity *e, String8 action_in_json,
{
if(actions[out->kind].takes_argument)
{
parse_action_argument(arena,&error_message, out->kind, action_argument_str, &out->argument);
parse_action_argument(arena, gs, &error_message, out->kind, action_argument_str, &out->argument);
}
}
}

@ -0,0 +1,9 @@
@echo off
cl /nologo /diagnostics:caret /DDEVTOOLS /Igen /Ithirdparty /Wall /FC /Zi /WX playground.c /link /noimplib /noexp || goto :error
goto :EOF
:error
echo Failed to build
exit /B %ERRORLEVEL%

@ -0,0 +1,136 @@
#include <stdio.h>
#pragma warning(disable : 4820) // don't care about padding
#pragma warning(disable : 4388) // signed/unsigned mismatch probably doesn't matter
// #pragma warning(disable : 4100) // unused local doesn't matter, because sometimes I want to kee
#pragma warning(disable : 4053) // void operands are used for tricks like applying printf linting to non printf function calls
#pragma warning(disable : 4255) // strange no function prototype given thing?
#pragma warning(disable : 4456) // I shadow local declarations often and it's fine
#pragma warning(disable : 4061) // I don't need to *explicitly* handle everything, having a default: case should mean no more warnings
#pragma warning(disable : 4201) // nameless struct/union occurs
#pragma warning(disable : 4366) // I think unaligned memory addresses are fine to ignore
#pragma warning(disable : 4459) // Local function decl hiding global declaration I think is fine
#pragma warning(disable : 5045) // spectre mitigation??
#pragma warning(disable : 4244) // loss of data warning
#pragma warning(disable : 4101) // unreferenced local variable
#pragma warning(disable : 4100) // unreferenced local variable again?
#pragma warning(disable : 4189) // initialized and not referenced
#pragma warning(disable : 4242) // conversion
#pragma warning(disable : 4457) // hiding function variable happens
#pragma warning(disable : 4668) // __GNU_C__ macro undefined, fixing
#pragma warning(disable : 4996) // fopen is safe. I don't care about fopen_s
#include "md.h"
#include "md.c"
#define DESKTOP
#define WINDOWS
#define RND_IMPLEMENTATION
#include "makeprompt.h"
#include <windows.h> // for sleep.
typedef struct Error {
struct Error *next, *prev;
String8 message;
int line;
} Error;
typedef struct ErrorList {
int count;
Error *first, *last;
} ErrorList;
void ErrorPush(Arena *arena, ErrorList *list, Error message)
{
Error *new_err = PushArrayZero(arena, Error, 1);
*new_err = message;
QueuePush(list->first, list->last, new_err);
list->count += 1;
}
void error_impl(Arena *arena, ErrorList *errors, int line_in_toparse, String8 message)
{
ErrorPush(arena, errors, (Error){.line = line_in_toparse, .message = message});
// this is a function so you can breakpoint here and discover when errors occur
}
#define error(line_in_toparse, fmt_str, ...) error_impl(arena, errors, line_in_toparse, S8Fmt(arena, fmt_str, __VA_ARGS__))
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
Arena *arena = ArenaAlloc();
Sleep(200); // have to wait for console to pop up, super annoying
// Prose is the name for this file format where you describe the souls of characters
// tokenize
typedef struct ProseToken
{
struct ProseToken *next, *prev;
String8 field;
int field_number; // this is -1 if no field_number, e.g if 'Field Text #0:' isn't specified and had no '#', then this would be -1
String8 value; // may be an empty string, if it's trying to be like, an object
int indentation;
int line;
} ProseToken;
ErrorList errors_lit = {0};
ErrorList *errors = &errors_lit;
Npc out = {0};
// all arena allocations done from here are temporary. As it just copies data into Npc
// parse 'playground.txt' into 'out'
{
// read the file
String8 to_parse = LoadEntireFile(arena, S8Lit("playground.txt"));
// tokenize to_parse
ProseToken *tokenized_first = 0;
ProseToken *tokenized_last = 0;
{
String8List as_lines = S8Split(arena, to_parse, 1, &S8Lit("\n"));
int line = 1; // lines start at 1
for (String8Node *cur = as_lines.first; cur; cur = cur->next)
{
int indentation = 0;
while(indentation < cur->string.size && cur->string.str[indentation] == '\t') indentation += 1;
String8 no_funny_business = S8SkipWhitespace(S8ChopWhitespace(cur->string));
if(no_funny_business.size == 0) continue;
String8List along_colon = S8Split(arena, no_funny_business, 1, &S8Lit(":"));
if(along_colon.node_count != 2 && along_colon.node_count != 1) {
error(line, "Requires exactly one ':' on the line to delimit the field and value. Got %d", along_colon.node_count - 1);
} else {
ProseToken *token_out = PushArrayZero(arena, ProseToken, 1);
token_out->field_number = -1;
if(along_colon.node_count == 2)
token_out->value = along_colon.last->string;
token_out->line = line;
token_out->indentation = indentation;
DblPushBack(tokenized_first, tokenized_last, token_out);
}
line += 1;
}
}
}
if (errors->count > 0)
{
printf("Failed with errors:\n");
for (Error *cur = errors->first; cur; cur = cur->next)
{
printf("On line %d of input: %.*s\n", cur->line, S8VArg(cur->message));
}
assert(false);
}
printf("Success.\n");
__debugbreak();
}

@ -0,0 +1,27 @@
Name: Roger Penrose
Prompt: He is an illusive testing character, who confounds even the most brilliant philosophers and doctors to this date.
Soul:
{
{
Situation: {
Memories: [
"mem1",
"",
"mem3",
]
Events: [
{
Author: Jesse Pinkman
Response: {
speech: "How dare you!"
action: SAY_TO
}
}
]
}
Response: {
}
},
}

@ -0,0 +1,33 @@
Name: Roger Penrose
Description: He is an illusive testing character, who confounds even the most brilliant philosophers and doctors to this date.
Soul:
Situation #0:
Memory #0: I'm being
Memory #1: Silly
Perception:
Some testing thing:
In Room: Farm
Characters Around Me:
John:
Health Status: decent
Drunkenness: 0.0
Holding: nothing
Items Around Me:
Whiskey
My Status:
Health Status: decent
Drunkenness: 0.5
Holding: whiskey
Previous Event #0: John said to me, "Why are you like that bro?"
Previous Event #1: I said to John, "Just because"
Response:
Say_To:
To: John
Speech: Why are you like that dude?
Throw:
To: John

@ -12,7 +12,7 @@ if exist gen\ (
thirdparty\sokol-shdc.exe --input threedee.glsl --output gen\threedee.glsl.h --slang glsl300es:hlsl5:glsl330 || goto :error
@REM metadesk codegen
cl /nologo /diagnostics:caret /Ithirdparty /W3 /Zi /WX codegen.c || goto :error
cl /nologo /diagnostics:caret /Ithirdparty /FC /W3 /Zi /WX codegen.c || goto :error
@REM zig cc -Ithirdparty -gfull -gcodeview codegen.c -o codegen.exe || goto error
codegen || goto :error

104
ser.h

@ -4,65 +4,64 @@
typedef struct
{
b8 failed;
String8 why;
b8 failed;
String8 why;
} SerError;
typedef struct
{
u8 *data; // set to 0 to dry run and get maximum size. max doesn't matter in this case
u64 cur;
u64 max;
u8 *data; // set to 0 to dry run and get maximum size. max doesn't matter in this case
u64 cur;
u64 max;
Arena *arena; // allocate everything new on this, so that if serialization fails allocations can be undone
// Serializing should never allocate. So this can be null when you serialize
Arena *arena; // allocate everything new on this, so that if serialization fails allocations can be undone
// Serializing should never allocate. So this can be null when you serialize
int version;
SerError cur_error;
Arena *error_arena; // all error messages are allocated here
int version;
SerError cur_error;
Arena *error_arena; // all error messages are allocated here
b8 serializing;
b8 serializing;
} SerState;
void ser_bytes(SerState *ser, u8 *bytes, u64 bytes_size)
{
if(!ser->data && !ser->serializing)
{
ser->cur_error = (SerError){.failed = true, .why = S8Lit("Deserializing but the data is null")};
}
if(!ser->cur_error.failed)
{
if(ser->data)
{
// maximum doesn't matter unless writing to data
if(ser->cur + bytes_size > ser->max)
{
ser->cur_error = (SerError){.failed = true, .why = S8Lit("Too big bro")};
}
else
{
if(ser->serializing)
{
memcpy(ser->data + ser->cur, bytes, bytes_size);
}
else
if (!ser->data && !ser->serializing)
{
memcpy(bytes, ser->data + ser->cur, bytes_size);
ser->cur_error = (SerError){.failed = true, .why = S8Lit("Deserializing but the data is null")};
}
}
}
if (!ser->cur_error.failed)
{
if (ser->data)
{
// maximum doesn't matter unless writing to data
if (ser->cur + bytes_size > ser->max)
{
ser->cur_error = (SerError){.failed = true, .why = S8Lit("Too big bro")};
}
else
{
if (ser->serializing)
{
memcpy(ser->data + ser->cur, bytes, bytes_size);
}
else
{
memcpy(bytes, ser->data + ser->cur, bytes_size);
}
}
}
ser->cur += bytes_size;
}
ser->cur += bytes_size;
}
}
#define SER_MAKE_FOR_TYPE(type) void ser_##type(SerState *ser, type *into) \
{ \
ser_bytes(ser, (u8*)into, sizeof(*into)); \
}
#define SER_MAKE_FOR_TYPE(type) \
void ser_##type(SerState *ser, type *into) \
{ \
ser_bytes(ser, (u8 *)into, sizeof(*into)); \
}
SER_MAKE_FOR_TYPE(int);
SER_MAKE_FOR_TYPE(u64);
@ -71,15 +70,14 @@ SER_MAKE_FOR_TYPE(u64);
// deserialized data onto a newly allocated buffer
void ser_String8(SerState *ser, String8 *s, Arena *allocate_onto)
{
ser_u64(ser, &s->size);
if(ser->serializing)
{
ser_bytes(ser, s->str, s->size);
}
else
{
s->str = ArenaPush(allocate_onto, s->size);
ser_bytes(ser, s->str, s->size);
}
ser_u64(ser, &s->size);
if (ser->serializing)
{
ser_bytes(ser, s->str, s->size);
}
else
{
s->str = ArenaPush(allocate_onto, s->size);
ser_bytes(ser, s->str, s->size);
}
}

Binary file not shown.

@ -3,6 +3,17 @@ Handmade Seattle Requirements:
- clear objective that's physical, not metaphysical
- music, fancy headphones
Todo List 10/2/2023:
- Working character anims with multiple npcs.
- Saving and loading working well enough to edit levels.
- Edit character brain UI, basic stuff at first.
- Change AI system to the memory fewer message thing tried out in the openai platform
- Items you can pick up, use, and throw. Add alcohol item. This makes the characters woozy optionally.
- Characters and player walking between rooms.
- Build out bill, jack, and jane. Get some interactions there.
Send this build to discord.
My estimated time of completion for all of these, as of 10/2/2023, is 5 days. So new build on 10/7/2023
Urgent:
- angel doesn't have characters near you

@ -3,14 +3,12 @@
#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
#define PERCEPTION_HEARING_RAGE (TILE_SIZE*4.0f)
#define CHARACTERS_PER_SEC 45.0f
#define ANGEL_CHARACTERS_PER_SEC 35.0f
#define PROPAGATE_ACTIONS_RADIUS 4.0f
#define SWORD_SWIPE_RADIUS (TILE_SIZE*3.0f)
#define ARROW_SPEED 200.0f
#define SECONDS_PER_ARROW 1.3f

@ -0,0 +1,27 @@
set -e
if [ -d gen ]; then
echo "Codegen dir already exists"
else
mkdir gen
fi
echo Shader Gen
unameOut="$(uname -s)"
# https://stackoverflow.com/questions/3466166/how-to-check-if-running-in-cygwin-mac-or-linux
case "${unameOut}" in
Darwin*)
if [[ $(uname -m) == 'arm64' ]]; then
SHADER_EXECUTABLE="thirdparty/sokol-shdc-mac-arm64"
else
echo "Haven't downloaded the x64 macos shader binary yet, sorry"
fi;;
Linux*) echo "Linux not supported yet.";;
*) echo "No idea what this machine is dude, sorry";;
esac
$SHADER_EXECUTABLE --input threedee.glsl --output gen/threedee.glsl.h --slang glsl300es:hlsl5:glsl330

@ -2,6 +2,7 @@
#include <stdio.h>
#define STRINGIZE(x) STRINGIZE2(x)
#define STRINGIZE2(x) #x
@ -25,3 +26,4 @@
#define assert game_assert
#define Log(...) { printf("%s Log %d | ", __FILE__, __LINE__); printf(__VA_ARGS__); }

Loading…
Cancel
Save