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.

7793 lines
226 KiB
C

#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??
#include "tuning.h"
#define SOKOL_IMPL
// ctags doesn't like the error macro so we do this instead. lame
#define ISANERROR(why) jfdskljfdsljfklja why
#if defined(WIN32) || defined(_WIN32)
#define DESKTOP
#define WINDOWS
#define SOKOL_GLCORE33
#define SAMPLE_COUNT 4
#endif
#if defined(__EMSCRIPTEN__)
#define WEB
#define SOKOL_GLES3
#define SAMPLE_COUNT 4
#endif
#define DRWAV_ASSERT game_assert
#define SOKOL_ASSERT game_assert
#define STBDS_ASSERT game_assert
#define STBI_ASSERT game_assert
#define STBTT_assert game_assert
#include "utility.h"
#ifdef WINDOWS
#pragma warning(push, 3)
#include <Windows.h>
#include <stdint.h>
// https://developer.download.nvidia.com/devzone/devcenter/gamegraphics/files/OptimusRenderingPolicies.pdf
// Tells nvidia to use dedicated graphics card if it can on laptops that also have integrated graphics
__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
// Vice versa for AMD but I don't have the docs link on me at the moment
__declspec(dllexport) uint32_t AmdPowerXpressRequestHighPerformance = 0x00000001;
#endif
#include "buff.h"
#include "sokol_app.h"
#pragma warning(push)
#pragma warning(disable : 4191) // unsafe function calling
#ifdef WEB
# include <GLES3/gl3.h>
# undef glGetError
# define glGetError() (GL_NO_ERROR)
#endif
#include "sokol_gfx.h"
#pragma warning(pop)
#include "sokol_time.h"
#include "sokol_audio.h"
#include "sokol_log.h"
#include "sokol_glue.h"
#define STB_IMAGE_IMPLEMENTATION
#pragma warning(push)
#pragma warning(disable : 4242) // unsafe conversion
#include "stb_image.h"
#pragma warning(pop)
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
#include "HandmadeMath.h"
#define DR_WAV_IMPLEMENTATION
#include "dr_wav.h"
#define STB_DS_IMPLEMENTATION
#include "stb_ds.h" // placed last because it includes <assert.h>
#undef assert
#define assert game_assert
#pragma warning(pop)
// web compatible metadesk
#ifdef WEB
#define __gnu_linux__
#define i386
#define DEFAULT_ARENA 0
typedef struct WebArena
{
char *data;
size_t cap;
size_t pos;
size_t align; // pls 
} WebArena;
static bool next_arena_big = false;
WebArena *web_arena_alloc()
{
// the pointer here is assumed to be aligned to whatever the maximum
// alignment you could want from the arena, because only the `pos` is
// modified to align with the requested alignment.
WebArena *to_return = malloc(sizeof(to_return));
size_t this_size = ARENA_SIZE;
if(next_arena_big) this_size = BIG_ARENA_SIZE;
*to_return = (WebArena) {
.data = calloc(1, this_size),
.cap = this_size,
.align = 8,
.pos = 0,
};
next_arena_big = false;
return to_return;
}
void web_arena_release(WebArena *arena)
{
free(arena->data);
arena->data = 0;
free(arena);
}
size_t web_arena_get_pos(WebArena *arena)
{
return arena->pos;
}
void *web_arena_push(WebArena *arena, size_t amount)
{
while(arena->pos % arena->align != 0)
{
arena->pos += 1;
assert(arena->pos < arena->cap);
}
void *to_return = arena->data + arena->pos;
arena->pos += amount;
bool arena_ok = arena->pos < arena->cap;
if(!arena_ok)
{
Log("Arena size: %lu\n", arena->cap);
Log("Arena pos: %lu\n", arena->pos);
}
assert(arena_ok);
return to_return;
}
void web_arena_pop_to(WebArena *arena, size_t new_pos)
{
arena->pos = new_pos;
assert(arena->pos < arena->cap);
}
void web_arena_set_auto_align(WebArena *arena, size_t align)
{
arena->align = align;
}
#define IMPL_Arena WebArena
#define IMPL_ArenaAlloc web_arena_alloc
#define IMPL_ArenaRelease web_arena_release
#define IMPL_ArenaGetPos web_arena_get_pos
#define IMPL_ArenaPush web_arena_push
#define IMPL_ArenaPopTo web_arena_pop_to
#define IMPL_ArenaSetAutoAlign web_arena_set_auto_align
#define IMPL_ArenaMinPos 64 // no idea what this is honestly
#endif // web
#pragma warning(disable : 4996) // fopen is safe. I don't care about fopen_s
#pragma warning(push)
#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
#define STBSP_ADD_TO_FUNCTIONS no_ubsan
#define FUNCTION no_ubsan
#include "md.h"
#undef Assert
#define Assert assert
#include "md.c"
#pragma warning(pop)
#include "ser.h"
#include <math.h>
#ifdef DEVTOOLS
#ifdef DESKTOP
#define PROFILING
#define PROFILING_IMPL
#endif
#endif
#include "profiling.h"
// the returned string's size doesn't include the null terminator.
String8 nullterm(Arena *copy_onto, String8 to_nullterm)
{
String8 to_return = {0};
to_return.str = PushArray(copy_onto, u8, to_nullterm.size + 1);
to_return.size = to_nullterm.size;
to_return.str[to_return.size] = '\0';
memcpy(to_return.str, to_nullterm.str, to_nullterm.size);
return to_return;
}
// all utilities that depend on md.h strings or look like md.h stuff in general
#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__))
void WriteEntireFile(char *null_terminated_path, String8 data) {
FILE* file = fopen(null_terminated_path, "wb");
if(file) {
fwrite(data.str, 1, data.size, file);
fclose(file);
} else {
Log("Failed to open file %s and save data.\n", null_terminated_path)
}
}
double clamp(double d, double min, double max)
{
const double t = d < min ? min : d;
return t > max ? max : t;
}
float clampf(float d, float min, float max)
{
const float t = d < min ? min : d;
return t > max ? max : t;
}
float clamp01(float f)
{
return clampf(f, 0.0f, 1.0f);
}
#ifdef min
#undef min
#endif
int min(int a, int b)
{
if (a < b) return a;
else return b;
}
// so can be grep'd and removed
#define dbgprint(...) { printf("Debug | %s:%d | ", __FILE__, __LINE__); printf(__VA_ARGS__); }
#define v3varg(v) v.x, v.y, v.z
#define v2varg(v) v.x, v.y
#define qvarg(v) v.x, v.y, v.z, v.w
Vec2 RotateV2(Vec2 v, float theta)
{
return V2(
v.X * cosf(theta) - v.Y * sinf(theta),
v.X * sinf(theta) + v.Y * cosf(theta)
);
}
Vec2 ReflectV2(Vec2 v, Vec2 normal)
{
assert(fabsf(LenV2(normal) - 1.0f) < 0.01f); // must be normalized
Vec2 to_return = SubV2(v, MulV2F(normal, 2.0f * DotV2(v, normal)));
assert(!isnan(to_return.x));
assert(!isnan(to_return.y));
return to_return;
}
float AngleOfV2(Vec2 v)
{
return atan2f(v.y, v.x);
}
#define TAU (PI*2.0)
float lerp_angle(float from, float t, float to)
{
double difference = fmod(to - from, TAU);
double distance = fmod(2.0 * difference, TAU) - difference;
return (float)(from + distance * t);
}
typedef struct AABB
{
Vec2 upper_left;
Vec2 lower_right;
} AABB;
typedef struct Circle
{
Vec2 center;
float radius;
} Circle;
typedef struct Quad
{
union
{
struct
{
Vec2 ul; // upper left
Vec2 ur; // upper right
Vec2 lr; // lower right
Vec2 ll; // lower left
};
Vec2 points[4];
};
} Quad;
// for intellisense in vscode I think?
#include "character_info.h"
#include "characters.gen.h"
#define RND_IMPLEMENTATION
#include "makeprompt.h"
typedef BUFF(Entity*, 16) Overlapping;
typedef struct AudioSample
{
float *pcm_data; // allocated by loader, must be freed
uint64_t pcm_data_length;
unsigned int num_channels;
} AudioSample;
typedef struct AudioPlayer
{
AudioSample *sample; // if not 0, exists
double volume; // ZII, 1.0 + this again
double pitch; // zero initialized, the pitch used to play is 1.0 + this
double cursor_time; // in seconds, current audio sample is cursor_time * sample_rate
} AudioPlayer;
AudioPlayer playing_audio[128] = { 0 };
#define SAMPLE_RATE 44100
AudioSample load_wav_audio(const char *path)
{
unsigned int sampleRate;
AudioSample to_return = { 0 };
to_return.pcm_data = drwav_open_file_and_read_pcm_frames_f32(path, &to_return.num_channels, &sampleRate, &to_return.pcm_data_length, 0);
assert(to_return.num_channels == 1 || to_return.num_channels == 2);
assert(sampleRate == SAMPLE_RATE);
return to_return;
}
void cursor_pcm(AudioPlayer *p, uint64_t *integer, float *fractional)
{
double sample_time = p->cursor_time * SAMPLE_RATE;
*integer = (uint64_t)sample_time;
*fractional = (float)(sample_time - *integer);
}
float float_rand(float min, float max)
{
float scale = rand() / (float) RAND_MAX; /* [0, 1.0] */
return min + scale * (max - min); /* [min, max] */
}
// always randomizes pitch
void play_audio(AudioSample *sample, float volume)
{
AudioPlayer *to_use = 0;
for (int i = 0; i < ARRLEN(playing_audio); i++)
{
if (playing_audio[i].sample == 0)
{
to_use = &playing_audio[i];
break;
}
}
assert(to_use);
*to_use = (AudioPlayer) { 0 };
to_use->sample = sample;
to_use->volume = volume;
to_use->pitch = float_rand(-0.1f, 0.1f);
}
// keydown needs to be referenced when begin text input,
// on web it disables event handling so the button up event isn't received
// directly accessing these should only be used for debugging purposes, and
// not in release. TODO make it so that this is enforced
// by leaving them out when devtools is turned off
#define SAPP_KEYCODE_MAX SAPP_KEYCODE_MENU
bool keydown[SAPP_KEYCODE_MAX] = { 0 };
bool keypressed[SAPP_KEYCODE_MAX] = { 0 };
Vec2 mouse_movement = { 0 };
typedef struct {
bool open;
bool for_giving;
} ItemgridState;
ItemgridState item_grid_state = {0};
struct { char *key; void *value; } *immediate_state = 0;
void init_immediate_state() {
sh_new_strdup(immediate_state);
}
void cleanup_immediate_state() {
hmfree(immediate_state);
}
void *get_state_function(char *key, size_t value_size) {
assert(key);
if(shgeti(immediate_state, key) == -1) {
shput(immediate_state, key, calloc(1, value_size));
}
return shget(immediate_state, key);
}
#define get_state(variable_name, type, ...) type* variable_name = get_state_function((char*)tprint(__VA_ARGS__).str, sizeof(*variable_name))
// set to true when should receive text input from the web input box
// or desktop text input
bool receiving_text_input = false;
float text_input_fade = 0.0f;
// called from the web to see if should do the text input modal
bool is_receiving_text_input()
{
return receiving_text_input;
}
#ifdef DESKTOP
TextChunk text_input = TextChunkLitC("");
#else
#ifdef WEB
EMSCRIPTEN_KEEPALIVE
void stop_controlling_input()
{
_sapp_emsc_unregister_eventhandlers(); // stop getting input, hand it off to text input
}
EMSCRIPTEN_KEEPALIVE
void start_controlling_input()
{
memset(keydown, 0, sizeof(keydown));
_sapp_emsc_register_eventhandlers();
}
#else
ISANERROR("No platform defined for text input!")
#endif // web
#endif // desktop
TextChunk text_input_result = {0};
TextInputResultKey text_result_key = 1;
bool text_ready = false;
TextInputResultKey begin_text_input(String8 placeholder_text)
{
receiving_text_input = true; // this notifies the web layer that it should show the modal. Currently placeholder text is unimplemented on the web
#ifdef DESKTOP
chunk_from_s8(&text_input, placeholder_text);
#endif
text_result_key += 1;
text_ready = false;
return text_result_key;
}
// doesn't last for more than after this is called! Don't rely on it
// e.g Once text is ready for you and this returns a non empty string, the next call to this will return an empty string
String8 text_ready_for_me(TextInputResultKey key) {
if(key == text_result_key && text_ready) {
text_ready = false;
return TextChunkString8(text_input_result);
}
return S8Lit("");
}
Vec2 FloorV2(Vec2 v)
{
return V2(floorf(v.x), floorf(v.y));
}
Arena *frame_arena = 0;
Arena *persistent_arena = 0; // watch out, arenas have limited size.
#ifdef WINDOWS
// uses frame arena
LPCWSTR windows_string(String8 s)
{
int num_characters = MultiByteToWideChar(CP_UTF8, 0, (LPCCH)s.str, (int)s.size, 0, 0);
wchar_t *to_return = PushArray(frame_arena, wchar_t, num_characters + 1); // also allocate for null terminating character
assert(MultiByteToWideChar(CP_UTF8, 0, (LPCCH)s.str, (int)s.size, to_return, num_characters) == num_characters);
to_return[num_characters] = '\0';
return to_return;
}
#endif
typedef enum
{
GEN_Deleted = -1,
GEN_NotDoneYet = 0,
GEN_Success = 1,
GEN_Failed = 2,
} GenRequestStatus;
#ifdef DESKTOP
#ifdef WINDOWS
#pragma warning(push, 3)
#pragma comment(lib, "WinHttp")
#include <WinHttp.h>
#include <process.h>
#pragma warning(pop)
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;
TextChunk generated;
uintptr_t thread_handle;
Arena *arena;
String8 post_req_body; // allocated on thread_arena
} ChatRequest;
ChatRequest *requests_first = 0;
ChatRequest *requests_last = 0;
int next_request_id = 1;
ChatRequest *requests_free_list = 0;
void generation_thread(void* my_request_voidptr)
{
ChatRequest *my_request = (ChatRequest*)my_request_voidptr;
bool succeeded = true;
#define WinAssertWithErrorCode(X) if( !( X ) ) { unsigned int error = GetLastError(); Log("Error %u in %s\n", error, #X); my_request->status = 2; return; }
HINTERNET hSession = WinHttpOpen(L"PlayGPT winhttp backend", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
WinAssertWithErrorCode(hSession);
LPCWSTR windows_server_name = windows_string(S8Lit(SERVER_DOMAIN));
HINTERNET hConnect = WinHttpConnect(hSession, windows_server_name, SERVER_PORT, 0);
WinAssertWithErrorCode(hConnect);
int security_flags = 0;
if(IS_SERVER_SECURE)
{
security_flags = WINHTTP_FLAG_SECURE;
}
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", L"completion", 0, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, security_flags);
WinAssertWithErrorCode(hRequest);
// @IMPORTANT @TODO the windows_string allocates on the frame arena, but
// according to https://learn.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpsendrequest
// the buffer needs to remain available as long as the http request is running, so to make this async and do the loading thing need some other way to allocate the winndows string.... arenas bad?
succeeded = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)my_request->post_req_body.str, (DWORD)my_request->post_req_body.size, (DWORD)my_request->post_req_body.size, 0);
if(my_request->should_close) return;
if(!succeeded)
{
Log("Couldn't do the web: %lu\n", GetLastError());
my_request->status = 2;
}
if(succeeded)
{
WinAssertWithErrorCode(WinHttpReceiveResponse(hRequest, 0));
DWORD status_code;
DWORD status_code_size = sizeof(status_code);
WinAssertWithErrorCode(WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX));
Log("Status code: %lu\n", status_code);
WinAssertWithErrorCode(status_code != 500);
DWORD dwSize = 0;
String8List received_data_list = {0};
do
{
dwSize = 0;
WinAssertWithErrorCode(WinHttpQueryDataAvailable(hRequest, &dwSize));
if(dwSize == 0)
{
Log("Didn't get anything back.\n");
}
else
{
u8* out_buffer = PushArray(my_request->arena, u8, dwSize + 1);
DWORD dwDownloaded = 0;
WinAssertWithErrorCode(WinHttpReadData(hRequest, (LPVOID)out_buffer, dwSize, &dwDownloaded));
out_buffer[dwDownloaded - 1] = '\0';
Log("Got this from http, size %lu: %s\n", dwDownloaded, out_buffer);
S8ListPush(my_request->arena, &received_data_list, S8(out_buffer, dwDownloaded - 1)); // the string shouldn't include a null terminator in its length, and WinHttpReadData has a null terminator here
}
} while (dwSize > 0);
String8 received_data = S8ListJoin(my_request->arena, received_data_list, &(StringJoin){0});
String8 ai_response = S8Substring(received_data, 1, received_data.size);
if(ai_response.size > ARRLEN(my_request->generated.text))
{
Log("%lld too big for %lld\n", ai_response.size, ARRLEN(my_request->generated.text));
my_request->status = 2;
return;
}
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)
{
ArenaTemp scratch = GetScratch(0,0);
String8 post_req_body = FmtWithLint(scratch.arena, "|%.*s", S8VArg(prompt));
// checking for taken characters, pipe should only occur at the beginning
for(u64 i = 1; i < post_req_body.size; i++)
{
assert(post_req_body.str[i] != '|');
}
ChatRequest *to_return = 0;
if(requests_free_list)
{
to_return = requests_free_list;
StackPop(requests_free_list);
*to_return = (ChatRequest){0};
}
else
{
to_return = PushArrayZero(persistent_arena, ChatRequest, 1);
}
to_return->arena = ArenaAlloc();
to_return->id = next_request_id;
next_request_id += 1;
to_return->post_req_body.str = PushArrayZero(to_return->arena, u8, post_req_body.size);
to_return->post_req_body.size = post_req_body.size;
memcpy(to_return->post_req_body.str, post_req_body.str, post_req_body.size);
to_return->thread_handle = _beginthread(generation_thread, 0, to_return);
assert(to_return->thread_handle);
DblPushBack(requests_first, requests_last, to_return);
ReleaseScratch(scratch);
return to_return->id;
}
// should never return null
// @TODO @IMPORTANT this doesn't work with save games because it assumes the id is always
// valid but saved IDs won't be valid on reboot
ChatRequest *get_by_id(int id)
{
for(ChatRequest *cur = requests_first; cur; cur = cur->next)
{
if(cur->id == id && !cur->user_is_done_with_this_request)
{
return cur;
}
}
return 0;
}
void done_with_request(int id)
{
ChatRequest *req = get_by_id(id);
if(req)
{
if(req->thread_is_done_with_this_request)
{
ArenaRelease(req->arena);
DblRemove(requests_first, requests_last, req);
*req = (ChatRequest){0};
StackPush(requests_free_list, req);
}
else
{
req->user_is_done_with_this_request = true;
}
}
}
GenRequestStatus gen_request_status(int id)
{
ChatRequest *req = get_by_id(id);
if(!req)
return GEN_Deleted;
else
return req->status;
}
TextChunk gen_request_content(int id)
{
assert(get_by_id(id));
return get_by_id(id)->generated;
}
#else
ISANERROR("Only know how to do desktop http requests on windows")
#endif // WINDOWS
#endif // DESKTOP
#ifdef WEB
int make_generation_request(String8 prompt_str)
{
ArenaTemp scratch = GetScratch(0, 0);
String8 terminated_completion_url = nullterm(scratch.arena, FmtWithLint(scratch.arena, "%s://%s:%d/completion", IS_SERVER_SECURE ? "https" : "http", SERVER_DOMAIN, SERVER_PORT));
int req_id = EM_ASM_INT({
return make_generation_request(UTF8ToString($0, $1), UTF8ToString($2, $3));
},
prompt_str.str, (int)prompt_str.size, terminated_completion_url.str, (int)terminated_completion_url.size);
ReleaseScratch(scratch);
return req_id;
}
GenRequestStatus gen_request_status(int id)
{
int status = EM_ASM_INT({
return get_generation_request_status($0);
}, id);
return status;
}
TextChunk gen_request_content(int id)
{
char sentence_cstr[MAX_SENTENCE_LENGTH] = {0};
EM_ASM({
let generation = get_generation_request_content($0);
stringToUTF8(generation, $1, $2);
},
id, sentence_cstr, ARRLEN(sentence_cstr) - 1); // I think minus one for null terminator...
TextChunk to_return = {0};
memcpy(to_return.text, sentence_cstr, MAX_SENTENCE_LENGTH);
to_return.text_length = strlen(sentence_cstr);
return to_return;
}
void done_with_request(int id)
{
EM_ASM({
done_with_generation_request($0);
},
id);
}
#endif // WEB
Memory *memories_free_list = 0;
RememberedError *remembered_error_free_list = 0;
// s.size must be less than MAX_SENTENCE_LENGTH, or assert fails
RememberedError *allocate_remembered_error(Arena *arena)
{
RememberedError *to_return = 0;
if(remembered_error_free_list)
{
to_return = remembered_error_free_list;
StackPop(remembered_error_free_list);
}
else
{
to_return = PushArray(arena, RememberedError, 1);
}
*to_return = (RememberedError){0};
return to_return;
}
void remove_remembered_error_from(RememberedError **first, RememberedError **last, RememberedError *chunk)
{
DblRemove(*first, *last, chunk);
StackPush(remembered_error_free_list, chunk);
}
void append_to_errors(Entity *from, Memory incorrect_memory, String8 s)
{
RememberedError *err = allocate_remembered_error(persistent_arena);
chunk_from_s8(&err->reason_why_its_bad, s);
err->offending_self_output = incorrect_memory;
while(true)
{
int count = 0;
for(RememberedError *cur = from->errorlist_first; cur; cur = cur->next) count++;
if(count < REMEMBERED_ERRORS) break;
remove_remembered_error_from(&from->errorlist_first, &from->errorlist_last, from->errorlist_first);
}
DblPushBack(from->errorlist_first, from->errorlist_last, err);
from->perceptions_dirty = true;
}
String8 tprint(char *format, ...)
{
String8 to_return = {0};
va_list argptr;
va_start(argptr, format);
to_return = S8FmtV(frame_arena, format, argptr);
va_end(argptr);
return nullterm(frame_arena, to_return);
}
bool V2ApproxEq(Vec2 a, Vec2 b)
{
return LenV2(SubV2(a, b)) <= 0.01f;
}
float max_coord(Vec2 v)
{
return v.x > v.y ? v.x : v.y;
}
AABB grow_from_center(AABB base, Vec2 growth)
{
AABB ret;
ret.upper_left = AddV2(base.upper_left, V2(-growth.x, growth.y));
ret.lower_right = AddV2(base.lower_right, V2(growth.x, -growth.y));
return ret;
}
AABB grow_from_ml(Vec2 ml, Vec2 offset, Vec2 size) {
AABB ret;
ret.upper_left = AddV2(AddV2(ml, offset), V2(0.0f, size.y/2.0f));
ret.lower_right = AddV2(ret.upper_left, V2(size.x, -size.y));
return ret;
}
// aabb advice by iRadEntertainment
Vec2 entity_aabb_size(Entity *e)
{
if (e->is_npc)
{
return V2(1,1);
}
else
{
assert(false);
return (Vec2) { 0 };
}
}
float entity_radius(Entity *e)
{
if (e->is_npc)
{
return 0.35f;
}
else
{
assert(false);
return 0;
}
}
Vec2 rotate_counter_clockwise(Vec2 v)
{
return V2(-v.Y, v.X);
}
Vec2 rotate_clockwise(Vec2 v)
{
return V2(v.y, -v.x);
}
Vec2 aabb_center(AABB aabb)
{
return MulV2F(AddV2(aabb.upper_left, aabb.lower_right), 0.5f);
}
AABB aabb_centered(Vec2 at, Vec2 size)
{
return (AABB) {
.upper_left = AddV2(at, V2(-size.X / 2.0f, size.Y / 2.0f)),
.lower_right = AddV2(at, V2(size.X / 2.0f, -size.Y / 2.0f)),
};
}
AABB entity_aabb_at(Entity *e, Vec2 at)
{
return aabb_centered(at, entity_aabb_size(e));
}
AABB entity_aabb(Entity *e)
{
Vec2 at = e->pos;
/* following doesn't work because in move_and_slide I'm not using this function
if(e->is_character) // aabb near feet
{
at = AddV2(at, V2(0.0f, -50.0f));
}
*/
return entity_aabb_at(e, at);
}
Entity *player(GameState *gs) {
ENTITIES_ITER(gs->entities) {
if(it->npc_kind == NPC_player) return it;
}
return 0;
}
Entity *world(GameState *gs) {
ENTITIES_ITER(gs->entities) {
if(it->is_world) return it;
}
return 0;
}
typedef struct LoadedImage
{
struct LoadedImage *next;
String8 name;
sg_image image;
} LoadedImage;
LoadedImage *loaded_images = 0;
sg_image load_image(String8 path)
{
ArenaTemp scratch = GetScratch(0, 0);
for(LoadedImage *cur = loaded_images; cur; cur = cur->next)
{
if(S8Match(cur->name, path, 0))
{
return cur->image;
}
}
LoadedImage *loaded = PushArray(persistent_arena, LoadedImage, 1);
loaded->name = S8Copy(persistent_arena, path);
StackPush(loaded_images, loaded);
sg_image to_return = { 0 };
int png_width, png_height, num_channels;
const int desired_channels = 4;
stbi_uc* pixels = stbi_load(
(const char*)nullterm(frame_arena, path).str,
&png_width, &png_height,
&num_channels, 0);
bool free_the_pixels = true;
if(num_channels == 3)
{
stbi_uc *old_pixels = pixels;
pixels = ArenaPush(scratch.arena, png_width * png_height * 4 * sizeof(stbi_uc));
for(u64 pixel_i = 0; pixel_i < png_width * png_height; pixel_i++)
{
pixels[pixel_i*4 + 0] = old_pixels[pixel_i*3 + 0];
pixels[pixel_i*4 + 1] = old_pixels[pixel_i*3 + 1];
pixels[pixel_i*4 + 2] = old_pixels[pixel_i*3 + 2];
pixels[pixel_i*4 + 3] = 255;
}
num_channels = 4;
free_the_pixels = false;
stbi_image_free(old_pixels);
}
assert(pixels);
assert(desired_channels == num_channels);
//Log("Path %.*s | Loading image with dimensions %d %d\n", S8VArg(path), png_width, png_height);
to_return = sg_make_image(&(sg_image_desc)
{
.width = png_width,
.height = png_height,
.pixel_format = sapp_sgcontext().color_format,
.num_mipmaps = 1,
.data.subimage[0][0] =
{
.ptr = pixels,
.size = (size_t)(png_width * png_height * num_channels),
}
});
loaded->image = to_return;
ReleaseScratch(scratch);
return to_return;
}
SER_MAKE_FOR_TYPE(uint64_t);
SER_MAKE_FOR_TYPE(bool);
SER_MAKE_FOR_TYPE(double);
SER_MAKE_FOR_TYPE(float);
SER_MAKE_FOR_TYPE(PropKind);
SER_MAKE_FOR_TYPE(NpcKind);
SER_MAKE_FOR_TYPE(ActionKind);
SER_MAKE_FOR_TYPE(Memory);
SER_MAKE_FOR_TYPE(Vec2);
SER_MAKE_FOR_TYPE(Vec3);
SER_MAKE_FOR_TYPE(EntityRef);
SER_MAKE_FOR_TYPE(NPCPlayerStanding);
SER_MAKE_FOR_TYPE(u16);
void ser_Quat(SerState *ser, Quat *q)
{
ser_float(ser, &q->x);
ser_float(ser, &q->y);
ser_float(ser, &q->z);
ser_float(ser, &q->w);
}
typedef struct
{
Vec3 offset;
Quat rotation;
Vec3 scale;
} Transform;
#pragma pack(1)
typedef struct
{
Vec3 pos;
Vec2 uv; // CANNOT have struct padding
} Vertex;
#pragma pack(1)
typedef struct
{
Vec3 position;
Vec2 uv;
u16 joint_indices[4];
float joint_weights[4];
} ArmatureVertex;
SER_MAKE_FOR_TYPE(Vertex);
typedef struct Mesh
{
struct Mesh *next;
Vertex *vertices;
u64 num_vertices;
sg_buffer loaded_buffer;
sg_image image;
String8 name;
} Mesh;
typedef struct PoseBone
{
float time; // time through animation this pose occurs at
Transform parent_space_pose;
} PoseBone;
typedef struct Bone
{
struct Bone *parent;
Mat4 matrix_local;
Mat4 inverse_model_space_pos;
String8 name;
float length;
} Bone;
typedef struct AnimationTrack
{
PoseBone *poses;
u64 poses_length;
} AnimationTrack;
typedef struct Animation
{
String8 name;
// assumed to be the same as the number of bones in the armature the animation is in
AnimationTrack *tracks;
} Animation;
typedef struct
{
Vec3 pos;
Vec3 euler_rotation;
Vec3 scale;
} BlenderTransform;
typedef struct PlacedMesh
{
struct PlacedMesh *next;
Transform t;
Mesh *draw_with;
String8 name;
} PlacedMesh;
typedef struct PlacedEntity
{
struct PlacedEntity *next;
Transform t;
NpcKind npc_kind;
} PlacedEntity;
// mesh_name is for debugging
// arena must last as long as the Mesh lasts. Internal data points to `arena`, such as
// the name of the mesh's buffer in sokol. The returned mesh doesn't point to the binary
// file anymore.
Mesh load_mesh(Arena *arena, String8 binary_file, String8 mesh_name)
{
ArenaTemp scratch = GetScratch(&arena, 1);
SerState ser = {
.data = binary_file.str,
.max = binary_file.size,
.arena = arena,
.error_arena = scratch.arena,
.serializing = false,
};
Mesh to_return = {0};
bool is_armature;
ser_bool(&ser, &is_armature);
assert(!is_armature);
String8 image_filename;
ser_String8(&ser, &image_filename, scratch.arena);
to_return.image = load_image(S8Fmt(scratch.arena, "assets/exported_3d/%.*s", S8VArg(image_filename)));
ser_u64(&ser, &to_return.num_vertices);
//Log("Mesh %.*s has %llu vertices and image filename '%.*s'\n", S8VArg(mesh_name), to_return.num_vertices, S8VArg(image_filename));
to_return.vertices = ArenaPush(arena, sizeof(*to_return.vertices) * to_return.num_vertices);
for(u64 i = 0; i < to_return.num_vertices; i++)
{
ser_Vertex(&ser, &to_return.vertices[i]);
}
assert(!ser.cur_error.failed);
ReleaseScratch(scratch);
to_return.loaded_buffer = sg_make_buffer(&(sg_buffer_desc)
{
.usage = SG_USAGE_IMMUTABLE,
.data = (sg_range){.ptr = to_return.vertices, .size = to_return.num_vertices * sizeof(Vertex)},
.label = (const char*)nullterm(arena, S8Fmt(arena, "%.*s-vertices", S8VArg(mesh_name))).str,
});
to_return.name = mesh_name;
return to_return;
}
// stored in row major
typedef struct
{
float elems[4 * 4];
} BlenderMat;
void ser_BlenderMat(SerState *ser, BlenderMat *b)
{
for(int i = 0; i < 4 * 4; i++)
{
ser_float(ser, &b->elems[i]);
}
}
Mat4 blender_to_handmade_mat(BlenderMat b)
{
Mat4 to_return;
assert(sizeof(to_return) == sizeof(b));
memcpy(&to_return, &b, sizeof(to_return));
return TransposeM4(to_return);
}
Mat4 transform_to_matrix(Transform t)
{
Mat4 to_return = M4D(1.0f);
to_return = MulM4(Scale(t.scale), to_return);
to_return = MulM4(QToM4(t.rotation), to_return);
to_return = MulM4(Translate(t.offset), to_return);
return to_return;
}
Transform lerp_transforms(Transform from, float t, Transform to)
{
return (Transform) {
.offset = LerpV3(from.offset, t, to.offset),
.rotation = SLerp(from.rotation, t, to.rotation),
.scale = LerpV3(from.scale, t, to.scale),
};
}
AABB lerp_aabbs(AABB from, float t, AABB to) {
return (AABB) {
.upper_left = LerpV2(from.upper_left, t, to.upper_left),
.lower_right = LerpV2(from.lower_right, t, to.lower_right),
};
}
Transform default_transform()
{
return (Transform){.rotation = Make_Q(0,0,0,1), .scale = V3(1,1,1)};
}
typedef struct
{
String8 name;
Bone *bones;
u64 bones_length;
Animation *animations;
u64 animations_length;
// when set, blends to that animation next time this armature is processed for that
String8 go_to_animation;
bool next_animation_isnt_looping;
Transform *current_poses; // allocated on loading of the armature
String8 currently_playing_animation; // CANNOT be null.
bool currently_playing_isnt_looping;
float animation_blend_t; // [0,1] how much between current_animation and target_animation. Once >= 1, current = target and target = null.
double cur_animation_time; // used for non looping animations to play once
Transform *anim_blended_poses; // recalculated once per frame depending on above parameters, which at the same code location are calculated. Is `bones_length` long
ArmatureVertex *vertices;
u64 vertices_length;
sg_buffer loaded_buffer;
uint64_t last_updated_bones_frame;
sg_image bones_texture;
sg_image image;
int bones_texture_width;
int bones_texture_height;
} Armature;
void initialize_animated_properties(Arena *arena, Armature *armature) {
armature->bones_texture = sg_make_image(&(sg_image_desc) {
.width = armature->bones_texture_width,
.height = armature->bones_texture_height,
.pixel_format = SG_PIXELFORMAT_RGBA8,
.usage = SG_USAGE_STREAM,
});
armature->current_poses = PushArray(arena, Transform, armature->bones_length);
armature->anim_blended_poses = PushArray(arena, Transform, armature->bones_length);
for(int i = 0; i < armature->bones_length; i++)
{
armature->anim_blended_poses[i] = (Transform){.scale = V3(1,1,1), .rotation = Make_Q(1,0,0,1)};
}
}
// sokol stuff needs to be cleaned up, because it's not allocated and managed on the gamestate arena...
// this cleans up the animated properties, not the images and vertex buffers, as those are loaded
// before the gamestate is created, and the purpose of this is to allow you to delete/recreate
// the gamestate without leaking resources.
void cleanup_armature(Armature *armature) {
sg_destroy_image(armature->bones_texture);
armature->bones_texture = (sg_image){0};
}
// still points to the source armature's vertex data, but all animation data is duplicated.
// the armature is allocated onto the arena
Armature *duplicate_armature(Arena *arena, Armature *source) {
Armature *ret = PushArrayZero(arena, Armature, 1);
*ret = *source;
initialize_animated_properties(arena, ret);
return ret;
}
// armature_name is used for debugging purposes, it has to effect on things
Armature load_armature(Arena *arena, String8 binary_file, String8 armature_name)
{
assert(binary_file.str);
ArenaTemp scratch = GetScratch(&arena, 1);
SerState ser = {
.data = binary_file.str,
.max = binary_file.size,
.arena = arena,
.error_arena = scratch.arena,
.serializing = false,
};
Armature to_return = {0};
bool is_armature;
ser_bool(&ser, &is_armature);
assert(is_armature);
ser_String8(&ser, &to_return.name, arena);
String8 image_filename;
ser_String8(&ser, &image_filename, scratch.arena);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
to_return.image = load_image(S8Fmt(scratch.arena, "assets/exported_3d/%.*s", S8VArg(image_filename)));
ser_u64(&ser, &to_return.bones_length);
//Log("Armature %.*s has %llu bones\n", S8VArg(armature_name), to_return.bones_length);
to_return.bones = PushArray(arena, Bone, to_return.bones_length);
for(u64 i = 0; i < to_return.bones_length; i++)
{
Bone *next_bone = &to_return.bones[i];
BlenderMat model_space_pose;
BlenderMat inverse_model_space_pose;
i32 parent_index;
ser_String8(&ser, &next_bone->name, arena);
ser_int(&ser, &parent_index);
ser_BlenderMat(&ser, &model_space_pose);
ser_BlenderMat(&ser, &inverse_model_space_pose);
ser_float(&ser, &next_bone->length);
next_bone->matrix_local = blender_to_handmade_mat(model_space_pose);
next_bone->inverse_model_space_pos = blender_to_handmade_mat(inverse_model_space_pose);
if(parent_index != -1)
{
if(parent_index < 0 || parent_index >= to_return.bones_length)
{
ser.cur_error = (SerError){.failed = true, .why = S8Fmt(arena, "Parent index deserialized %d is out of range of the pose bones, which has a size of %llu", parent_index, to_return.bones_length)};
}
else
{
next_bone->parent = &to_return.bones[parent_index];
}
}
}
ser_u64(&ser, &to_return.animations_length);
//Log("Armature %.*s has %llu animations\n", S8VArg(armature_name), to_return.animations_length);
to_return.animations = PushArray(arena, Animation, to_return.animations_length);
for(u64 i = 0; i < to_return.animations_length; i++)
{
Animation *new_anim = &to_return.animations[i];
*new_anim = (Animation){0};
ser_String8(&ser, &new_anim->name, arena);
new_anim->tracks = PushArray(arena, AnimationTrack, to_return.bones_length);
u64 frames_in_anim;
ser_u64(&ser, &frames_in_anim);
//Log("There are %llu animation frames in animation '%.*s'\n", frames_in_anim, S8VArg(new_anim->name));
for(u64 i = 0; i < to_return.bones_length; i++)
{
new_anim->tracks[i].poses = PushArray(arena, PoseBone, frames_in_anim);
new_anim->tracks[i].poses_length = frames_in_anim;
}
for(u64 anim_i = 0; anim_i < frames_in_anim; anim_i++)
{
float time_through;
ser_float(&ser, &time_through);
for(u64 pose_bone_i = 0; pose_bone_i < to_return.bones_length; pose_bone_i++)
{
PoseBone *next_pose_bone = &new_anim->tracks[pose_bone_i].poses[anim_i];
ser_Vec3(&ser, &next_pose_bone->parent_space_pose.offset);
ser_Quat(&ser, &next_pose_bone->parent_space_pose.rotation);
ser_Vec3(&ser, &next_pose_bone->parent_space_pose.scale);
next_pose_bone->time = time_through;
}
}
}
ser_u64(&ser, &to_return.vertices_length);
to_return.vertices = PushArray(arena, ArmatureVertex, to_return.vertices_length);
for(u64 i = 0; i < to_return.vertices_length; i++)
{
ser_Vec3(&ser, &to_return.vertices[i].position);
ser_Vec2(&ser, &to_return.vertices[i].uv);
u16 joint_indices[4];
float joint_weights[4];
for(int ii = 0; ii < 4; ii++)
ser_u16(&ser, &joint_indices[ii]);
for(int ii = 0; ii < 4; ii++)
ser_float(&ser, &joint_weights[ii]);
for(int ii = 0; ii < 4; ii++)
to_return.vertices[i].joint_indices[ii] = joint_indices[ii];
for(int ii = 0; ii < 4; ii++)
to_return.vertices[i].joint_weights[ii] = joint_weights[ii];
}
//Log("Armature %.*s has %llu vertices\n", S8VArg(armature_name), to_return.vertices_length);
assert(!ser.cur_error.failed);
ReleaseScratch(scratch);
to_return.loaded_buffer = sg_make_buffer(&(sg_buffer_desc)
{
.usage = SG_USAGE_IMMUTABLE,
.data = (sg_range){.ptr = to_return.vertices, .size = to_return.vertices_length * sizeof(ArmatureVertex)},
.label = (const char*)nullterm(arena, S8Fmt(arena, "%.*s-vertices", S8VArg(armature_name))).str,
});
to_return.bones_texture_width = 16;
to_return.bones_texture_height = (int)to_return.bones_length;
//Log("Armature %.*s has bones texture size (%d, %d)\n", S8VArg(armature_name), to_return.bones_texture_width, to_return.bones_texture_height);
initialize_animated_properties(arena, &to_return);
// a sanity check
SLICE_ITER(Bone, to_return.bones)
{
Mat4 should_be_identity = MulM4(it->matrix_local, it->inverse_model_space_pos);
for(int r = 0; r < 4; r++)
{
for(int c = 0; c < 4; c++)
{
const float eps = 0.0001f;
if(r == c)
{
assert(fabsf(should_be_identity.Elements[c][r] - 1.0f) < eps);
}
else
{
assert(fabsf(should_be_identity.Elements[c][r] - 0.0f) < eps);
}
}
}
}
return to_return;
}
typedef struct CollisionCylinder
{
struct CollisionCylinder *next;
Circle bounds;
} CollisionCylinder;
typedef struct Room
{
struct Room *next;
struct Room *prev;
bool camera_offset_is_overridden;
Vec3 camera_offset;
String8 name;
u64 roomid;
PlacedMesh *placed_mesh_list;
CollisionCylinder *collision_list;
PlacedEntity *placed_entity_list;
} Room;
typedef struct
{
Mesh *mesh_list;
Room *room_list_first;
Room *room_list_last;
} ThreeDeeLevel;
void ser_BlenderTransform(SerState *ser, BlenderTransform *t)
{
ser_Vec3(ser, &t->pos);
ser_Vec3(ser, &t->euler_rotation);
ser_Vec3(ser, &t->scale);
}
Transform blender_to_game_transform(BlenderTransform blender_transform)
{
Transform to_return = {0};
to_return.offset = blender_transform.pos;
to_return.scale.x = blender_transform.scale.x;
to_return.scale.y = blender_transform.scale.z;
to_return.scale.z = blender_transform.scale.y;
Mat4 rotation_matrix = M4D(1.0f);
rotation_matrix = MulM4(Rotate_RH(AngleRad(blender_transform.euler_rotation.x), V3(1,0,0)), rotation_matrix);
rotation_matrix = MulM4(Rotate_RH(AngleRad(blender_transform.euler_rotation.y), V3(0,0,-1)), rotation_matrix);
rotation_matrix = MulM4(Rotate_RH(AngleRad(blender_transform.euler_rotation.z), V3(0,1,0)), rotation_matrix);
Quat out_rotation = M4ToQ_RH(rotation_matrix);
to_return.rotation = out_rotation;
return to_return;
}
ThreeDeeLevel load_level(Arena *arena, String8 binary_file)
{
ArenaTemp scratch = GetScratch(&arena, 1);
SerState ser = {
.data = binary_file.str,
.max = binary_file.size,
.arena = arena,
.error_arena = scratch.arena,
.serializing = false,
};
ThreeDeeLevel out = {0};
u64 num_rooms = 0;
ser_u64(&ser, &num_rooms);
for(u64 i = 0; i < num_rooms; i++)
{
Room *new_room = PushArray(arena, Room, 1);
ser_String8(&ser, &new_room->name, arena);
ser_u64(&ser, &new_room->roomid);
ser_bool(&ser, &new_room->camera_offset_is_overridden);
if(new_room->camera_offset_is_overridden)
{
ser_Vec3(&ser, &new_room->camera_offset);
}
// placed meshes
{
u64 num_placed = 0;
ser_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for (u64 i = 0; i < num_placed; i++)
{
PlacedMesh *new_placed = PushArray(arena, PlacedMesh, 1);
// PlacedMesh *new_placed = calloc(sizeof(PlacedMesh), 1);
ser_String8(&ser, &new_placed->name, arena);
BlenderTransform blender_transform = {0};
ser_BlenderTransform(&ser, &blender_transform);
new_placed->t = blender_to_game_transform(blender_transform);
StackPush(new_room->placed_mesh_list, new_placed);
// Log("Placed mesh '%.*s' pos %f %f %f rotation %f %f %f %f scale %f %f %f\n", S8VArg(placed_mesh_name), v3varg(new_placed->t.offset), qvarg(new_placed->t.rotation), v3varg(new_placed->t.scale));
// load the mesh if we haven't already
bool mesh_found = false;
for (Mesh *cur = out.mesh_list; cur; cur = cur->next)
{
if (S8Match(cur->name, new_placed->name, 0))
{
mesh_found = true;
new_placed->draw_with = cur;
assert(cur->name.size > 0);
break;
}
}
if (!mesh_found)
{
String8 to_load_filepath = S8Fmt(scratch.arena, "assets/exported_3d/%.*s.bin", S8VArg(new_placed->name));
// Log("Loading mesh '%.*s'...\n", S8VArg(to_load_filepath));
String8 binary_mesh_file = LoadEntireFile(scratch.arena, to_load_filepath);
if (!binary_mesh_file.str)
{
ser.cur_error = (SerError){.failed = true, .why = S8Fmt(ser.error_arena, "Couldn't load file '%.*s'", to_load_filepath)};
}
else
{
Mesh *new_mesh = PushArray(arena, Mesh, 1);
*new_mesh = load_mesh(arena, binary_mesh_file, new_placed->name);
StackPush(out.mesh_list, new_mesh);
new_placed->draw_with = new_mesh;
}
}
}
}
u64 num_collision_cubes;
ser_u64(&ser, &num_collision_cubes);
for (u64 i = 0; i < num_collision_cubes; i++)
{
CollisionCylinder *new_cylinder = PushArray(arena, CollisionCylinder, 1);
Vec2 twodee_pos;
Vec2 size;
ser_Vec2(&ser, &twodee_pos);
ser_Vec2(&ser, &size);
new_cylinder->bounds.center = twodee_pos;
new_cylinder->bounds.radius = (size.x + size.y) * 0.5f; // @TODO(Phillip): @Temporary
StackPush(new_room->collision_list, new_cylinder);
}
// placed entities
{
u64 num_placed = 0;
ser_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
assert(num_placed == 0); // not thinking about how to go from name to entity kind right now, but in the future this will be for like machines or interactible things like the fishing rod
}
DblPushBack(out.room_list_first, out.room_list_last, new_room);
}
assert(!ser.cur_error.failed);
ReleaseScratch(scratch);
return out;
}
#include "assets.gen.c"
#include "threedee.glsl.h"
AABB level_aabb = { .upper_left = { 0.0f, 0.0f }, .lower_right = { TILE_SIZE * LEVEL_TILES, -(TILE_SIZE * LEVEL_TILES) } };
ThreeDeeLevel level_threedee = {0};
GameState gs = { 0 };
bool flycam = false;
Vec3 flycam_pos = {0};
float flycam_horizontal_rotation = 0.0;
float flycam_vertical_rotation = 0.0;
float flycam_speed = 1.0f;
Mat4 view = {0};
Mat4 projection = {0};
Room mystery_room = {
.name = S8LitC("???"),
};
Room *get_room(ThreeDeeLevel *level, u64 roomid) {
Room *ret = &mystery_room;
for(Room *cur = level->room_list_first; cur; cur = cur->next)
{
if(cur->roomid == roomid)
{
ret = cur;
break;
}
}
return ret;
}
Room *get_cur_room(GameState *gs, ThreeDeeLevel *level)
{
Room *in_room = 0;
if(gs->edit.enabled) {
in_room = get_room(level, gs->edit.current_roomid);
} else {
in_room = get_room(level, gs->current_roomid);
}
assert(in_room);
return in_room;
}
int num_rooms(ThreeDeeLevel *level) {
int ret = 0;
for(Room *cur = level->room_list_first; cur; cur = cur->next) {
ret++;
}
return ret;
}
Entity *npcs_entity(NpcKind kind) {
if(kind == NPC_nobody) return 0;
Entity *ret = 0;
ENTITIES_ITER(gs.entities)
{
if (it->is_npc && it->npc_kind == kind)
{
assert(!ret); // no duplicate entities for this character. Bad juju when more than one npc
ret = it;
}
}
return ret;
}
Vec4 IsPoint(Vec3 point)
{
return V4(point.x, point.y, point.z, 1.0f);
}
Vec3 MulM4V3(Mat4 m, Vec3 v)
{
return MulM4V4(m, IsPoint(v)).xyz;
}
typedef struct
{
Vec3 right; // X+
Vec3 forward; // Z-
Vec3 up;
} Basis;
void print_matrix(Mat4 m)
{
for(int r = 0; r < 4; r++)
{
for(int c = 0; c < 4; c++)
{
printf("%f ", m.Elements[c][r]);
}
printf("\n");
//printf("%f %f %f %f\n", m.Columns[i].x, m.Columns[i].y, m.Columns[i].z, m.Columns[i].w);
}
printf("\n");
for(int r = 0; r < 4; r++)
{
printf("%f %f %f %f\n", m.Columns[0].Elements[r], m.Columns[1].Elements[r], m.Columns[2].Elements[r], m.Columns[3].Elements[r]);
}
}
Basis flycam_basis()
{
// This basis function is wrong. Do not use it. Something about the order of rotations for
// each basis vector is screwey
Basis to_return = {
.forward = V3(0,0,-1),
.right = V3(1,0,0),
.up = V3(0, 1, 0),
};
Mat4 rotate_horizontal = Rotate_RH(flycam_horizontal_rotation, V3(0, 1, 0));
Mat4 rotate_vertical = Rotate_RH(flycam_vertical_rotation, V3(1, 0, 0));
to_return.forward = MulM4V4(rotate_horizontal, MulM4V4(rotate_vertical, IsPoint(to_return.forward))).xyz;
to_return.right = MulM4V4(rotate_horizontal, MulM4V4(rotate_vertical, IsPoint(to_return.right))).xyz;
to_return.up = MulM4V4(rotate_horizontal, MulM4V4(rotate_vertical, IsPoint(to_return.up))).xyz;
return to_return;
}
Mat4 flycam_matrix()
{
Basis basis = flycam_basis();
Mat4 to_return = {0};
to_return.Columns[0] = IsPoint(basis.right);
to_return.Columns[1] = IsPoint(basis.up);
to_return.Columns[2] = IsPoint(basis.forward);
to_return.Columns[3] = IsPoint(flycam_pos);
return to_return;
}
#define S8LitConst(s) \
{ \
(u8 *)(s), sizeof(s) - 1 \
}
String8 showing_secret_str = S8LitConst("");
float showing_secret_alpha = 0.0f;
PathCache cached_paths[32] = {0};
bool is_path_cache_old(double elapsed_time, PathCache *cache)
{
double time_delta = elapsed_time - cache->elapsed_time;
if (time_delta < 0.0)
{
// path was cached in the future... likely from old save or something. Always invalidate
return true;
}
else
{
return time_delta >= TIME_BETWEEN_PATH_GENS;
}
}
PathCacheHandle cache_path(double elapsed_time, AStarPath *path)
{
ARR_ITER_I(PathCache, cached_paths, i)
{
if (!it->exists || is_path_cache_old(elapsed_time, it))
{
int gen = it->generation;
*it = (PathCache){0};
it->generation = gen + 1;
it->path = *path;
it->elapsed_time = elapsed_time;
it->exists = true;
return (PathCacheHandle){.generation = it->generation, .index = i};
}
}
return (PathCacheHandle){0};
}
// passes in the time to return 0 and invalidate if too old
PathCache *get_path_cache(double elapsed_time, PathCacheHandle handle)
{
if (handle.generation == 0)
{
return 0;
}
else
{
assert(handle.index >= 0);
assert(handle.index < ARRLEN(cached_paths));
PathCache *to_return = &cached_paths[handle.index];
if (to_return->exists && to_return->generation == handle.generation)
{
if (is_path_cache_old(elapsed_time, to_return))
{
to_return->exists = false;
return 0;
}
else
{
return to_return;
}
}
else
{
return 0;
}
}
}
double unprocessed_gameplay_time = 0.0;
#define MINIMUM_TIMESTEP (1.0 / 60.0)
EntityRef frome(Entity *e)
{
if (e == 0)
{
return (EntityRef){0};
}
else
{
EntityRef to_return = {
.index = (int)(e - gs.entities),
.generation = e->generation,
};
assert(to_return.index >= 0);
assert(to_return.index < ARRLEN(gs.entities));
return to_return;
}
}
Entity *gete(EntityRef ref)
{
return gete_specified(&gs, ref);
}
void push_memory(GameState *gs, Entity *e, Memory new_memory)
{
Memory *memory_allocated = 0;
if (memories_free_list)
{
memory_allocated = memories_free_list;
StackPop(memories_free_list);
}
else
{
memory_allocated = PushArray(persistent_arena, Memory, 1);
}
*memory_allocated = new_memory;
int count = 0;
for (Memory *cur = e->memories_first; cur; cur = cur->next)
count += 1;
while (count >= REMEMBERED_MEMORIES)
{
Memory *to_remove = e->memories_first;
DblRemove(e->memories_first, e->memories_last, to_remove);
StackPush(memories_free_list, to_remove);
count -= 1;
}
if (gs->stopped_time)
StackPush(e->memories_added_while_time_stopped, memory_allocated);
else
DblPushBack(e->memories_first, e->memories_last, memory_allocated);
if (!new_memory.context.i_said_this)
{
// self speech doesn't dirty
e->perceptions_dirty = true;
}
}
CanTalkTo get_can_talk_to(Entity *e)
{
CanTalkTo to_return = {0};
ENTITIES_ITER(gs.entities)
{
if (it != e && (it->is_npc) && it->current_roomid == e->current_roomid)
{
BUFF_APPEND(&to_return, it);
}
}
return to_return;
}
Entity *get_targeted(Entity *from, NpcKind targeted)
{
ENTITIES_ITER(gs.entities)
{
if (it != from && (it->is_npc) && it->npc_kind == targeted)
{
if (it->current_roomid == from->current_roomid)
{
return it;
}
}
}
return 0;
}
Memory make_memory(ActionOld a, MemoryContext context)
{
Memory new_memory = {0};
new_memory.action = a;
new_memory.context = context;
return new_memory;
}
void remember_action(GameState *gs, Entity *to_modify, ActionOld a, MemoryContext context)
{
Memory new_memory = make_memory(a, context);
push_memory(gs, to_modify, new_memory);
if (context.i_said_this && (a.speech.text_length > 0 || a.kind != ACT_none))
{
to_modify->undismissed_action = true;
to_modify->undismissed_action_tick = gs->tick;
to_modify->characters_of_word_animated = 0.0f;
to_modify->words_said_on_page = 0;
to_modify->cur_page_index = 0;
}
}
u8 CharToUpper(u8 c);
String8 npc_identifier(String8 name)
{
String8 ret;
ret.str = PushArray(frame_arena, u8, name.size);
ret.size = name.size;
for (u64 i = 0; i < ret.size; i++)
{
ret.str[i] = CharToUpper(name.str[i]);
}
return ret;
}
// bad helper for now.
String8 npc_identifier_chunk(TextChunk chunk)
{
return npc_identifier(TextChunkString8(chunk));
}
// returns reason why allocated on arena if invalid
// to might be null here, from can't be null
String8 is_action_valid(Arena *arena, Entity *from, ActionOld a)
{
assert(a.speech.text_length <= MAX_SENTENCE_LENGTH && a.speech.text_length >= 0);
assert(a.kind >= 0 && a.kind < ARRLEN(actions));
assert(from);
String8 error_message = (String8){0};
if (error_message.size == 0 && a.speech.text_length > 0)
{
if (S8FindSubstring(TextChunkString8(a.speech), S8Lit("assist"), 0, StringMatchFlag_CaseInsensitive) != a.speech.text_length)
{
error_message = S8Lit("You cannot use the word 'assist' in any form, you are not an assistant, do not act overtly helpful");
}
}
CanTalkTo talk = get_can_talk_to(from);
if (error_message.size == 0 && a.talking_to_kind)
{
bool found = false;
BUFF_ITER(Entity *, &talk)
{
if ((*it)->npc_kind == a.talking_to_kind)
{
found = true;
break;
}
}
if (!found)
{
error_message = FmtWithLint(arena, "Character you're talking to, %.*s, isn't in the same room and so can't be talked to", S8VArg(npc_identifier_chunk(npc_data(&gs, a.talking_to_kind)->name)));
}
}
if (error_message.size == 0 && a.kind == ACT_leave && gete(from->joined) == 0)
{
error_message = S8Lit("You can't leave somebody unless you joined them.");
}
if (error_message.size == 0 && a.kind == ACT_join && gete(from->joined) != 0)
{
error_message = FmtWithLint(arena, "You can't join somebody, you're already in %.*s's party", TextChunkVArg(npc_data(&gs, gete(from->joined)->npc_kind)->name));
}
if (error_message.size == 0 && a.kind == ACT_fire_shotgun && gete(from->aiming_shotgun_at) == 0)
{
error_message = S8Lit("You can't fire your shotgun without aiming it first");
}
if (error_message.size == 0 && a.kind == ACT_put_shotgun_away && gete(from->aiming_shotgun_at) == 0)
{
error_message = S8Lit("You can't put your shotgun away without aiming it first");
}
bool target_is_character = a.kind == ACT_join || a.kind == ACT_aim_shotgun;
if (error_message.size == 0 && target_is_character)
{
bool arg_valid = false;
BUFF_ITER(Entity *, &talk)
{
if ((*it)->npc_kind == a.argument.targeting)
arg_valid = true;
}
if (arg_valid == false)
{
error_message = FmtWithLint(arena, "Your action.argument for who the action `%s` be directed at, %.*s, is either invalid (you can't operate on nobody) or it's not an NPC that's near you right now.", actions[a.kind].name, TextChunkVArg(npc_data(&gs, a.argument.targeting)->name));
}
}
if (error_message.size == 0)
{
AvailableActions available = {0};
fill_available_actions(&gs, from, &available);
bool found = false;
String8List action_strings_list = {0};
BUFF_ITER(ActionKind, &available)
{
S8ListPush(arena, &action_strings_list, S8CString(actions[*it].name));
if (*it == a.kind)
found = true;
}
if (!found)
{
String8 action_strings = S8ListJoin(arena, action_strings_list, &(StringJoin){.mid = S8Lit(", ")});
error_message = FmtWithLint(arena, "You cannot perform action %s right now, you can only perform these actions: [%.*s]", actions[a.kind].name, S8VArg(action_strings));
}
}
assert(error_message.size < MAX_SENTENCE_LENGTH); // is copied into text chunks
return error_message;
}
// from must not be null
// the action must have been validated to be valid if you're calling this
void cause_action_side_effects(Entity *from, ActionOld a)
{
assert(from);
ArenaTemp scratch = GetScratch(0, 0);
String8 failure_reason = is_action_valid(scratch.arena, from, a);
if (failure_reason.size > 0)
{
Log("Failed to process action, invalid action: `%.*s`\n", S8VArg(failure_reason));
assert(false);
}
Entity *to = 0;
if (a.talking_to_kind != NPC_nobody)
{
to = get_targeted(from, a.talking_to_kind);
assert(to);
}
if (to)
{
from->looking_at = frome(to);
}
if (a.kind == ACT_join)
{
Entity *target = get_targeted(from, a.argument.targeting);
assert(target); // error checked in is_action_valid
from->joined = frome(target);
}
if (a.kind == ACT_leave)
{
from->joined = (EntityRef){0};
}
if (a.kind == ACT_aim_shotgun)
{
Entity *target = get_targeted(from, a.argument.targeting);
assert(target); // error checked in is_action_valid
from->aiming_shotgun_at = frome(target);
}
if (a.kind == ACT_fire_shotgun)
{
assert(gete(from->aiming_shotgun_at));
gete(from->aiming_shotgun_at)->killed = true;
}
ReleaseScratch(scratch);
}
// only called when the action is instantiated, correctly propagates the information
// of the action physically and through the party
// If the action is invalid, remembers the error if it's an NPC, and does nothing else
// Returns if the action was valid or not
bool perform_action(GameState *gs, Entity *from, ActionOld a)
{
ArenaTemp scratch = GetScratch(0, 0);
MemoryContext context = {0};
context.author_npc_kind = from->npc_kind;
if (a.speech.text_length > 0)
from->dialog_fade = 2.5f;
String8 is_valid = is_action_valid(scratch.arena, from, a);
bool proceed_propagating = true;
if (is_valid.size > 0)
{
assert(from->npc_kind != NPC_player);
append_to_errors(from, make_memory(a, context), is_valid);
proceed_propagating = false;
}
Entity *targeted = 0;
if (proceed_propagating)
{
targeted = get_targeted(from, a.talking_to_kind);
if (from->errorlist_first)
StackPush(remembered_error_free_list, from->errorlist_first);
from->errorlist_first = 0;
from->errorlist_last = 0;
cause_action_side_effects(from, a);
// self memory
if (from->npc_kind != NPC_player)
{
MemoryContext my_context = context;
my_context.i_said_this = true;
remember_action(gs, from, a, my_context);
}
if (a.speech.text_length == 0 && a.kind == ACT_none)
{
proceed_propagating = false; // didn't say or do anything
}
}
if (proceed_propagating)
{
// memory of target
if (targeted)
{
remember_action(gs, targeted, a, context);
}
// propagate to other npcs in the room
ENTITIES_ITER(gs->entities)
{
if (
it->npc_kind != NPC_player && it->current_roomid == from->current_roomid && it != from && it != targeted)
{
remember_action(gs, it, a, context);
}
}
}
ReleaseScratch(scratch);
return proceed_propagating;
}
bool eq(EntityRef ref1, EntityRef ref2)
{
return ref1.index == ref2.index && ref1.generation == ref2.generation;
}
Entity *new_entity(GameState *gs)
{
for (int i = 0; i < ARRLEN(gs->entities); i++)
{
if (!gs->entities[i].exists)
{
Entity *to_return = &gs->entities[i];
int gen = to_return->generation;
*to_return = (Entity){0};
to_return->exists = true;
to_return->generation = gen + 1;
return to_return;
}
}
assert(false);
return 0;
}
typedef struct ToVisit
{
struct ToVisit *next;
struct ToVisit *prev;
Node *ptr;
int depth;
} ToVisit;
bool in_arr(ToVisit *arr, Node *n)
{
for (ToVisit *cur = arr; cur; cur = cur->next)
{
if (cur->ptr == n)
return true;
}
return false;
}
void dump_nodes(Node *node)
{
ArenaTemp scratch = GetScratch(0, 0);
ToVisit *horizon_first = 0;
ToVisit *horizon_last = 0;
ToVisit *visited = 0;
ToVisit *first = PushArrayZero(scratch.arena, ToVisit, 1);
first->ptr = node;
DblPushBack(horizon_first, horizon_last, first);
while (horizon_first)
{
ToVisit *cur_tovisit = horizon_first;
DblRemove(horizon_first, horizon_last, cur_tovisit);
StackPush(visited, cur_tovisit);
char *tagstr = " ";
if (cur_tovisit->ptr->kind == NodeKind_Tag)
tagstr = "TAG";
printf("%s", tagstr);
for (int i = 0; i < cur_tovisit->depth; i++)
printf(" |");
printf(" `%.*s`\n", S8VArg(cur_tovisit->ptr->string));
for (Node *cur = cur_tovisit->ptr->first_child; !NodeIsNil(cur); cur = cur->next)
{
if (!in_arr(visited, cur))
{
ToVisit *new = PushArrayZero(scratch.arena, ToVisit, 1);
new->depth = cur_tovisit->depth + 1;
new->ptr = cur;
DblPushFront(horizon_first, horizon_last, new);
}
}
for (Node *cur = cur_tovisit->ptr->first_tag; !NodeIsNil(cur); cur = cur->next)
{
if (!in_arr(visited, cur))
{
ToVisit *new = PushArrayZero(scratch.arena, ToVisit, 1);
new->depth = cur_tovisit->depth + 1;
new->ptr = cur;
DblPushFront(horizon_first, horizon_last, new);
}
}
}
ReleaseScratch(scratch);
}
// allocates the error on the arena
Node *expect_childnode(Arena *arena, Node *parent, String8 string, String8List *errors)
{
Node *to_return = NilNode();
if (errors->node_count == 0)
{
Node *child_node = MD_ChildFromString(parent, string, 0);
if (NodeIsNil(child_node))
{
PushWithLint(arena, errors, "Couldn't find expected field %.*s", S8VArg(string));
}
else
{
to_return = child_node;
}
}
return to_return;
}
int parse_enumstr_impl(Arena *arena, String8 enum_str, char **enumstr_array, int enumstr_array_length, String8List *errors, char *enum_kind_name, char *prefix)
{
ArenaTemp scratch = GetScratch(&arena, 1);
int to_return = -1;
if (errors->node_count == 0)
{
String8 enum_name_looking_for = enum_str;
if (enum_name_looking_for.size == 0)
{
PushWithLint(arena, errors, "`%s` string must be of size greater than 0", enum_kind_name);
}
else
{
for (int i = 0; i < enumstr_array_length; i++)
{
if (S8Match(FmtWithLint(scratch.arena, "%s%s", prefix, enumstr_array[i]), enum_name_looking_for, 0))
{
to_return = i;
break;
}
}
}
}
if (to_return == -1)
{
PushWithLint(arena, errors, "The %s `%.*s` could not be recognized in the game", enum_kind_name, S8VArg(enum_str));
}
ReleaseScratch(scratch);
return to_return;
}
Vec3 plane_point(Vec2 p)
{
return V3(p.x, 0.0, p.y);
}
Vec2 point_plane(Vec3 p)
{
return V2(p.x, p.z);
}
#define parse_enumstr(arena, enum_str, errors, string_array, enum_kind_name, prefix) parse_enumstr_impl(arena, enum_str, string_array, ARRLEN(string_array), errors, enum_kind_name, prefix)
void transition_to_room(GameState *gs, ThreeDeeLevel *level, u64 new_roomid)
{
Room *new_room = get_room(level, new_roomid);
Log("Transitioning to %.*s...\n", S8VArg(new_room->name));
assert(gs);
gs->current_roomid = new_roomid;
}
// no crash or anything bad if called on an already cleaned up gamestate
void cleanup_gamestate(GameState *gs) {
if(gs) {
ENTITIES_ITER(gs->entities) {
if(it->armature) {
cleanup_armature(it->armature);
it->armature = 0;
}
}
if(gs->arena) {
ArenaRelease(gs->arena);
}
memset(gs, 0, sizeof(GameState));
}
}
// this can be called more than once, it cleanly handles all states of the gamestate
void initialize_gamestate(GameState *gs, u64 roomid) {
if(!gs->arena) gs->arena = ArenaAlloc();
if(!world(gs)) {
Entity *world = new_entity(gs);
world->is_world = true;
}
gs->current_roomid = roomid;
rnd_gamerand_seed(&gs->random, RANDOM_SEED);
}
void reset_level()
{
Log("STUB\n");
// you prob want to do something like all dead entities are alive and reset to their editor positions.
// This means entities need an editor spawnpoint position and a gameplay position....
}
enum
{
V0,
V1,
// V2-V4 skipped because they're vector macros lol
V5,
VEditingDialog,
VDescription,
VDead,
VMax,
} Version;
#define SER_BUFF(ser, BuffElemType, buff_ptr) \
{ \
ser_int(ser, &((buff_ptr)->cur_index)); \
if ((buff_ptr)->cur_index > ARRLEN((buff_ptr)->data)) \
{ \
ser->cur_error = (SerError){.failed = true, .why = S8Fmt(ser->error_arena, "Current index %d is more than the buffer %s's maximum, %d", (buff_ptr)->cur_index, #buff_ptr, ARRLEN((buff_ptr)->data))}; \
} \
BUFF_ITER(BuffElemType, buff_ptr) \
{ \
ser_##BuffElemType(ser, it); \
} \
}
void ser_TextChunk(SerState *ser, TextChunk *t)
{
ser_int(ser, &t->text_length);
if (t->text_length >= ARRLEN(t->text))
{
ser->cur_error = (SerError){.failed = true, .why = S8Fmt(ser->error_arena, "In text chunk, length %d is too big to fit into %d", t->text_length, ARRLEN(t->text))};
}
ser_bytes(ser, (u8 *)t->text, t->text_length);
}
void ser_Entity(SerState *ser, Entity *e)
{
ser_bool(ser, &e->exists);
ser_bool(ser, &e->destroy);
ser_int(ser, &e->generation);
ser_Vec2(ser, &e->pos);
ser_Vec2(ser, &e->vel);
ser_float(ser, &e->damage);
if(ser->version >= VDead) {
ser_bool(ser, &e->dead);
ser_u64(ser, &e->current_roomid);
}
ser_bool(ser, &e->is_world);
ser_bool(ser, &e->is_npc);
ser_bool(ser, &e->being_hovered);
ser_bool(ser, &e->perceptions_dirty);
/* rememboring errorlist is disabled for being a bad idea because as game is improved the errors go out of date, and to begin with it's not even that necessary
But also it's too hard to maintain
if(ser->serializing)
{
RememberedError *cur = e->errorlist_first;
bool more_errors = cur != 0;
ser_bool(ser, &more_errors);
while(more_errors)
{
ser_TextChunk(ser, &cur->reason_why_its_bad);
ser_Memory(ser, &cur->offending_self_output)
cur = cur->next;
more_errors = cur != 0;
ser_bool(ser, &more_errors);
}
}
else
{
bool more_errors;
ser_bool(ser, &more_errors);
while(more_errors)
{
TextChunkList *new_chunk = PushArray(ser->arena, TextChunkList, 1);
ser_TextChunk(ser, &new_chunk->text);
DblPushBack(e->errorlist_first, e->errorlist_last, new_chunk);
ser_bool(ser, &more_errors);
}
}
*/
if (ser->serializing)
{
Memory *cur = e->memories_first;
bool more_memories = cur != 0;
ser_bool(ser, &more_memories);
while (more_memories)
{
ser_Memory(ser, cur);
cur = cur->next;
more_memories = cur != 0;
ser_bool(ser, &more_memories);
}
}
else
{
bool more_memories;
ser_bool(ser, &more_memories);
while (more_memories)
{
Memory *new_chunk = PushArray(ser->arena, Memory, 1);
ser_Memory(ser, new_chunk);
DblPushBack(e->memories_first, e->memories_last, new_chunk);
ser_bool(ser, &more_memories);
}
}
ser_float(ser, &e->dialog_panel_opacity);
ser_bool(ser, &e->undismissed_action);
ser_uint64_t(ser, &e->undismissed_action_tick);
ser_float(ser, &e->characters_of_word_animated);
ser_int(ser, &e->words_said_on_page);
ser_int(ser, &e->cur_page_index);
ser_NpcKind(ser, &e->npc_kind);
ser_int(ser, &e->gen_request_id);
ser_Vec2(ser, &e->target_goto);
SER_BUFF(ser, Vec2, &e->position_history);
ser_EntityRef(ser, &e->talking_to);
}
void ser_Npc(SerState *ser, Npc *npc)
{
ser_TextChunk(ser, &npc->name);
if(ser->version >= VDescription)
ser_TextChunk(ser, &npc->description);
ser_int(ser, &npc->kind);
}
void ser_EditorState(SerState *ser, EditorState *ed) {
ser_bool(ser, &ed->enabled);
ser_u64(ser, &ed->current_roomid);
ser_Vec2(ser, &ed->camera_panning_target);
ser_Vec2(ser, &ed->camera_panning);
ser_NpcKind(ser, &ed->placing_npc);
ser_NpcKind(ser, &ed->editing_npc);
if(ser->version >= VEditingDialog)
ser_bool(ser, &ed->editing_dialog_open);
ser_bool(ser, &ed->placing_spawn);
ser_u64(ser, &ed->player_spawn_roomid);
ser_Vec2(ser, &ed->player_spawn_position);
}
void ser_GameState(SerState *ser, GameState *gs)
{
if (ser->serializing)
ser->version = VMax - 1;
ser_int(ser, &ser->version);
if (ser->version >= VMax)
{
ser->cur_error = (SerError){.failed = true, .why = S8Fmt(ser->error_arena, "Version %d is beyond the current version, %d", ser->version, VMax - 1)};
}
ser_uint64_t(ser, &gs->tick);
ser_bool(ser, &gs->won);
ser_EditorState(ser, &gs->edit);
ser_double(ser, &gs->time);
SER_BUFF(ser, Npc, &gs->characters);
int num_entities = MAX_ENTITIES;
ser_int(ser, &num_entities);
assert(num_entities <= MAX_ENTITIES);
for (int i = 0; i < num_entities; i++)
{
ser_bool(ser, &gs->entities[i].exists);
if (gs->entities[i].exists)
{
ser_Entity(ser, &(gs->entities[i]));
}
}
if (!ser->cur_error.failed)
{
if (world(gs) == 0)
{
ser->cur_error = (SerError){.failed = true, .why = S8Lit("No world entity found in deserialized entities")};
}
}
if(!ser->serializing)
initialize_gamestate(gs, gs->current_roomid);
}
// error_out is allocated onto arena if it fails
String8 save_to_string(Arena *output_bytes_arena, Arena *error_arena, String8 *error_out, GameState *gs)
{
u8 *serialized_data = 0;
u64 serialized_length = 0;
{
SerState ser = {
.version = VMax - 1,
.serializing = true,
.error_arena = error_arena,
};
ser_GameState(&ser, gs);
if (ser.cur_error.failed)
{
*error_out = ser.cur_error.why;
}
else
{
ser.arena = 0; // serialization should never require allocation
ser.max = ser.cur;
ser.cur = 0;
ser.version = VMax - 1;
ArenaTemp temp = ArenaBeginTemp(output_bytes_arena);
serialized_data = ArenaPush(temp.arena, ser.max);
ser.data = serialized_data;
ser_GameState(&ser, gs);
if (ser.cur_error.failed)
{
Log("Very weird that serialization fails a second time...\n");
*error_out = S8Fmt(error_arena, "VERY BAD Serialization failed after it already had no error: %.*s", ser.cur_error.why);
ArenaEndTemp(temp);
serialized_data = 0;
}
else
{
serialized_length = ser.cur;
}
}
}
return S8(serialized_data, serialized_length);
}
// error strings are allocated on error_arena, probably scratch for that. If serialization fails,
// nothing is allocated onto arena, the allocations are rewound
// If there was an error, the gamestate returned might be partially constructed and bad. Don't use it
GameState *load_from_string(Arena *arena, Arena *error_arena, String8 data, String8 *error_out)
{
ArenaTemp temp = ArenaBeginTemp(arena);
SerState ser = {
.serializing = false,
.data = data.str,
.max = data.size,
.arena = temp.arena,
.error_arena = error_arena,
};
GameState *to_return = PushArrayZero(temp.arena, GameState, 1);
ser_GameState(&ser, to_return);
if (ser.cur_error.failed)
{
cleanup_gamestate(to_return);
ArenaEndTemp(temp); // no allocations if it fails
*error_out = ser.cur_error.why;
}
return to_return;
}
#ifdef WEB
EMSCRIPTEN_KEEPALIVE
void dump_save_data()
{
ArenaTemp scratch = GetScratch(0, 0);
String8 error = {0};
String8 saved = save_to_string(scratch.arena, scratch.arena, &error, &gs);
if (error.size > 0)
{
Log("Failed to save game: %.*s\n", S8VArg(error));
}
else
{
EM_ASM({
save_game_data = new Int8Array(Module.HEAP8.buffer, $0, $1);
},
(char *)(saved.str), saved.size);
}
ReleaseScratch(scratch);
}
EMSCRIPTEN_KEEPALIVE
void read_from_save_data(char *data, size_t length)
{
ArenaTemp scratch = GetScratch(0, 0);
String8 data_str = S8((u8 *)data, length);
String8 error = {0};
GameState new_gs = load_from_string(persistent_arena, scratch.arena, data_str, &error);
if (error.size > 0)
{
Log("Failed to load from size %lu: %.*s\n", length, S8VArg(error));
}
else
{
gs = new_gs;
}
ReleaseScratch(scratch);
}
#endif
// a callback, when 'text backend' has finished making text. End dialog
void end_text_input(char *what_was_entered_cstr)
{
String8 gotten = S8CString(what_was_entered_cstr);
if (gotten.size == 0)
{
// means cancelled, so can do nothing
}
else
{
String8 trimmed = S8Chop(gotten, MAX_SENTENCE_LENGTH);
String8 without_newlines = S8ListJoin(frame_arena, S8Split(frame_arena, trimmed, 1, &S8Lit("\n")), &(StringJoin){0});
text_ready = true;
chunk_from_s8(&text_input_result, without_newlines);
}
receiving_text_input = false;
/*
*/
}
/*
AnimatedSprite moose_idle =
{
.img = &image_moose,
.time_per_frame = 0.15,
.num_frames = 8,
.start = {0.0, 0.0},
.horizontal_diff_btwn_frames = 347.0f,
.region_size = {347.0f, 160.0f},
.offset = {-1.5f, -10.0f},
};
*/
typedef struct
{
sg_pass_action pass_action;
sg_pass pass;
sg_pipeline pip;
sg_image color_img;
sg_image depth_img;
sg_pipeline armature_pip;
} Shadow_State;
Shadow_State init_shadow_state();
// @Place(sokol state struct)
static struct
{
sg_pass_action clear_everything_pass_action;
sg_pass_action threedee_msaa_pass_action;
sg_pass_action clear_depth_buffer_pass_action;
sg_pipeline twodee_pip;
sg_bindings bind;
sg_pipeline threedee_pip;
sg_pipeline threedee_alpha_blended_pip;
sg_pipeline armature_pip;
sg_bindings threedee_bind;
sg_image outline_pass_image;
sg_image outline_pass_resolve_image;
sg_pass outline_pass;
sg_pipeline outline_mesh_pip;
sg_pipeline outline_armature_pip;
sg_pass threedee_pass; // is a pass so I can do post processing in a shader
sg_image threedee_pass_image;
sg_image threedee_pass_resolve_image;
sg_image threedee_pass_depth_image;
sg_pipeline twodee_outline_pip;
sg_pipeline twodee_colorcorrect_pip;
sg_sampler sampler_linear;
sg_sampler sampler_linear_border;
sg_sampler sampler_nearest;
Shadow_State shadows;
} state;
// is a function, because also called when window resized to recreate the pass and the image.
// its target image must be the same size as the viewport. Is the reason. Cowabunga!
void create_screenspace_gfx_state()
{
// this prevents common bug of whats passed to destroy func not being the resource