2799 lines
123 KiB
C
2799 lines
123 KiB
C
#include "user.h"
|
|
#include "sim.h"
|
|
#include "sim_ent.h"
|
|
#include "sim_step.h"
|
|
#include "gp.h"
|
|
#include "font.h"
|
|
#include "sprite.h"
|
|
#include "draw.h"
|
|
#include "intrinsics.h"
|
|
#include "asset_cache.h"
|
|
#include "string.h"
|
|
#include "scratch.h"
|
|
#include "math.h"
|
|
#include "sys.h"
|
|
#include "mixer.h"
|
|
#include "atomic.h"
|
|
#include "collider.h"
|
|
#include "rand.h"
|
|
#include "log.h"
|
|
#include "sock.h"
|
|
#include "host.h"
|
|
#include "bitbuff.h"
|
|
#include "gstat.h"
|
|
#include "app.h"
|
|
|
|
struct bind_state {
|
|
b32 is_held; /* Is this bind held down this frame */
|
|
u32 num_presses; /* How many times was this bind's pressed since last frame */
|
|
u32 num_repeats; /* How many times was this bind's key repeated since last frame */
|
|
u32 num_presses_and_repeats; /* Same as `num_presses` but includes key repeats as well */
|
|
u32 num_releases; /* How many times was this bind released since last frame */
|
|
};
|
|
|
|
struct second_stat {
|
|
u64 last_second_start;
|
|
u64 last_second_end;
|
|
u64 last_second;
|
|
};
|
|
|
|
struct console_log {
|
|
struct string msg;
|
|
i32 level;
|
|
i32 color_index;
|
|
struct sys_datetime datetime;
|
|
i64 time_ns;
|
|
struct rect bounds;
|
|
struct console_log *prev;
|
|
struct console_log *next;
|
|
};
|
|
|
|
GLOBAL struct {
|
|
struct atomic_i32 user_thread_shutdown;
|
|
struct sys_thread *user_thread;
|
|
|
|
struct atomic_i32 local_sim_thread_shutdown;
|
|
struct sys_thread *local_sim_thread;
|
|
struct sim_ctx *local_sim_ctx;
|
|
|
|
struct arena *arena;
|
|
struct sys_window *window;
|
|
struct string connect_address_str;
|
|
|
|
struct sim_client_store *user_client_store;
|
|
struct sim_client *user_unblended_client; /* Contains snapshots received from local sim */
|
|
struct sim_client *user_blended_client; /* Contains single snapshot from result of blending local sim snapshots */
|
|
struct sim_snapshot *ss_blended; /* Points to blended snapshot contained in blended client */
|
|
|
|
/* Usage stats */
|
|
i64 last_second_reset_ns;
|
|
struct second_stat net_bytes_read;
|
|
struct second_stat net_bytes_sent;
|
|
|
|
/* Gpu handles */
|
|
struct gp_handle user_texture;
|
|
|
|
struct gp_handle world_gp_flow;
|
|
struct gp_handle ui_gp_flow;
|
|
|
|
struct xform world_to_user_xf;
|
|
|
|
struct bind_state bind_states[USER_BIND_KIND_COUNT];
|
|
|
|
/* Debug camera */
|
|
struct sim_ent_id debug_following;
|
|
b32 debug_camera;
|
|
b32 debug_camera_panning;
|
|
struct v2 debug_camera_pan_start;
|
|
b32 debug_draw;
|
|
|
|
/* Debug console */
|
|
struct sys_mutex *console_logs_mutex;
|
|
struct arena *console_logs_arena;
|
|
struct console_log *first_console_log;
|
|
struct console_log *last_console_log;
|
|
i32 console_log_color_indices[LOG_LEVEL_COUNT];
|
|
f32 console_logs_height;
|
|
b32 debug_console;
|
|
|
|
/* Window -> user */
|
|
struct sys_mutex *sys_events_mutex;
|
|
struct arena *sys_events_arena;
|
|
|
|
/* User -> local sim */
|
|
struct sys_mutex *user_sim_cmd_mutex;
|
|
struct sim_control user_sim_cmd_control;
|
|
struct sim_ent_id user_hovered_ent;
|
|
u64 last_user_sim_cmd_gen;
|
|
u64 user_sim_cmd_gen;
|
|
|
|
struct atomic_i32 user_paused;
|
|
struct atomic_i32 user_paused_steps;
|
|
|
|
/* Local sim -> user */
|
|
struct sys_mutex *local_to_user_client_mutex;
|
|
struct sim_client_store *local_to_user_client_store;
|
|
struct sim_client *local_to_user_client;
|
|
i64 local_to_user_client_publish_dt_ns;
|
|
i64 local_to_user_client_publish_time_ns;
|
|
|
|
/* Rolling window of local sim -> user publish time deltas */
|
|
i64 last_local_to_user_snapshot_published_at_ns;
|
|
i64 average_local_to_user_snapshot_publish_dt_ns;
|
|
|
|
i64 local_sim_predicted_time_ns; /* Calculated from <last local sim to user pubilsh time> + <time since last local sim to user publish> */
|
|
i64 render_time_target_ns; /* Claculated from <local_sim_rpedicted_time_ns> - <render interp delay> */
|
|
i64 render_time_ns; /* Incremented at a constant rate based on average local to user publish delta, but snaps to render_time_target_ns if it gets too distant */
|
|
|
|
u64 local_sim_last_known_tick;
|
|
i64 local_sim_last_known_time_ns;
|
|
|
|
i64 real_dt_ns;
|
|
i64 real_time_ns;
|
|
|
|
/* Per-frame */
|
|
struct v2 screen_size;
|
|
struct v2 screen_cursor;
|
|
struct v2 user_screen_offset;
|
|
struct v2 user_size;
|
|
struct v2 user_center;
|
|
struct v2 user_cursor;
|
|
struct v2 world_cursor;
|
|
struct v2 focus_send;
|
|
} G = ZI, DEBUG_ALIAS(G, G_user);
|
|
|
|
/* ========================== *
|
|
* Bind state
|
|
* ========================== */
|
|
|
|
/* TODO: Remove this */
|
|
|
|
GLOBAL READONLY enum user_bind_kind g_binds[SYS_BTN_COUNT] = {
|
|
[SYS_BTN_W] = USER_BIND_KIND_MOVE_UP,
|
|
[SYS_BTN_S] = USER_BIND_KIND_MOVE_DOWN,
|
|
[SYS_BTN_A] = USER_BIND_KIND_MOVE_LEFT,
|
|
[SYS_BTN_D] = USER_BIND_KIND_MOVE_RIGHT,
|
|
[SYS_BTN_ALT] = USER_BIND_KIND_WALK,
|
|
[SYS_BTN_M1] = USER_BIND_KIND_FIRE,
|
|
[SYS_BTN_M2] = USER_BIND_KIND_FIRE_ALT,
|
|
|
|
/* Testing */
|
|
|
|
[SYS_BTN_Z] = USER_BIND_KIND_TILE_TEST,
|
|
|
|
[SYS_BTN_M5] = USER_BIND_KIND_DEBUG_DRAG,
|
|
[SYS_BTN_M4] = USER_BIND_KIND_DEBUG_DELETE,
|
|
[SYS_BTN_F] = USER_BIND_KIND_DEBUG_EXPLODE,
|
|
[SYS_BTN_T] = USER_BIND_KIND_DEBUG_TELEPORT,
|
|
[SYS_BTN_C] = USER_BIND_KIND_DEBUG_CLEAR,
|
|
[SYS_BTN_1] = USER_BIND_KIND_DEBUG_SPAWN1,
|
|
[SYS_BTN_2] = USER_BIND_KIND_DEBUG_SPAWN2,
|
|
[SYS_BTN_3] = USER_BIND_KIND_DEBUG_SPAWN3,
|
|
[SYS_BTN_4] = USER_BIND_KIND_DEBUG_SPAWN4,
|
|
[SYS_BTN_G] = USER_BIND_KIND_DEBUG_WALLS,
|
|
[SYS_BTN_N] = USER_BIND_KIND_DEBUG_STEP,
|
|
[SYS_BTN_Q] = USER_BIND_KIND_DEBUG_FOLLOW,
|
|
[SYS_BTN_F1] = USER_BIND_KIND_DEBUG_PAUSE,
|
|
[SYS_BTN_F2] = USER_BIND_KIND_DEBUG_CAMERA,
|
|
[SYS_BTN_F3] = USER_BIND_KIND_DEBUG_DRAW,
|
|
[SYS_BTN_GRAVE_ACCENT] = USER_BIND_KIND_DEBUG_CONSOLE,
|
|
[SYS_BTN_F11] = USER_BIND_KIND_FULLSCREEN,
|
|
[SYS_BTN_MWHEELUP] = USER_BIND_KIND_ZOOM_IN,
|
|
[SYS_BTN_MWHEELDOWN] = USER_BIND_KIND_ZOOM_OUT,
|
|
[SYS_BTN_M3] = USER_BIND_KIND_PAN,
|
|
|
|
#if COLLIDER_DEBUG
|
|
/* Debug */
|
|
[SYS_BTN_FORWARD_SLASH] = USER_BIND_KIND_RESET_COLLIDER_GJK_STEPS,
|
|
[SYS_BTN_COMMA] = USER_BIND_KIND_DECR_COLLIDER_GJK_STEPS,
|
|
[SYS_BTN_PERIOD] = USER_BIND_KIND_INCR_COLLIDER_GJK_STEPS
|
|
#endif
|
|
};
|
|
|
|
/* ========================== *
|
|
* Startup
|
|
* ========================== */
|
|
|
|
INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(user_shutdown);
|
|
INTERNAL LOG_EVENT_CALLBACK_FUNC_DEF(debug_console_log_callback, log);
|
|
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_thread_entry_point, arg);
|
|
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_local_sim_thread_entry_point, arg);
|
|
INTERNAL SYS_WINDOW_EVENT_CALLBACK_FUNC_DEF(window_event_callback, event);
|
|
|
|
struct user_startup_receipt user_startup(struct work_startup_receipt *work_sr,
|
|
struct gp_startup_receipt *gp_sr,
|
|
struct font_startup_receipt *font_sr,
|
|
struct sprite_startup_receipt *sprite_sr,
|
|
struct draw_startup_receipt *draw_sr,
|
|
struct asset_cache_startup_receipt *asset_cache_sr,
|
|
struct sound_startup_receipt *sound_sr,
|
|
struct mixer_startup_receipt *mixer_sr,
|
|
struct host_startup_receipt *host_sr,
|
|
struct sim_startup_receipt *sim_sr,
|
|
struct string connect_address_str,
|
|
struct sys_window *window)
|
|
{
|
|
(UNUSED)work_sr;
|
|
(UNUSED)gp_sr;
|
|
(UNUSED)font_sr;
|
|
(UNUSED)sprite_sr;
|
|
(UNUSED)draw_sr;
|
|
(UNUSED)asset_cache_sr;
|
|
(UNUSED)sound_sr;
|
|
(UNUSED)mixer_sr;
|
|
(UNUSED)host_sr;
|
|
(UNUSED)sim_sr;
|
|
|
|
G.arena = arena_alloc(GIGABYTE(64));
|
|
G.real_time_ns = sys_time_ns();
|
|
|
|
/* TODO: Remove this */
|
|
G.connect_address_str = string_copy(G.arena, connect_address_str);
|
|
|
|
/* Initialize average dt to a reasonable value */
|
|
G.average_local_to_user_snapshot_publish_dt_ns = NS_FROM_SECONDS(1) / SIM_TICKS_PER_SECOND;
|
|
|
|
/* Sys events */
|
|
G.sys_events_mutex = sys_mutex_alloc();
|
|
G.sys_events_arena = arena_alloc(GIGABYTE(64));
|
|
|
|
/* User blend clients */
|
|
G.user_client_store = sim_client_store_alloc();
|
|
G.user_unblended_client = sim_client_alloc(G.user_client_store);
|
|
G.user_blended_client = sim_client_alloc(G.user_client_store);
|
|
G.ss_blended = sim_snapshot_nil();
|
|
|
|
/* Local to user client */
|
|
G.local_to_user_client_mutex = sys_mutex_alloc();
|
|
G.local_to_user_client_store = sim_client_store_alloc();
|
|
G.local_to_user_client = sim_client_alloc(G.local_to_user_client_store);
|
|
|
|
/* User sim control */
|
|
G.user_sim_cmd_mutex = sys_mutex_alloc();
|
|
|
|
/* GPU handles */
|
|
G.world_to_user_xf = XFORM_IDENT;
|
|
G.world_gp_flow = gp_flow_alloc();
|
|
G.ui_gp_flow = gp_flow_alloc();
|
|
|
|
G.console_logs_mutex = sys_mutex_alloc();
|
|
G.console_logs_arena = arena_alloc(GIGABYTE(64));
|
|
//log_register_callback(debug_console_log_callback, LOG_LEVEL_SUCCESS);
|
|
log_register_callback(debug_console_log_callback, LOG_LEVEL_DEBUG);
|
|
|
|
G.window = window;
|
|
sys_window_register_event_callback(G.window, &window_event_callback);
|
|
|
|
G.local_sim_thread = sys_thread_alloc(&user_local_sim_thread_entry_point, G.local_sim_ctx, LIT("[P8] Local sim thread"));
|
|
|
|
//G.debug_draw = true;
|
|
|
|
G.user_thread = sys_thread_alloc(&user_thread_entry_point, NULL, LIT("[P9] User thread"));
|
|
app_register_exit_callback(&user_shutdown);
|
|
|
|
return (struct user_startup_receipt) { 0 };
|
|
}
|
|
|
|
INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(user_shutdown)
|
|
{
|
|
__prof;
|
|
|
|
sys_window_unregister_event_callback(G.window, &window_event_callback);
|
|
|
|
atomic_i32_eval_exchange(&G.user_thread_shutdown, true);
|
|
sys_thread_wait_release(G.user_thread);
|
|
|
|
#if 0
|
|
if (G.local_sim_ctx) {
|
|
atomic_i32_eval_exchange(&G.local_sim_thread_shutdown, true);
|
|
sys_thread_wait_release(&G.local_sim_thread);
|
|
sim_ctx_release(G.local_sim_ctx);
|
|
}
|
|
#else
|
|
atomic_i32_eval_exchange(&G.local_sim_thread_shutdown, true);
|
|
sys_thread_wait_release(G.local_sim_thread);
|
|
#endif
|
|
}
|
|
|
|
/* ========================== *
|
|
* Window -> user communication
|
|
* ========================== */
|
|
|
|
INTERNAL struct sys_event_array pop_sys_events(struct arena *arena)
|
|
{
|
|
struct sys_event_array array = ZI;
|
|
struct sys_lock lock = sys_mutex_lock_e(G.sys_events_mutex);
|
|
{
|
|
struct sys_event *src_events = (struct sys_event *)arena_base(G.sys_events_arena);
|
|
array.count = G.sys_events_arena->pos / sizeof(*src_events);
|
|
array.events = arena_push_array_no_zero(arena, struct sys_event, array.count);
|
|
MEMCPY(array.events, src_events, array.count * sizeof(*src_events));
|
|
arena_reset(G.sys_events_arena);
|
|
}
|
|
sys_mutex_unlock(&lock);
|
|
return array;
|
|
}
|
|
|
|
INTERNAL SYS_WINDOW_EVENT_CALLBACK_FUNC_DEF(window_event_callback, event)
|
|
{
|
|
struct sys_lock lock = sys_mutex_lock_e(G.sys_events_mutex);
|
|
{
|
|
*arena_push_no_zero(G.sys_events_arena, struct sys_event) = event;
|
|
}
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Debug draw
|
|
* ========================== */
|
|
|
|
/* TODO: remove this (testing) */
|
|
INTERNAL void debug_draw_xform(struct xform xf, u32 color_x, u32 color_y)
|
|
{
|
|
f32 thickness = 2.f;
|
|
f32 arrowhead_len = 15.f;
|
|
|
|
struct v2 pos = xform_mul_v2(G.world_to_user_xf, xf.og);
|
|
struct v2 x_ray = xform_basis_mul_v2(G.world_to_user_xf, xform_get_right(xf));
|
|
struct v2 y_ray = xform_basis_mul_v2(G.world_to_user_xf, xform_get_up(xf));
|
|
|
|
f32 ray_scale = 1;
|
|
x_ray = v2_mul(x_ray, ray_scale);
|
|
y_ray = v2_mul(y_ray, ray_scale);
|
|
|
|
draw_arrow_ray(G.ui_gp_flow, pos, x_ray, thickness, arrowhead_len, color_x);
|
|
draw_arrow_ray(G.ui_gp_flow, pos, y_ray, thickness, arrowhead_len, color_y);
|
|
|
|
//u32 color_quad = RGBA32_F(0, 1, 1, 0.3);
|
|
//struct quad quad = quad_from_rect(RECT(0, 0, 1, -1));
|
|
//quad = xform_mul_quad(xf, quad_scale(quad, 0.075f));
|
|
//draw_quad(G.ui_gp_flow, quad, color);
|
|
}
|
|
|
|
INTERNAL void debug_draw_movement(struct sim_ent *ent)
|
|
{
|
|
f32 thickness = 2.f;
|
|
f32 arrow_len = 15.f;
|
|
|
|
u32 color_vel = COLOR_ORANGE;
|
|
|
|
struct xform xf = sim_ent_get_xform(ent);
|
|
struct v2 velocity = ent->linear_velocity;
|
|
|
|
struct v2 pos = xform_mul_v2(G.world_to_user_xf, xf.og);
|
|
struct v2 vel_ray = xform_basis_mul_v2(G.world_to_user_xf, velocity);
|
|
|
|
if (v2_len(vel_ray) > 0.00001) {
|
|
draw_arrow_ray(G.ui_gp_flow, pos, vel_ray, thickness, arrow_len, color_vel);
|
|
}
|
|
}
|
|
|
|
INTERNAL struct string get_ent_debug_text(struct arena *arena, struct sim_ent *ent)
|
|
{
|
|
struct arena_temp scratch = scratch_begin(arena);
|
|
struct sim_snapshot *ss = ent->ss;
|
|
|
|
const u8 hex[] = "0123456789abcdef";
|
|
|
|
struct string res = ZI;
|
|
res.text = arena_push_dry(arena, u8);
|
|
|
|
res.len += string_format(arena, LIT("[%F]"), FMT_UID(ent->id.uid)).len;
|
|
{
|
|
b32 transmitting = sim_ent_has_prop(ent, SEPROP_SYNC_SRC);
|
|
b32 receiving = sim_ent_has_prop(ent, SEPROP_SYNC_DST);
|
|
if (transmitting & receiving) {
|
|
res.len += string_copy(arena, LIT(" networked (sending & receiving)")).len;
|
|
} else if (transmitting) {
|
|
res.len += string_copy(arena, LIT(" networked (sending)")).len;
|
|
} else if (receiving) {
|
|
res.len += string_copy(arena, LIT(" networked (receiving)")).len;
|
|
} else {
|
|
res.len += string_copy(arena, LIT(" local")).len;
|
|
}
|
|
}
|
|
res.len += string_copy(arena, LIT("\n")).len;
|
|
|
|
res.len += string_format(arena, LIT("owner: [%F]\n"), FMT_UID(ent->owner.uid)).len;
|
|
|
|
res.len += string_copy(arena, LIT("\n")).len;
|
|
|
|
{
|
|
res.len += string_copy(arena, LIT("props: 0x")).len;
|
|
for (u64 chunk_index = ARRAY_COUNT(ent->props); chunk_index-- > 0;) {
|
|
u64 chunk = ent->props[chunk_index];
|
|
for (u64 part_index = 8; part_index-- > 0;) {
|
|
if ((chunk_index != (ARRAY_COUNT(ent->props) - 1)) || ((chunk_index * 64) + (part_index * 8)) <= SEPROP_COUNT) {
|
|
u8 part = (chunk >> (part_index * 8)) & 0xFF;
|
|
string_from_char(arena, hex[(part >> 4) & 0x0F]);
|
|
string_from_char(arena, hex[(part >> 0) & 0x0F]);
|
|
res.len += 2;
|
|
}
|
|
}
|
|
}
|
|
res.len += string_copy(arena, LIT("\n")).len;
|
|
}
|
|
|
|
if (!sim_ent_id_eq(ent->parent, SIM_ENT_ROOT_ID)) {
|
|
res.len += string_format(arena, LIT("parent: [%F]\n"), FMT_UID(ent->parent.uid)).len;
|
|
}
|
|
|
|
if (!sim_ent_id_is_nil(ent->next) || !sim_ent_id_is_nil(ent->prev)) {
|
|
res.len += string_format(arena, LIT("prev: [%F]\n"), FMT_UID(ent->prev.uid)).len;
|
|
res.len += string_format(arena, LIT("next: [%F]\n"), FMT_UID(ent->next.uid)).len;
|
|
}
|
|
|
|
res.len += string_copy(arena, LIT("\n")).len;
|
|
|
|
/* Pos */
|
|
struct xform xf = sim_ent_get_xform(ent);
|
|
struct v2 linear_velocity = ent->linear_velocity;
|
|
f32 angular_velocity = ent->angular_velocity;
|
|
res.len += string_format(arena, LIT("pos: (%F, %F)\n"), FMT_FLOAT(xf.og.x), FMT_FLOAT(xf.og.y)).len;
|
|
res.len += string_format(arena, LIT("linear velocity: (%F, %F)\n"), FMT_FLOAT(linear_velocity.x), FMT_FLOAT(linear_velocity.y)).len;
|
|
res.len += string_format(arena, LIT("angular velocity: %F\n"), FMT_FLOAT(angular_velocity)).len;
|
|
|
|
/* Test */
|
|
res.len += string_format(arena, LIT("collision dir: (%F, %F)\n"), FMT_FLOAT(ent->collision_dir.x), FMT_FLOAT(ent->collision_dir.y)).len;
|
|
|
|
/* Children */
|
|
if (!sim_ent_id_is_nil(ent->first) || !sim_ent_id_is_nil(ent->last)) {
|
|
struct sim_ent *child = sim_ent_from_id(ss, ent->first);
|
|
if (!sim_ent_id_eq(ent->first, ent->last) || !child->valid) {
|
|
res.len += string_format(arena, LIT("first child: [%F]\n"), FMT_UID(ent->first.uid)).len;
|
|
res.len += string_format(arena, LIT("last child: [%F]\n"), FMT_UID(ent->last.uid)).len;
|
|
}
|
|
while (child->valid) {
|
|
res.len += string_copy(arena, LIT("\n---------------------------------\n")).len;
|
|
res.len += string_copy(arena, LIT("CHILD\n")).len;
|
|
struct string child_text = get_ent_debug_text(scratch.arena, child);
|
|
res.len += string_indent(arena, child_text, 4).len;
|
|
child = sim_ent_from_id(ss, child->next);
|
|
}
|
|
}
|
|
|
|
scratch_end(scratch);
|
|
return res;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Debug console
|
|
* ========================== */
|
|
|
|
INTERNAL LOG_EVENT_CALLBACK_FUNC_DEF(debug_console_log_callback, log)
|
|
{
|
|
struct sys_lock lock = sys_mutex_lock_e(G.console_logs_mutex);
|
|
{
|
|
struct console_log *clog = arena_push(G.console_logs_arena, struct console_log);
|
|
clog->level = log.level;
|
|
clog->msg = string_copy(G.console_logs_arena, log.msg);
|
|
clog->datetime = log.datetime;
|
|
clog->time_ns = log.time_ns;
|
|
|
|
if (G.last_console_log) {
|
|
G.last_console_log->next = clog;
|
|
clog->prev = G.last_console_log;
|
|
/* Alternating color index between logs of same level */
|
|
i32 *color_index = &G.console_log_color_indices[log.level];
|
|
clog->color_index = *color_index;
|
|
*color_index = 1 - *color_index;
|
|
} else {
|
|
G.first_console_log = clog;
|
|
}
|
|
G.last_console_log = clog;
|
|
}
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
INTERNAL void draw_debug_console(i32 level, b32 minimized)
|
|
{
|
|
struct arena_temp scratch = scratch_begin_no_conflict();
|
|
|
|
struct v2 desired_start_pos = V2(10, minimized ? 100 : 600);
|
|
i64 fade_time_ns = NS_FROM_SECONDS(10);
|
|
f32 fade_curve = 0.5;
|
|
f32 spacing = 0;
|
|
f32 bg_margin = 5;
|
|
|
|
LOCAL_PERSIST u32 colors[LOG_LEVEL_COUNT][2] = ZI;
|
|
MEMSET(colors, 0xFF, sizeof(colors));
|
|
#if 1
|
|
colors[LOG_LEVEL_DEBUG][0] = RGB32_F(0.4, 0.1, 0.4); colors[LOG_LEVEL_DEBUG][1] = RGB32_F(0.5, 0.2, 0.5);
|
|
colors[LOG_LEVEL_INFO][0] = RGB32_F(0.4, 0.4, 0.4); colors[LOG_LEVEL_INFO][1] = RGB32_F(0.5, 0.5, 0.5);
|
|
colors[LOG_LEVEL_SUCCESS][0] = RGB32_F(0.1, 0.3, 0.1); colors[LOG_LEVEL_SUCCESS][1] = RGB32_F(0.2, 0.4, 0.2);
|
|
colors[LOG_LEVEL_WARNING][0] = RGB32_F(0.4, 0.4, 0.1); colors[LOG_LEVEL_WARNING][1] = RGB32_F(0.5, 0.5, 0.2);
|
|
colors[LOG_LEVEL_ERROR][0] = RGB32_F(0.4, 0.1, 0.1); colors[LOG_LEVEL_ERROR][1] = RGB32_F(0.5, 0.2, 0.2);
|
|
#else
|
|
u32 info_colors[2] = { RGB32_F(0.4, 0.4, 0.4), RGB32_F(0.5, 0.5, 0.5) };
|
|
u32 success_colors[2] = { RGB32_F(0.1, 0.3, 0.1), RGB32_F(0.2, 0.4, 0.2) };
|
|
u32 warning_colors[2] = { RGB32_F(0.4, 0.4, 0.1), RGB32_F(0.5, 0.5, 0.2) };
|
|
u32 error_colors[2] = { RGB32_F(0.4, 0.1, 0.1), RGB32_F(0.5, 0.2, 0.2) };
|
|
#endif
|
|
|
|
struct v2 draw_pos = desired_start_pos;
|
|
f32 bounds_top = F32_INFINITY;
|
|
f32 bounds_bottom = -F32_INFINITY;
|
|
|
|
if (G.console_logs_height < desired_start_pos.y) {
|
|
draw_pos.y = G.console_logs_height;
|
|
}
|
|
G.console_logs_height = 0;
|
|
|
|
i64 now_ns = sys_time_ns();
|
|
struct font *font = font_load_async(LIT("font/fixedsys.ttf"), 12.0f);
|
|
if (font) {
|
|
struct sys_lock lock = sys_mutex_lock_e(G.console_logs_mutex);
|
|
{
|
|
for (struct console_log *log = G.last_console_log; log; log = log->prev) {
|
|
f32 opacity = 0.75;
|
|
if (minimized) {
|
|
f32 lin = 1.0 - clamp_f64((f64)(now_ns - log->time_ns) / (f64)fade_time_ns, 0, 1);
|
|
opacity *= math_pow(lin, fade_curve);
|
|
}
|
|
if (draw_pos.y > -desired_start_pos.y && opacity > 0) {
|
|
if (log->level <= level) {
|
|
/* Draw background */
|
|
u32 color = colors[log->level][log->color_index];
|
|
draw_quad(G.ui_gp_flow, quad_from_rect(log->bounds), ALPHA32_F(color, opacity));
|
|
|
|
/* Draw text */
|
|
struct string text = log->msg;
|
|
if (!minimized) {
|
|
struct sys_datetime datetime = log->datetime;
|
|
text = string_format(
|
|
scratch.arena,
|
|
LIT("[%F:%F:%F.%F] %F"),
|
|
FMT_UINT_Z(datetime.hour, 2),
|
|
FMT_UINT_Z(datetime.minute, 2),
|
|
FMT_UINT_Z(datetime.second, 2),
|
|
FMT_UINT_Z(datetime.milliseconds, 3),
|
|
FMT_STR(text));
|
|
}
|
|
|
|
struct draw_text_params params = DRAW_TEXT_PARAMS(.font = font, .pos = draw_pos, .offset_y = DRAW_TEXT_OFFSET_Y_BOTTOM, .color = ALPHA32_F(COLOR_WHITE, opacity), .str = text);
|
|
struct rect bounds = draw_text(G.ui_gp_flow, params);
|
|
|
|
struct rect draw_bounds = bounds;
|
|
draw_bounds.x -= bg_margin;
|
|
draw_bounds.y -= bg_margin;
|
|
draw_bounds.width += bg_margin * 2.f;
|
|
draw_bounds.height += bg_margin * 2.f;
|
|
draw_pos.y -= draw_bounds.height + spacing;
|
|
log->bounds = draw_bounds;
|
|
|
|
bounds_top = min_f32(bounds_top, draw_bounds.y);
|
|
bounds_bottom = max_f32(bounds_bottom, draw_bounds.y + draw_bounds.height);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
if (bounds_top < F32_INFINITY && bounds_bottom > -F32_INFINITY) {
|
|
G.console_logs_height = bounds_bottom - bounds_top;
|
|
}
|
|
scratch_end(scratch);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Sort entities
|
|
* ========================== */
|
|
|
|
INTERNAL SORT_COMPARE_FUNC_DEF(ent_draw_order_cmp, arg_a, arg_b, udata)
|
|
{
|
|
(UNUSED)udata;
|
|
struct sim_ent *a = *(struct sim_ent **)arg_a;
|
|
struct sim_ent *b = *(struct sim_ent **)arg_b;
|
|
|
|
i32 res = 0;
|
|
|
|
if (res == 0) {
|
|
/* Sort by light */
|
|
b32 a_cmp = sim_ent_has_prop(a, SEPROP_LIGHT_TEST);
|
|
b32 b_cmp = sim_ent_has_prop(b, SEPROP_LIGHT_TEST);
|
|
res = (a_cmp > b_cmp) - (a_cmp < b_cmp);
|
|
}
|
|
if (res == 0) {
|
|
/* Sort by layer */
|
|
i32 a_cmp = a->layer;
|
|
i32 b_cmp = b->layer;
|
|
res = (a_cmp < b_cmp) - (a_cmp > b_cmp);
|
|
}
|
|
if (res == 0) {
|
|
/* Sort by sprite */
|
|
u64 a_cmp = a->sprite.hash;
|
|
u64 b_cmp = b->sprite.hash;
|
|
res = (a_cmp < b_cmp) - (a_cmp > b_cmp);
|
|
}
|
|
if (res == 0) {
|
|
/* Sort by activation */
|
|
u64 a_cmp = a->activation_tick;
|
|
u64 b_cmp = b->activation_tick;
|
|
res = (a_cmp < b_cmp) - (a_cmp > b_cmp);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update
|
|
* ========================== */
|
|
|
|
INTERNAL void user_update(void)
|
|
{
|
|
struct arena_temp scratch = scratch_begin_no_conflict();
|
|
|
|
/* ========================== *
|
|
* Begin frame
|
|
* ========================== */
|
|
|
|
G.real_dt_ns = sys_time_ns() - G.real_time_ns;
|
|
G.real_time_ns += G.real_dt_ns;
|
|
G.screen_size = sys_window_get_size(G.window);
|
|
|
|
struct sprite_scope *sprite_frame_scope = sprite_scope_begin();
|
|
|
|
/* ========================== *
|
|
* Pull latest local sim snapshot
|
|
* ========================== */
|
|
|
|
{
|
|
struct sys_lock lock = sys_mutex_lock_e(G.local_to_user_client_mutex);
|
|
u64 old_last_tick = G.user_unblended_client->last_tick;
|
|
u64 last_tick = G.local_to_user_client->last_tick;
|
|
if (last_tick > old_last_tick) {
|
|
struct sim_snapshot *src = sim_snapshot_from_tick(G.local_to_user_client, last_tick);
|
|
sim_snapshot_alloc(G.user_unblended_client, src, src->tick);
|
|
G.last_local_to_user_snapshot_published_at_ns = G.local_to_user_client_publish_time_ns;
|
|
G.average_local_to_user_snapshot_publish_dt_ns -= G.average_local_to_user_snapshot_publish_dt_ns / 50;
|
|
G.average_local_to_user_snapshot_publish_dt_ns += G.local_to_user_client_publish_dt_ns / 50;
|
|
}
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Create user world from blended snapshots
|
|
* ========================== */
|
|
|
|
{
|
|
/* How along are we between sim ticks (0 = start of tick, 1 = end of tick) */
|
|
f64 tick_progress = 0;
|
|
i64 next_tick_expected_ns = G.last_local_to_user_snapshot_published_at_ns + G.average_local_to_user_snapshot_publish_dt_ns;
|
|
if (next_tick_expected_ns > G.last_local_to_user_snapshot_published_at_ns) {
|
|
tick_progress = (f64)(G.real_time_ns - G.last_local_to_user_snapshot_published_at_ns) / (f64)(next_tick_expected_ns - G.last_local_to_user_snapshot_published_at_ns);
|
|
}
|
|
|
|
/* Predict local sim time based on average snapshot publish dt. */
|
|
struct sim_snapshot *newest_snapshot = sim_snapshot_from_tick(G.user_unblended_client, G.user_unblended_client->last_tick);
|
|
G.local_sim_last_known_time_ns = newest_snapshot->sim_time_ns;
|
|
G.local_sim_last_known_tick = newest_snapshot->tick;
|
|
if (atomic_i32_eval(&G.user_paused)) {
|
|
G.local_sim_predicted_time_ns = G.local_sim_last_known_tick;
|
|
} else {
|
|
G.local_sim_predicted_time_ns = newest_snapshot->sim_time_ns + (newest_snapshot->sim_dt_ns * tick_progress);
|
|
}
|
|
|
|
if (USER_INTERP_ENABLED && !atomic_i32_eval(&G.user_paused)) {
|
|
/* Determine render time */
|
|
G.render_time_target_ns = G.local_sim_predicted_time_ns - (USER_INTERP_RATIO * G.average_local_to_user_snapshot_publish_dt_ns);
|
|
if (G.average_local_to_user_snapshot_publish_dt_ns > 0) {
|
|
/* Increment render time based on average publish dt */
|
|
f64 sim_publish_timescale = (f64)newest_snapshot->sim_dt_ns / (f64)G.average_local_to_user_snapshot_publish_dt_ns;
|
|
G.render_time_ns += G.real_dt_ns * sim_publish_timescale;
|
|
}
|
|
i64 render_time_target_diff_ns = G.render_time_target_ns - G.render_time_ns;
|
|
if (render_time_target_diff_ns > NS_FROM_SECONDS(0.010) || render_time_target_diff_ns < NS_FROM_SECONDS(-0.005)) {
|
|
/* Snap render time if it gets too out of sync with target render time */
|
|
G.render_time_ns = G.render_time_target_ns;
|
|
}
|
|
|
|
/* Get two snapshots nearest to render time */
|
|
struct sim_snapshot *left_snapshot = sim_snapshot_nil();
|
|
struct sim_snapshot *right_snapshot = newest_snapshot;
|
|
{
|
|
struct sim_snapshot *ss = sim_snapshot_from_tick(G.user_unblended_client, G.user_unblended_client->first_tick);
|
|
while (ss->valid) {
|
|
u64 next_tick = ss->next_tick;
|
|
i64 ss_time_ns = ss->sim_time_ns;
|
|
if (ss_time_ns < G.render_time_ns && ss_time_ns > left_snapshot->sim_time_ns) {
|
|
left_snapshot = ss;
|
|
}
|
|
if (ss_time_ns > G.render_time_ns && ss_time_ns < right_snapshot->sim_time_ns) {
|
|
right_snapshot = ss;
|
|
}
|
|
ss = sim_snapshot_from_tick(G.user_unblended_client, next_tick);
|
|
}
|
|
}
|
|
|
|
/* Create world from blended snapshots */
|
|
if (left_snapshot->valid && right_snapshot->valid) {
|
|
f64 blend = (f64)(G.render_time_ns - left_snapshot->sim_time_ns) / (f64)(right_snapshot->sim_time_ns - left_snapshot->sim_time_ns);
|
|
G.ss_blended = sim_snapshot_alloc_from_lerp(G.user_blended_client, left_snapshot, right_snapshot, blend);
|
|
} else if (left_snapshot->valid) {
|
|
G.ss_blended = sim_snapshot_alloc(G.user_blended_client, left_snapshot, left_snapshot->tick);
|
|
} else if (right_snapshot->valid) {
|
|
G.ss_blended = sim_snapshot_alloc(G.user_blended_client, right_snapshot, right_snapshot->tick);
|
|
}
|
|
|
|
/* Release unneeded unblended snapshots */
|
|
if (left_snapshot->tick > 0) {
|
|
sim_snapshot_release_ticks_in_range(G.user_unblended_client, 0, left_snapshot->tick - 1);
|
|
}
|
|
} else {
|
|
/* Interp disabled, just copy latest snapshot */
|
|
G.render_time_target_ns = newest_snapshot->sim_time_ns;
|
|
G.render_time_ns = newest_snapshot->sim_time_ns;
|
|
G.ss_blended = sim_snapshot_alloc(G.user_blended_client, newest_snapshot, newest_snapshot->tick);
|
|
|
|
/* Release unneeded unblended snapshots */
|
|
if (newest_snapshot->tick > 0) {
|
|
sim_snapshot_release_ticks_in_range(G.user_unblended_client, 0, newest_snapshot->tick - 1);
|
|
}
|
|
}
|
|
|
|
/* Release unneeded blended snapshots */
|
|
if (G.ss_blended->tick > 0) {
|
|
sim_snapshot_release_ticks_in_range(G.user_blended_client, 0, G.ss_blended->tick - 1);
|
|
sim_snapshot_release_ticks_in_range(G.user_blended_client, G.ss_blended->tick + 1, U64_MAX);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Process sys events into user bind state
|
|
* ========================== */
|
|
|
|
{
|
|
struct sys_event_array events = pop_sys_events(scratch.arena);
|
|
|
|
/* Reset bind pressed / released states */
|
|
for (u32 i = 0; i < ARRAY_COUNT(G.bind_states); ++i) {
|
|
G.bind_states[i] = (struct bind_state) {
|
|
.is_held = G.bind_states[i].is_held
|
|
};
|
|
}
|
|
|
|
for (u64 ent_index = 0; ent_index < events.count; ++ent_index) {
|
|
struct sys_event *event = &events.events[ent_index];
|
|
if (event->kind == SYS_EVENT_KIND_QUIT) {
|
|
app_exit();
|
|
}
|
|
|
|
if (event->kind == SYS_EVENT_KIND_BUTTON_UP) {
|
|
/* Escape quit */
|
|
if (event->button == SYS_BTN_ESC) {
|
|
app_exit();
|
|
}
|
|
}
|
|
|
|
/* Update mouse pos */
|
|
if (event->kind == SYS_EVENT_KIND_CURSOR_MOVE) {
|
|
G.screen_cursor = event->cursor_position;
|
|
}
|
|
|
|
/* Update bind states */
|
|
if ((event->kind == SYS_EVENT_KIND_BUTTON_DOWN || event->kind == SYS_EVENT_KIND_BUTTON_UP)) {
|
|
enum sys_btn button = event->button;
|
|
button = button >= SYS_BTN_COUNT ? SYS_BTN_NONE : button;
|
|
enum user_bind_kind bind = g_binds[button];
|
|
if (bind) {
|
|
b32 pressed = event->kind == SYS_EVENT_KIND_BUTTON_DOWN;
|
|
#if 0
|
|
b32 out_of_bounds = button >= SYS_BTN_M1 && button <= SYS_BTN_M5 &&
|
|
(G.user_cursor.x < 0 ||
|
|
G.user_cursor.y < 0 ||
|
|
G.user_cursor.x > G.user_size.x ||
|
|
G.user_cursor.y > G.user_size.y);
|
|
#else
|
|
b32 out_of_bounds = false;
|
|
#endif
|
|
G.bind_states[bind].is_held = pressed && !out_of_bounds;
|
|
if (pressed) {
|
|
if (!out_of_bounds) {
|
|
++G.bind_states[bind].num_presses_and_repeats;
|
|
if (event->is_repeat) {
|
|
++G.bind_states[bind].num_repeats;
|
|
} else {
|
|
++G.bind_states[bind].num_presses;
|
|
}
|
|
}
|
|
} else {
|
|
++G.bind_states[bind].num_releases;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Find local entities
|
|
* ========================== */
|
|
|
|
struct sim_ent *local_player = sim_ent_from_id(G.ss_blended, G.ss_blended->local_player);
|
|
struct sim_ent *local_control = sim_ent_from_id(G.ss_blended, local_player->player_control_ent);
|
|
struct sim_ent *local_camera = sim_ent_from_id(G.ss_blended, local_player->player_camera_ent);
|
|
|
|
/* ========================== *
|
|
* Find hovered entity
|
|
* ========================== */
|
|
|
|
struct sim_ent *hovered_ent = sim_ent_nil();
|
|
{
|
|
struct xform mouse_xf = xform_from_pos(G.world_cursor);
|
|
struct collider_shape mouse_shape = ZI;
|
|
mouse_shape.points[0] = V2(0, 0);
|
|
mouse_shape.count = 1;
|
|
mouse_shape.radius = 0.01f;
|
|
for (u64 ent_index = 0; ent_index < G.ss_blended->num_ents_reserved; ++ent_index) {
|
|
struct sim_ent *ent = &G.ss_blended->ents[ent_index];
|
|
if (!sim_ent_is_valid_and_active(ent)) continue;
|
|
|
|
struct collider_shape ent_collider = ent->local_collider;
|
|
if (ent_collider.count > 0) {
|
|
/* TODO: Can just use boolean GJK */
|
|
struct xform ent_xf = sim_ent_get_xform(ent);
|
|
struct collider_collision_points_result res = collider_collision_points(&ent_collider, &mouse_shape, ent_xf, mouse_xf);
|
|
if (res.num_points > 0) {
|
|
hovered_ent = sim_ent_from_id(G.ss_blended, ent->top);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update user state from binds
|
|
* ========================== */
|
|
|
|
/* Test fullscreen */
|
|
{
|
|
struct bind_state state = G.bind_states[USER_BIND_KIND_FULLSCREEN];
|
|
if (state.num_presses) {
|
|
struct sys_window_settings settings = sys_window_get_settings(G.window);
|
|
settings.flags ^= SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN;
|
|
sys_window_update_settings(G.window, &settings);
|
|
}
|
|
}
|
|
|
|
if (G.bind_states[USER_BIND_KIND_DEBUG_DRAW].num_presses > 0) {
|
|
G.debug_draw = !G.debug_draw;
|
|
}
|
|
|
|
if (G.bind_states[USER_BIND_KIND_DEBUG_CONSOLE].num_presses > 0) {
|
|
G.debug_console = !G.debug_console;
|
|
}
|
|
|
|
if (G.bind_states[USER_BIND_KIND_DEBUG_CAMERA].num_presses > 0) {
|
|
G.debug_camera = !G.debug_camera;
|
|
}
|
|
|
|
{
|
|
if (G.bind_states[USER_BIND_KIND_DEBUG_FOLLOW].num_presses > 0) {
|
|
if (sim_ent_id_is_nil(G.debug_following)) {
|
|
G.debug_following = hovered_ent->id;
|
|
} else {
|
|
G.debug_following = SIM_ENT_NIL_ID;
|
|
}
|
|
}
|
|
if (!sim_ent_id_is_nil(G.debug_following)) {
|
|
struct sim_ent *follow_ent = sim_ent_from_id(G.ss_blended, G.debug_following);
|
|
struct sim_ent *follow_camera = sim_ent_nil();
|
|
for (u64 i = 0; i < G.ss_blended->num_ents_reserved; ++i) {
|
|
struct sim_ent *ent = &G.ss_blended->ents[i];
|
|
struct sim_ent *ent_camera_follow = sim_ent_from_id(G.ss_blended, ent->camera_follow);
|
|
if (ent_camera_follow->valid && ent_camera_follow == follow_ent) {
|
|
follow_camera = ent;
|
|
break;
|
|
}
|
|
}
|
|
if (follow_camera->valid) {
|
|
local_camera = follow_camera;
|
|
} else {
|
|
G.debug_following = SIM_ENT_NIL_ID;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Apply shake
|
|
* ========================== */
|
|
|
|
for (u64 ent_index = 0; ent_index < G.ss_blended->num_ents_reserved; ++ent_index) {
|
|
struct sim_ent *ent = &G.ss_blended->ents[ent_index];
|
|
if (!sim_ent_is_valid_and_active(ent)) continue;
|
|
|
|
/* How much time between camera shakes */
|
|
i64 frequency_ns = NS_FROM_SECONDS(0.01f);
|
|
f32 shake = ent->shake;
|
|
if (shake > 0) {
|
|
u64 angle_seed0 = ent->id.uid.lo + (u64)(G.ss_blended->sim_time_ns / frequency_ns);
|
|
u64 angle_seed1 = angle_seed0 + 1;
|
|
f32 angle0 = rand_f64_from_seed(angle_seed0, 0, TAU);
|
|
f32 angle1 = rand_f64_from_seed(angle_seed1, 0, TAU);
|
|
|
|
struct v2 vec0 = v2_with_len(v2_from_angle(angle0), shake);
|
|
/* NOTE: vec1 not completely accurate since shake can change between frames, it's just a prediction */
|
|
struct v2 vec1 = v2_with_len(v2_from_angle(angle1), shake);
|
|
|
|
/* TODO: Cubic interp? */
|
|
f32 blend = (f32)(G.ss_blended->sim_time_ns % frequency_ns) / (f32)frequency_ns;
|
|
struct v2 vec = v2_lerp(vec0, vec1, blend);
|
|
|
|
struct xform xf = sim_ent_get_xform(ent);
|
|
xf.og = v2_add(xf.og, v2_mul(vec, shake));
|
|
sim_ent_set_xform(ent, xf);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update ui from camera
|
|
* ========================== */
|
|
|
|
/* Calculate ui dimensions */
|
|
if (G.debug_camera) {
|
|
G.user_size = G.screen_size;
|
|
G.user_screen_offset = V2(0, 0);
|
|
} else {
|
|
/* Determine ui size by camera & window dimensions */
|
|
f32 aspect_ratio = (f32)(DEFAULT_CAMERA_WIDTH / DEFAULT_CAMERA_HEIGHT);
|
|
if (local_camera->valid) {
|
|
struct xform quad_xf = xform_mul(sim_ent_get_xform(local_camera), local_camera->camera_quad_xform);
|
|
struct v2 camera_size = xform_get_scale(quad_xf);
|
|
if (!v2_is_zero(camera_size)) {
|
|
aspect_ratio = camera_size.x / camera_size.y;
|
|
}
|
|
}
|
|
f32 width = G.screen_size.x;
|
|
f32 height = G.screen_size.y;
|
|
if (width / height > aspect_ratio) {
|
|
width = height * aspect_ratio;
|
|
} else {
|
|
height = math_ceil(width / aspect_ratio);
|
|
}
|
|
G.user_size = V2(width, height);
|
|
|
|
/* Center ui in window */
|
|
f32 x = 0;
|
|
f32 y = 0;
|
|
x = math_round(G.screen_size.x / 2 - width / 2);
|
|
y = math_round(G.screen_size.y / 2 - height / 2);
|
|
G.user_screen_offset = V2(x, y);
|
|
}
|
|
|
|
G.user_center = v2_mul(G.user_size, 0.5);
|
|
G.user_cursor = v2_sub(G.screen_cursor, G.user_screen_offset);
|
|
|
|
/* ========================== *
|
|
* Update view from camera
|
|
* ========================== */
|
|
|
|
if (G.debug_camera) {
|
|
G.world_to_user_xf = xform_basis_with_rotation_world(G.world_to_user_xf, 0);
|
|
|
|
struct v2 world_cursor = xform_invert_mul_v2(G.world_to_user_xf, G.user_cursor);
|
|
|
|
/* Pan view */
|
|
if (G.bind_states[USER_BIND_KIND_PAN].is_held) {
|
|
if (!G.debug_camera_panning) {
|
|
G.debug_camera_pan_start = world_cursor;
|
|
G.debug_camera_panning = true;
|
|
}
|
|
struct v2 offset = v2_neg(v2_sub(G.debug_camera_pan_start, world_cursor));
|
|
G.world_to_user_xf = xform_translated(G.world_to_user_xf, offset);
|
|
world_cursor = xform_invert_mul_v2(G.world_to_user_xf, G.user_cursor);
|
|
G.debug_camera_pan_start = world_cursor;
|
|
} else {
|
|
G.debug_camera_panning = false;
|
|
}
|
|
|
|
/* Zoom view */
|
|
i32 input_zooms = G.bind_states[USER_BIND_KIND_ZOOM_IN].num_presses - G.bind_states[USER_BIND_KIND_ZOOM_OUT].num_presses;
|
|
if (input_zooms != 0) {
|
|
/* Zoom to cursor */
|
|
f32 zoom_rate = 2;
|
|
f32 zoom = math_pow(zoom_rate, input_zooms);
|
|
G.world_to_user_xf = xform_translated(G.world_to_user_xf, world_cursor);
|
|
G.world_to_user_xf = xform_scaled(G.world_to_user_xf, V2(zoom, zoom));
|
|
G.world_to_user_xf = xform_translated(G.world_to_user_xf, v2_neg(world_cursor));
|
|
}
|
|
} else {
|
|
struct xform xf = sim_ent_get_xform(local_camera);
|
|
|
|
struct v2 center = xf.og;
|
|
f32 rot = xform_get_rotation(xf);
|
|
|
|
/* Scale view into viewport based on camera size */
|
|
struct v2 size = G.user_size;
|
|
{
|
|
struct xform quad_xf = xform_mul(xf, local_camera->camera_quad_xform);
|
|
struct v2 camera_size = xform_get_scale(quad_xf);
|
|
if (!v2_is_zero(camera_size)) {
|
|
size = v2_div_v2(size, camera_size);
|
|
}
|
|
}
|
|
f32 scale_ui = min_f32(size.x, size.y);
|
|
|
|
struct trs trs = TRS(.t = v2_sub(G.user_center, center), .r = rot, .s = V2(scale_ui, scale_ui));
|
|
struct v2 pivot = center;
|
|
G.world_to_user_xf = XFORM_IDENT;
|
|
G.world_to_user_xf = xform_translated(G.world_to_user_xf, pivot);
|
|
G.world_to_user_xf = xform_translated(G.world_to_user_xf, trs.t);
|
|
G.world_to_user_xf = xform_rotated(G.world_to_user_xf, trs.r);
|
|
G.world_to_user_xf = xform_scaled(G.world_to_user_xf, trs.s);
|
|
G.world_to_user_xf = xform_translated(G.world_to_user_xf, v2_neg(pivot));
|
|
}
|
|
G.world_cursor = xform_invert_mul_v2(G.world_to_user_xf, G.user_cursor);
|
|
|
|
/* ========================== *
|
|
* Update listener from view
|
|
* ========================== */
|
|
|
|
{
|
|
struct v2 up = V2(0, -1);
|
|
struct v2 listener_pos = xform_invert_mul_v2(G.world_to_user_xf, G.user_center);
|
|
struct v2 listener_dir = v2_norm(xform_basis_invert_mul_v2(G.world_to_user_xf, up));
|
|
mixer_set_listener(listener_pos, listener_dir);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Draw grid
|
|
* ========================== */
|
|
|
|
{
|
|
f32 thickness = 2;
|
|
|
|
struct v2 offset = v2_neg(xform_mul_v2(G.world_to_user_xf, V2(0, 0)));
|
|
f32 spacing = xform_get_scale(G.world_to_user_xf).x;
|
|
|
|
struct v2 pos = xform_invert_mul_v2(G.world_to_user_xf, V2(0, 0));
|
|
struct v2 size = xform_basis_invert_mul_v2(G.world_to_user_xf, G.user_size);
|
|
u32 color0 = RGBA32_F(0.17f, 0.17f, 0.17f, 1.f);
|
|
u32 color1 = RGBA32_F(0.15f, 0.15f, 0.15f, 1.f);
|
|
draw_grid(G.world_gp_flow, xform_from_rect(RECT_FROM_V2(pos, size)), color0, color1, RGBA32(0x3f, 0x3f, 0x3f, 0xFF), COLOR_RED, COLOR_GREEN, thickness, spacing, offset);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Draw test
|
|
* ========================== */
|
|
|
|
{
|
|
struct v2 pos = xform_invert_mul_v2(G.world_to_user_xf, V2(0, 0));
|
|
struct v2 size = xform_basis_invert_mul_v2(G.world_to_user_xf, G.user_size);
|
|
struct gp_cmd_desc cmd = ZI;
|
|
cmd.kind = GP_CMD_KIND_TEST;
|
|
cmd.test.xf = xform_from_rect(RECT_FROM_V2(pos, size));
|
|
gp_push_cmd(G.world_gp_flow, cmd);
|
|
}
|
|
|
|
#if 0
|
|
/* ========================== *
|
|
* Alloc / release tile cache entries
|
|
* ========================== */
|
|
|
|
/* Alloc entries from new sim chunks */
|
|
|
|
for (u64 ent_index = 0; ent_index < G.ss_blended->num_ents_reserved; ++ent_index) {
|
|
struct sim_ent *chunk_ent = &G.ss_blended->ents[ent_index];
|
|
if (sim_ent_is_valid_and_active(chunk_ent) && sim_ent_has_prop(chunk_ent, SEPROP_TILE_CHUNK)) {
|
|
struct user_tile_cache_entry *entry = user_tile_cache_entry_from_chunk_pos(chunk_ent->tile_chunk_pos);
|
|
if (!entry->valid) {
|
|
entry = user_tile_cache_entry_alloc(chunk_ent->tile_chunk_pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Release entries with invalid sim chunks */
|
|
|
|
for (u64 entry_index = 0; entry_index < G.tile_cache.num_reserved_entries; ++entry_index) {
|
|
struct tile_cache_entry *entry = &G.tile_cache.entries[entry_index];
|
|
if (entry->valid) {
|
|
struct sim_ent *chunk_ent = sim_ent_from_chunk_pos(entry->pos);
|
|
if (!chunk_ent->valid) {
|
|
user_tile_cache_entry_release(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Draw dirty tile cache entries
|
|
* ========================== */
|
|
|
|
for (u64 entry_index = 0; entry_index < G.tile_cache.num_reserved_entries; ++entry_index) {
|
|
struct tile_cache_entry *entry = &G.tile_cache.entries[entry_index];
|
|
if (entry->valid) {
|
|
struct v2i32 chunk_pos = entry->pos;
|
|
struct sim_ent *chunk_ent = sim_ent_from_chunk_pos(chunk_pos);
|
|
if (entry->applied_dirty_gen != chunk_ent->dirty_gen) {
|
|
entry->applied_dirty_gen = chunk_ent->dirty_gen;
|
|
/* TODO: Autotiling */
|
|
|
|
struct string data = sim_ent_get_chunk_tile_data(chunk_ent);
|
|
u64 tile_count = data.len;
|
|
if (tile_count == SIM_TILES_PER_CHUNK_SQRT * SIM_TILES_PER_CHUNK_SQRT) {
|
|
for (u64 y_in_chunk = 0; y_in_chunk < SIM_TILES_PER_CHUNK_SQRT; ++y_in_chunk) {
|
|
for (u64 x_in_chunk = 0; x_in_chunk < SIM_TILES_PER_CHUNK_SQRT; ++x_in_chunk) {
|
|
}
|
|
}
|
|
} else {
|
|
/* TODO: Clear gpu buffer if it exists */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
for (u64 entry_index = 0; entry_index < G.tile_cache.num_reserved_entries; ++entry_index) {
|
|
struct tile_cache_entry *entry = &G.tile_cache.entries[entry_index];
|
|
if (entry->valid) {
|
|
struct v2i32 chunk_pos = entry->pos;
|
|
struct sim_ent *chunk_ent = sim_ent_from_chunk_pos(chunk_pos);
|
|
if (entry->applied_dirty_gen != chunk_ent->dirty_gen) {
|
|
entry->applied_dirty_gen = chunk_ent->dirty_gen;
|
|
|
|
/* Retreive surrounding chunk info since we're auto-tiling
|
|
* [TL] [T] [TR]
|
|
* [L ] X [R ]
|
|
* [BL] [B] [BR]
|
|
*/
|
|
struct v2i32 chunk_pos_tl = V2I32(chunk_pos.x - 1, chunk_pos.y - 1);
|
|
struct v2i32 chunk_pos_t = V2I32(chunk_pos.x, chunk_pos.y - 1);
|
|
struct v2i32 chunk_pos_tr = V2I32(chunk_pos.x + 1, chunk_pos.y - 1);
|
|
struct v2i32 chunk_pos_l = V2I32(chunk_pos.x - 1, chunk_pos.y);
|
|
struct v2i32 chunk_pos_r = V2I32(chunk_pos.x + 1, chunk_pos.y);
|
|
struct v2i32 chunk_pos_bl = V2I32(chunk_pos.x - 1, chunk_pos.y + 1);
|
|
struct v2i32 chunk_pos_b = V2I32(chunk_pos.x, chunk_pos.y + 1);
|
|
struct v2i32 chunk_pos_br = V2I32(chunk_pos.x + 1, chunk_pos.y + 1);
|
|
struct sim_ent *chunk_ent_tl = sim_ent_from_chunk_pos(chunk_pos_tl);
|
|
struct sim_ent *chunk_ent_t = sim_ent_from_chunk_pos(chunk_pos_t);
|
|
struct sim_ent *chunk_ent_tr = sim_ent_from_chunk_pos(chunk_pos_tr);
|
|
struct sim_ent *chunk_ent_l = sim_ent_from_chunk_pos(chunk_pos_l);
|
|
struct sim_ent *chunk_ent_r = sim_ent_from_chunk_pos(chunk_pos_r);
|
|
struct sim_ent *chunk_ent_bl = sim_ent_from_chunk_pos(chunk_pos_bl);
|
|
struct sim_ent *chunk_ent_b = sim_ent_from_chunk_pos(chunk_pos_b);
|
|
struct sim_ent *chunk_ent_br = sim_ent_from_chunk_pos(chunk_pos_br);
|
|
|
|
struct string data = sim_ent_get_chunk_tile_data(chunk_ent);
|
|
|
|
//for (u64 x = 0; x <
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
/* ========================== *
|
|
* Sort drawable entities
|
|
* ========================== */
|
|
|
|
struct sim_ent **sorted = arena_push_dry(scratch.arena, struct sim_ent *);
|
|
u64 sorted_count = 0;
|
|
{
|
|
/* Copy valid entities */
|
|
{
|
|
__profscope(copy_sprites_for_sorting);
|
|
for (u64 ent_index = 0; ent_index < G.ss_blended->num_ents_reserved; ++ent_index) {
|
|
struct sim_ent *ent = &G.ss_blended->ents[ent_index];
|
|
if (sim_ent_is_valid_and_active(ent)) {
|
|
*arena_push_no_zero(scratch.arena, struct sim_ent *) = ent;
|
|
++sorted_count;
|
|
}
|
|
}
|
|
}
|
|
/* Sort */
|
|
{
|
|
__profscope(sort_sprites);
|
|
merge_sort(sorted, sorted_count, sizeof(*sorted), ent_draw_order_cmp, NULL);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Draw entities
|
|
* ========================== */
|
|
|
|
{
|
|
__profscope(draw_entities);
|
|
for (u64 sorted_index = 0; sorted_index < sorted_count; ++sorted_index) {
|
|
struct sim_ent *ent = sorted[sorted_index];
|
|
if (!sim_ent_is_valid_and_active(ent)) continue;
|
|
//if (sprite_tag_is_nil(ent->sprite)) continue;
|
|
|
|
struct sprite_tag sprite = ent->sprite;
|
|
|
|
struct sim_ent *parent = sim_ent_from_id(G.ss_blended, ent->parent);
|
|
|
|
struct xform xf = sim_ent_get_xform(ent);
|
|
struct xform parent_xf = sim_ent_get_xform(parent);
|
|
|
|
b32 skip_debug_draw = !G.debug_camera && ent == local_camera;
|
|
skip_debug_draw = skip_debug_draw || sim_ent_has_prop(ent, SEPROP_MOTOR_JOINT);
|
|
|
|
b32 skip_debug_draw_transform = sim_ent_has_prop(ent, SEPROP_CAMERA);
|
|
skip_debug_draw_transform = true;
|
|
|
|
struct xform sprite_xform = xform_mul(xf, ent->sprite_local_xform);
|
|
|
|
/* Draw tracer */
|
|
if (sim_ent_has_prop(ent, SEPROP_TRACER)) {
|
|
struct v2 velocity = ent->tracer_start_velocity;
|
|
|
|
struct v2 a = ent->tracer_start;
|
|
struct v2 b = xf.og;
|
|
struct v2 c = ent->tracer_gradient_start;
|
|
struct v2 d = ent->tracer_gradient_end;
|
|
|
|
struct v2 vcd = v2_sub(d, c);
|
|
struct v2 vca = v2_sub(a, c);
|
|
struct v2 vdb = v2_sub(b, d);
|
|
struct v2 vdc = v2_neg(vcd);
|
|
|
|
f32 opacity_a = 1;
|
|
f32 opacity_b = 1;
|
|
if (v2_len_sq(vcd) != 0) {
|
|
if (v2_dot(velocity, vca) <= 0) {
|
|
a = c;
|
|
opacity_a = 0;
|
|
} else {
|
|
opacity_a = v2_dot(vcd, vca) / v2_len_sq(vcd);
|
|
}
|
|
opacity_a = clamp_f32(opacity_a, 0, 1);
|
|
opacity_b = clamp_f32(1.f - (v2_dot(vdc, vdb) / v2_len_sq(vdc)), 0, 1);
|
|
}
|
|
|
|
f32 thickness = 0.01f;
|
|
u32 color_start = RGBA32_F(1, 0.5, 0, opacity_a);
|
|
u32 color_end = RGBA32_F(1, 0.8, 0.4, opacity_b);
|
|
|
|
if (opacity_b > 0.99f) {
|
|
draw_circle(G.world_gp_flow, b, thickness / 2, color_end, 20);
|
|
}
|
|
draw_gradient_line(G.world_gp_flow, a, b, thickness, color_start, color_end);
|
|
|
|
}
|
|
|
|
/* Draw sprite */
|
|
if (!sprite_tag_is_nil(sprite)) {
|
|
struct sprite_sheet *sheet = sprite_sheet_from_tag_async(sprite_frame_scope, sprite);
|
|
sprite_texture_from_tag_prefetch(sprite_frame_scope, sprite);
|
|
|
|
/* TODO: Fade in placeholder if texture isn't loaded */
|
|
if (sheet->loaded) {
|
|
b32 is_light = sim_ent_has_prop(ent, SEPROP_LIGHT_TEST);
|
|
f32 emittance = is_light ? 1.0 : 0.0;
|
|
u32 tint = ent->sprite_tint;
|
|
struct sprite_sheet_frame frame = sprite_sheet_get_frame(sheet, ent->animation_frame);
|
|
struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.xf = sprite_xform, .sprite = sprite, .tint = tint, .clip = frame.clip, .emittance = emittance);
|
|
draw_texture(G.world_gp_flow, params);
|
|
}
|
|
}
|
|
|
|
/* Draw tiles */
|
|
/* TODO: Something better */
|
|
if (sim_ent_has_prop(ent, SEPROP_TILE_CHUNK)) {
|
|
struct v2i32 chunk_index = ent->tile_chunk_index;
|
|
struct sprite_tag tile_sprite = sprite_tag_from_path(LIT("sprite/tile.ase"));
|
|
f32 tile_size = 1.f / SIM_TILES_PER_UNIT_SQRT;
|
|
for (i32 tile_y = 0; tile_y < SIM_TILES_PER_CHUNK_SQRT; ++tile_y) {
|
|
for (i32 tile_x = 0; tile_x < SIM_TILES_PER_CHUNK_SQRT; ++tile_x) {
|
|
struct v2i32 local_tile_index = V2I32(tile_x, tile_y);
|
|
enum sim_tile_kind tile = ent->tile_chunk_tiles[local_tile_index.x + (local_tile_index.y * SIM_TILES_PER_CHUNK_SQRT)];
|
|
//if (tile > -1) {
|
|
if (tile == SIM_TILE_KIND_WALL) {
|
|
struct v2i32 world_tile_index = sim_world_tile_index_from_local_tile_index(chunk_index, local_tile_index);
|
|
struct v2 pos = sim_pos_from_world_tile_index(world_tile_index);
|
|
struct xform tile_xf = xform_from_rect(RECT_FROM_V2(pos, V2(tile_size, tile_size)));
|
|
struct draw_texture_params params = DRAW_TEXTURE_PARAMS(.xf = tile_xf, .sprite = tile_sprite);
|
|
draw_texture(G.world_gp_flow, params);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Debug draw entity info */
|
|
if (G.debug_draw && !skip_debug_draw) {
|
|
struct arena_temp temp = arena_temp_begin(scratch.arena);
|
|
|
|
if (sim_ent_has_prop(ent, SEPROP_KINEMATIC) || sim_ent_has_prop(ent, SEPROP_DYNAMIC)) {
|
|
debug_draw_movement(ent);
|
|
}
|
|
|
|
/* Draw xform */
|
|
if (!skip_debug_draw_transform) {
|
|
u32 color_x = RGBA32_F(1, 0, 0, 0.5);
|
|
u32 color_y = RGBA32_F(0, 1, 0, 0.5);
|
|
debug_draw_xform(xf, color_x, color_y);
|
|
}
|
|
|
|
/* Draw AABB */
|
|
if (ent->local_collider.count > 0) {
|
|
struct aabb aabb = collider_aabb_from_collider(&ent->local_collider, xf);
|
|
f32 thickness = 1;
|
|
u32 color = RGBA32_F(1, 0, 1, 0.5);
|
|
struct quad quad = quad_from_aabb(aabb);
|
|
quad = xform_mul_quad(G.world_to_user_xf, quad);
|
|
draw_quad_line(G.ui_gp_flow, quad, thickness, color);
|
|
}
|
|
|
|
/* Draw focus arrow */
|
|
if (ent == local_control || sim_ent_id_eq(ent->id, G.debug_following)) {
|
|
struct sprite_sheet *sheet = sprite_sheet_from_tag_async(sprite_frame_scope, ent->sprite);
|
|
struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, LIT("attach.wep"), ent->animation_frame);
|
|
struct v2 start = xform_mul_v2(sprite_xform, slice.center);
|
|
start = xform_mul_v2(G.world_to_user_xf, start);
|
|
struct v2 end = v2_add(xf.og, ent->control.focus);
|
|
end = xform_mul_v2(G.world_to_user_xf, end);
|
|
draw_arrow_line(G.ui_gp_flow, start, end, 3, 10, RGBA32_F(1, 1, 1, 0.5));
|
|
}
|
|
|
|
#if 0
|
|
/* Draw slices */
|
|
if (!sprite_tag_is_nil(ent->sprite)) {
|
|
struct sprite_sheet *sheet = sprite_sheet_from_tag_async(sprite_frame_scope, sprite);
|
|
|
|
u32 quad_color = RGBA32_F(1, 0, 0.5, 1);
|
|
u32 point_color = RGBA32_F(1, 0, 0, 1);
|
|
u32 ray_color = RGBA32_F(1, 0, 0.5, 1);
|
|
|
|
for (u64 i = 0; i < sheet->slice_groups_count; ++i) {
|
|
struct sprite_sheet_slice_group *group = &sheet->slice_groups[i];
|
|
if (string_ends_with(group->name, LIT(".ray"))) continue;
|
|
|
|
for (u32 j = 0; j < group->per_frame_count; ++j) {
|
|
struct sprite_sheet_slice slice = group->frame_slices[(ent->animation_frame * group->per_frame_count) + j];
|
|
|
|
struct v2 center = xform_mul_v2(sprite_xform, slice.center);
|
|
center = xform_mul_v2(G.world_to_user_xf, center);
|
|
|
|
if (!slice.has_ray) {
|
|
struct quad quad = quad_from_rect(slice.rect);
|
|
quad = xform_mul_quad(sprite_xform, quad);
|
|
quad = xform_mul_quad(G.world_to_user_xf, quad);
|
|
draw_quad_line(G.ui_gp_flow, quad, 2, quad_color);
|
|
}
|
|
|
|
draw_circle(G.ui_gp_flow, center, 3, point_color, 20);
|
|
|
|
if (slice.has_ray) {
|
|
struct v2 ray = xform_basis_mul_v2(sprite_xform, slice.dir);
|
|
ray = xform_basis_mul_v2(G.world_to_user_xf, ray);
|
|
ray = v2_with_len(ray, 25);
|
|
draw_arrow_ray(G.ui_gp_flow, center, ray, 2, 10, ray_color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Draw weld joint */
|
|
#if 0
|
|
if (sim_ent_has_prop(ent, SEPROP_WELD_JOINT)) {
|
|
struct sim_ent *e1 = sim_ent_from_id(G.ss_blended, ent->weld_joint_data.e1);
|
|
struct xform e1_xf = sim_ent_get_xform(e1);
|
|
|
|
u32 color = COLOR_YELLOW;
|
|
f32 radius = 3;
|
|
struct v2 point = xform_mul_v2(e1_xf, ent->weld_joint_data.point_local_e1);
|
|
point = xform_mul_v2(G.world_to_user_xf, point);
|
|
draw_circle(G.ui_gp_flow, point, radius, color, 10);
|
|
|
|
DEBUGBREAKABLE;
|
|
}
|
|
#endif
|
|
|
|
/* Draw mouse joint */
|
|
if (sim_ent_has_prop(ent, SEPROP_MOUSE_JOINT)) {
|
|
struct sim_ent *target = sim_ent_from_id(G.ss_blended, ent->mouse_joint_data.target);
|
|
struct xform target_xf = sim_ent_get_xform(target);
|
|
u32 color = COLOR_WHITE;
|
|
struct v2 point_start = xform_mul_v2(target_xf, ent->mouse_joint_data.point_local_start);
|
|
struct v2 point_end = G.world_cursor;
|
|
point_start = xform_mul_v2(G.world_to_user_xf, point_start);
|
|
point_end = xform_mul_v2(G.world_to_user_xf, point_end);
|
|
draw_arrow_line(G.ui_gp_flow, point_start, point_end, 3, 10, color);
|
|
draw_circle(G.ui_gp_flow, point_start, 4, color, 10);
|
|
}
|
|
|
|
/* Draw collider */
|
|
if (ent->local_collider.count > 0) {
|
|
struct collider_shape collider = ent->local_collider;
|
|
u32 color = RGBA32_F(1, 1, 0, 0.5);
|
|
f32 thickness = 2;
|
|
{
|
|
/* Draw collider using support points */
|
|
u32 detail = 32;
|
|
struct xform collider_draw_xf = xform_mul(G.world_to_user_xf, xf);
|
|
draw_collider_line(G.ui_gp_flow, collider, collider_draw_xf, thickness, color, detail);
|
|
}
|
|
{
|
|
/* Draw collider shape points */
|
|
for (u32 i = 0; i < collider.count; ++i) {
|
|
struct v2 p = xform_mul_v2(xform_mul(G.world_to_user_xf, xf), collider.points[i]);
|
|
draw_circle(G.ui_gp_flow, p, 3, COLOR_BLUE, 10);
|
|
}
|
|
}
|
|
if (collider.count == 1 && collider.radius > 0) {
|
|
/* Draw upwards line for circle */
|
|
struct v2 start = xf.og;
|
|
struct v2 end = collider_get_support_point(&collider, xf, v2_neg(xf.by)).p;
|
|
start = xform_mul_v2(G.world_to_user_xf, start);
|
|
end = xform_mul_v2(G.world_to_user_xf, end);
|
|
draw_line(G.ui_gp_flow, start, end, thickness, color);
|
|
}
|
|
#if 0
|
|
/* Draw support point at focus dir */
|
|
{
|
|
struct v2 p = collider_support_point(&collider, xf, ent->control.focus);
|
|
p = xform_mul_v2(G.world_to_user_xf, p);
|
|
draw_circle(G.ui_gp_flow, p, 3, COLOR_RED, 10);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* Draw contact constraint */
|
|
if (sim_ent_has_prop(ent, SEPROP_CONTACT_CONSTRAINT)) {
|
|
struct phys_contact_constraint *data = &ent->contact_constraint_data;
|
|
struct sim_ent *e0 = sim_ent_from_id(G.ss_blended, data->e0);
|
|
struct sim_ent *e1 = sim_ent_from_id(G.ss_blended, data->e1);
|
|
(UNUSED)e0;
|
|
(UNUSED)e1;
|
|
|
|
#if DEVELOPER
|
|
/* Draw contact points */
|
|
{
|
|
f32 radius = 5;
|
|
for (u32 i = 0; i < data->num_points; ++i) {
|
|
u32 color = (data->skip_solve || data->wrong_dir) ? ALPHA32_F(COLOR_YELLOW, 0.3) : RGBA32_F(0.8, 0.2, 0.2, 1);
|
|
struct phys_contact_point point = data->points[i];
|
|
struct v2 dbg_pt = point.dbg_pt;
|
|
|
|
/* Draw point */
|
|
{
|
|
draw_circle(G.ui_gp_flow, xform_mul_v2(G.world_to_user_xf, dbg_pt), radius, color, 10);
|
|
}
|
|
|
|
/* Draw normal */
|
|
{
|
|
f32 len = 0.1f;
|
|
f32 arrow_thickness = 2;
|
|
f32 arrow_height = 5;
|
|
struct v2 start = xform_mul_v2(G.world_to_user_xf, dbg_pt);
|
|
struct v2 end = xform_mul_v2(G.world_to_user_xf, v2_add(dbg_pt, v2_mul(v2_norm(data->normal), len)));
|
|
draw_arrow_line(G.ui_gp_flow, start, end, arrow_thickness, arrow_height, color);
|
|
}
|
|
#if 0
|
|
/* Draw contact info */
|
|
{
|
|
struct font *disp_font = font_load_async(LIT("font/fixedsys.ttf"), 12.0f);
|
|
if (disp_font) {
|
|
f32 offset_px = 10;
|
|
|
|
struct string fmt = LIT(
|
|
"e0 index: %F\n"
|
|
"e1 index: %F\n"
|
|
"id: 0x%F\n"
|
|
"impulse (n): %F\n"
|
|
"impulse (t): %F\n"
|
|
"separation: %F\n"
|
|
"normal: (%F, %F)\n"
|
|
"num contacts: %F"
|
|
);
|
|
struct string text = string_format(temp.arena, fmt,
|
|
FMT_UINT(e0->handle.idx),
|
|
FMT_UINT(e1->handle.idx),
|
|
FMT_HEX(point.id),
|
|
FMT_FLOAT(point.normal_impulse),
|
|
FMT_FLOAT(point.tangent_impulse),
|
|
FMT_FLOAT_P(point.starting_separation, 6),
|
|
FMT_FLOAT_P(data->normal.x, 6), FMT_FLOAT_P(data->normal.y, 6),
|
|
FMT_UINT(data->num_points));
|
|
|
|
|
|
draw_text(G.ui_gp_flow, disp_font, v2_add(v2_round(xform_mul_v2(G.world_to_user_xf, dbg_pt)), V2(0, offset_px)), text);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* Draw collision debug */
|
|
#if COLLIDER_DEBUG
|
|
if (sim_ent_has_prop(ent, SEPROP_COLLISION_DEBUG)) {
|
|
struct phys_collision_debug *data = &ent->collision_debug_data;
|
|
struct collider_collision_points_result collider_res = data->res;
|
|
struct sim_ent *e0 = sim_ent_from_id(G.ss_blended, data->e0);
|
|
struct sim_ent *e1 = sim_ent_from_id(G.ss_blended, data->e1);
|
|
struct collider_shape e0_collider = e0->local_collider;
|
|
struct collider_shape e1_collider = e1->local_collider;
|
|
(UNUSED)e0_collider;
|
|
(UNUSED)e1_collider;
|
|
|
|
/* Draw closest points */
|
|
#if 0
|
|
{
|
|
f32 radius = 4;
|
|
u32 color = RGBA32_F(1, 1, 0, 0.5);
|
|
struct v2 a = xform_mul_v2(G.world_to_user_xf, data->closest0);
|
|
struct v2 b = xform_mul_v2(G.world_to_user_xf, data->closest1);
|
|
draw_circle(G.ui_gp_flow, a, radius, color, 10);
|
|
draw_circle(G.ui_gp_flow, b, radius, color, 10);
|
|
}
|
|
#endif
|
|
|
|
/* Draw clipping */
|
|
{
|
|
f32 thickness = 4;
|
|
f32 radius = 4;
|
|
u32 color_line = RGBA32_F(1, 0, 1, 0.75);
|
|
u32 color_a = RGBA32_F(1, 0, 0, 0.25);
|
|
u32 color_b = RGBA32_F(0, 1, 0, 0.25);
|
|
u32 color_line_clipped = RGBA32_F(1, 0, 1, 1);
|
|
u32 color_a_clipped = RGBA32_F(1, 0, 0, 1);
|
|
u32 color_b_clipped = RGBA32_F(0, 1, 0, 1);
|
|
{
|
|
struct v2 a = xform_mul_v2(G.world_to_user_xf, collider_res.a0);
|
|
struct v2 b = xform_mul_v2(G.world_to_user_xf, collider_res.b0);
|
|
draw_line(G.ui_gp_flow, a, b, thickness, color_line);
|
|
draw_circle(G.ui_gp_flow, a, radius, color_a, 10);
|
|
draw_circle(G.ui_gp_flow, b, radius, color_b, 10);
|
|
|
|
struct v2 a_clipped = xform_mul_v2(G.world_to_user_xf, collider_res.a0_clipped);
|
|
struct v2 b_clipped = xform_mul_v2(G.world_to_user_xf, collider_res.b0_clipped);
|
|
draw_line(G.ui_gp_flow, a_clipped, b_clipped, thickness, color_line_clipped);
|
|
draw_circle(G.ui_gp_flow, a_clipped, radius, color_a_clipped, 10);
|
|
draw_circle(G.ui_gp_flow, b_clipped, radius, color_b_clipped, 10);
|
|
}
|
|
{
|
|
struct v2 a = xform_mul_v2(G.world_to_user_xf, collider_res.a1);
|
|
struct v2 b = xform_mul_v2(G.world_to_user_xf, collider_res.b1);
|
|
draw_line(G.ui_gp_flow, a, b, thickness, color_line);
|
|
draw_circle(G.ui_gp_flow, a, radius, color_a, 10);
|
|
draw_circle(G.ui_gp_flow, b, radius, color_b, 10);
|
|
|
|
struct v2 a_clipped = xform_mul_v2(G.world_to_user_xf, collider_res.a1_clipped);
|
|
struct v2 b_clipped = xform_mul_v2(G.world_to_user_xf, collider_res.b1_clipped);
|
|
draw_line(G.ui_gp_flow, a_clipped, b_clipped, thickness, color_line_clipped);
|
|
draw_circle(G.ui_gp_flow, a_clipped, radius, color_a_clipped, 10);
|
|
draw_circle(G.ui_gp_flow, b_clipped, radius, color_b_clipped, 10);
|
|
}
|
|
}
|
|
|
|
#if COLLIDER_DEBUG_DETAILED_DRAW_MENKOWSKI
|
|
struct xform e0_xf = data->xf0;
|
|
struct xform e1_xf = data->xf1;
|
|
|
|
#if 0
|
|
/* Only draw points with large separation */
|
|
b32 should_draw = false;
|
|
for (u32 i = 0; i < data->num_points; ++i) {
|
|
if (data->points[i].starting_separation < -0.1) {
|
|
should_draw = true;
|
|
break;
|
|
}
|
|
}
|
|
#else
|
|
b32 should_draw = true;
|
|
#endif
|
|
|
|
if (should_draw) {
|
|
#if 0
|
|
/* Test info */
|
|
{
|
|
struct font *disp_font = font_load_async(LIT("font/fixedsys.ttf"), 12.0f);
|
|
if (disp_font) {
|
|
f32 offset_px = 10;
|
|
struct string fmt = LIT(
|
|
"e0 pos: (%F, %F)\n"
|
|
"e0 rot: %F\n"
|
|
"e1 pos: (%F, %F)\n"
|
|
"e1 rot: %F\n"
|
|
);
|
|
struct string text = string_format(temp.arena, fmt,
|
|
FMT_FLOAT_P(e0_xf.og.x, 24), FMT_FLOAT_P(e0_xf.og.y, 24),
|
|
FMT_FLOAT_P(xform_get_rotation(e0_xf), 24),
|
|
FMT_FLOAT_P(e1_xf.og.x, 24), FMT_FLOAT_P(e1_xf.og.y, 24),
|
|
FMT_FLOAT_P(xform_get_rotation(e1_xf), 24));
|
|
|
|
|
|
draw_text(G.ui_gp_flow, disp_font, v2_add(v2_round(xform_mul_v2(G.world_to_user_xf, V2(0, 0))), V2(0, offset_px)), text);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Draw menkowski */
|
|
{
|
|
|
|
u32 color = collider_res.solved ? RGBA32_F(0, 0, 0.25, 1) : RGBA32_F(0, 0.25, 0.25, 1);
|
|
f32 thickness = 2;
|
|
u32 detail = 512;
|
|
(UNUSED)thickness;
|
|
|
|
struct v2_array m = menkowski(temp.arena, &e0_collider, &e1_collider, e0_xf, e1_xf, detail);
|
|
|
|
for (u64 i = 0; i < m.count; ++i) m.points[i] = xform_mul_v2(G.world_to_user_xf, m.points[i]);
|
|
draw_poly_line(G.ui_gp_flow, m, true, thickness, color);
|
|
//draw_poly(G.ui_gp_flow, m, color);
|
|
}
|
|
|
|
/* Draw cloud */
|
|
{
|
|
u32 color = RGBA32_F(1, 1, 1, 1);
|
|
f32 radius = 2;
|
|
|
|
struct v2_array m = cloud(temp.arena, &e0_collider, &e1_collider, e0_xf, e1_xf);
|
|
|
|
for (u64 i = 0; i < m.count; ++i) {
|
|
struct v2 p = xform_mul_v2(G.world_to_user_xf, m.points[i]);
|
|
draw_circle(G.ui_gp_flow, p, radius, color, 10);
|
|
}
|
|
}
|
|
|
|
/* Draw prototype */
|
|
{
|
|
f32 thickness = 2;
|
|
u32 color = RGBA32_F(1, 1, 1, 0.25);
|
|
|
|
struct v2_array m = {
|
|
.points = collider_res.prototype.points,
|
|
.count = collider_res.prototype.len
|
|
};
|
|
for (u64 i = 0; i < m.count; ++i) m.points[i] = xform_mul_v2(G.world_to_user_xf, m.points[i]);
|
|
draw_poly_line(G.ui_gp_flow, m, true, thickness, color);
|
|
for (u64 i = 0; i < m.count; ++i) draw_circle(G.ui_gp_flow, m.points[i], 10, color, 10);
|
|
}
|
|
|
|
/* Draw simplex */
|
|
{
|
|
f32 thickness = 2;
|
|
u32 line_color = COLOR_YELLOW;
|
|
u32 color_first = RGBA32_F(1, 0, 0, 0.75);
|
|
u32 color_second = RGBA32_F(0, 1, 0, 0.75);
|
|
u32 color_third = RGBA32_F(0, 0, 1, 0.75);
|
|
|
|
struct collider_menkowski_simplex simplex = collider_res.simplex;
|
|
struct v2 simplex_points[] = { simplex.a.p, simplex.b.p, simplex.c.p };
|
|
for (u64 i = 0; i < ARRAY_COUNT(simplex_points); ++i) simplex_points[i] = xform_mul_v2(G.world_to_user_xf, simplex_points[i]);
|
|
struct v2_array simplex_array = { .count = simplex.len, .points = simplex_points };
|
|
|
|
if (simplex.len >= 1) {
|
|
u32 color = simplex.len == 1 ? color_first : (simplex.len == 2 ? color_second : color_third);
|
|
draw_circle(G.ui_gp_flow, simplex_array.points[0], thickness * 3, color, 10);
|
|
}
|
|
if (simplex.len >= 2) {
|
|
u32 color = simplex.len == 2 ? color_first : color_second;
|
|
draw_circle(G.ui_gp_flow, simplex_array.points[1], thickness * 3, color, 10);
|
|
}
|
|
if (simplex.len >= 3) {
|
|
u32 color = color_first;
|
|
draw_circle(G.ui_gp_flow, simplex_array.points[2], thickness * 3, color, 10);
|
|
}
|
|
if (simplex.len >= 2) {
|
|
draw_poly_line(G.ui_gp_flow, simplex_array, simplex.len > 2, thickness, line_color);
|
|
}
|
|
}
|
|
|
|
/* Draw normal */
|
|
{
|
|
u32 color = COLOR_WHITE;
|
|
f32 len = 0.1f;
|
|
f32 arrow_thickness = 4;
|
|
f32 arrowhead_height = 10;
|
|
struct v2 start = xform_mul_v2(G.world_to_user_xf, V2(0, 0));
|
|
struct v2 end = xform_mul_v2(G.world_to_user_xf, v2_mul(v2_norm(collider_res.normal), len));
|
|
draw_arrow_line(G.ui_gp_flow, start, end, arrow_thickness, arrowhead_height, color);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
/* Draw hierarchy */
|
|
if (sim_ent_has_prop(parent, SEPROP_ACTIVE) && !parent->is_root) {
|
|
u32 color = RGBA32_F(0.6, 0.6, 1, 0.75);
|
|
f32 thickness = 2;
|
|
f32 arrow_height = 15;
|
|
|
|
struct v2 start = xform_mul_v2(G.world_to_user_xf, xf.og);
|
|
struct v2 end = xform_mul_v2(G.world_to_user_xf, parent_xf.og);
|
|
draw_arrow_line(G.ui_gp_flow, start, end, thickness, arrow_height, color);
|
|
}
|
|
|
|
/* Draw camera rect */
|
|
if (sim_ent_has_prop(ent, SEPROP_CAMERA)) {
|
|
u32 color = ent == local_camera ? RGBA32_F(1, 1, 1, 0.5) : RGBA32_F(0, 0.75, 0, 0.5);
|
|
f32 thickness = 3;
|
|
|
|
struct xform quad_xf = xform_mul(xf, ent->camera_quad_xform);
|
|
struct quad quad = xform_mul_quad(quad_xf, QUAD_UNIT_SQUARE_CENTERED);
|
|
quad = xform_mul_quad(G.world_to_user_xf, quad);
|
|
|
|
draw_quad_line(G.ui_gp_flow, quad, thickness, color);
|
|
}
|
|
|
|
arena_temp_end(temp);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Draw crosshair or show cursor */
|
|
if (!G.debug_camera) {
|
|
__profscope(draw_crosshair);
|
|
struct v2 crosshair_pos = G.user_cursor;
|
|
|
|
struct sprite_tag crosshair_tag = sprite_tag_from_path(LIT("sprite/crosshair.ase"));
|
|
struct sprite_texture *t = sprite_texture_from_tag_async(sprite_frame_scope, crosshair_tag);
|
|
|
|
struct v2 size = V2(t->width, t->height);
|
|
struct xform xf = XFORM_TRS(.t = crosshair_pos, .s = size);
|
|
draw_texture(G.ui_gp_flow, DRAW_TEXTURE_PARAMS(.xf = xf, .sprite = crosshair_tag));
|
|
|
|
struct rect cursor_clip = RECT_FROM_V2(G.user_screen_offset, G.user_size);
|
|
cursor_clip.pos = v2_add(cursor_clip.pos, v2_mul(size, 0.5f));
|
|
cursor_clip.pos = v2_add(cursor_clip.pos, V2(1, 1));
|
|
cursor_clip.size = v2_sub(cursor_clip.size, size);
|
|
sys_window_cursor_hide(G.window);
|
|
sys_window_cursor_enable_clip(G.window, cursor_clip);
|
|
} else {
|
|
__profscope(update_window_cursor);
|
|
sys_window_cursor_disable_clip(G.window);
|
|
sys_window_cursor_show(G.window);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Create user sim cmd
|
|
* ========================== */
|
|
|
|
{
|
|
/* Queue player move cmd */
|
|
f32 move_speed = 1.0f;
|
|
if (G.bind_states[USER_BIND_KIND_WALK].is_held) {
|
|
//const f32 walk_ratio = 0.25f;
|
|
const f32 walk_ratio = 0.05f;
|
|
move_speed *= walk_ratio;
|
|
}
|
|
|
|
struct v2 input_move_dir = ZI;
|
|
{
|
|
for (enum user_bind_kind bind = 0; bind < (i32)ARRAY_COUNT(G.bind_states); ++bind) {
|
|
struct bind_state state = G.bind_states[bind];
|
|
|
|
if (!state.is_held && state.num_presses <= 0) {
|
|
continue;
|
|
}
|
|
|
|
switch (bind) {
|
|
/* Movement */
|
|
case USER_BIND_KIND_MOVE_UP:
|
|
{
|
|
input_move_dir.y -= 1;
|
|
} break;
|
|
|
|
case USER_BIND_KIND_MOVE_DOWN:
|
|
{
|
|
input_move_dir.y += 1;
|
|
} break;
|
|
|
|
case USER_BIND_KIND_MOVE_LEFT:
|
|
{
|
|
input_move_dir.x -= 1;
|
|
} break;
|
|
|
|
case USER_BIND_KIND_MOVE_RIGHT:
|
|
{
|
|
input_move_dir.x += 1;
|
|
} break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
input_move_dir = xform_basis_invert_mul_v2(G.world_to_user_xf, input_move_dir); /* Make move dir relative to world view */
|
|
input_move_dir = v2_mul(v2_norm(input_move_dir), move_speed);
|
|
}
|
|
|
|
if (!G.debug_camera) {
|
|
G.focus_send = v2_sub(G.world_cursor, sim_ent_get_xform(local_control).og);
|
|
}
|
|
struct v2 input_aim_dir = G.focus_send;
|
|
|
|
/* Queue player control cmd */
|
|
{
|
|
struct sim_control control = ZI;
|
|
control.move = input_move_dir;
|
|
control.focus = input_aim_dir;
|
|
control.dbg_cursor = G.world_cursor;
|
|
|
|
struct bind_state fire_state = G.bind_states[USER_BIND_KIND_FIRE];
|
|
struct bind_state fire_alt_state = G.bind_states[USER_BIND_KIND_FIRE_ALT];
|
|
struct bind_state drag_state = G.bind_states[USER_BIND_KIND_DEBUG_DRAG];
|
|
struct bind_state delete_state = G.bind_states[USER_BIND_KIND_DEBUG_DELETE];
|
|
struct bind_state clear_state = G.bind_states[USER_BIND_KIND_DEBUG_CLEAR];
|
|
struct bind_state spawn1_state = G.bind_states[USER_BIND_KIND_DEBUG_SPAWN1];
|
|
struct bind_state spawn2_state = G.bind_states[USER_BIND_KIND_DEBUG_SPAWN2];
|
|
struct bind_state spawn3_state = G.bind_states[USER_BIND_KIND_DEBUG_SPAWN3];
|
|
struct bind_state spawn4_state = G.bind_states[USER_BIND_KIND_DEBUG_SPAWN4];
|
|
struct bind_state walls_state = G.bind_states[USER_BIND_KIND_DEBUG_WALLS];
|
|
struct bind_state pause_state = G.bind_states[USER_BIND_KIND_DEBUG_PAUSE];
|
|
struct bind_state step_state = G.bind_states[USER_BIND_KIND_DEBUG_STEP];
|
|
struct bind_state tile_state = G.bind_states[USER_BIND_KIND_TILE_TEST];
|
|
struct bind_state explode_state = G.bind_states[USER_BIND_KIND_DEBUG_EXPLODE];
|
|
struct bind_state teleport_state = G.bind_states[USER_BIND_KIND_DEBUG_TELEPORT];
|
|
|
|
if (fire_state.num_presses || fire_state.is_held) {
|
|
control.flags |= SIM_CONTROL_FLAG_FIRE;
|
|
}
|
|
if (fire_alt_state.num_presses || fire_alt_state.is_held) {
|
|
control.flags |= SIM_CONTROL_FLAG_FIRE_ALT;
|
|
}
|
|
if (drag_state.num_presses || drag_state.is_held) {
|
|
control.flags |= SIM_CONTROL_FLAG_DRAG;
|
|
}
|
|
if (delete_state.num_presses || delete_state.is_held) {
|
|
control.flags |= SIM_CONTROL_FLAG_DELETE;
|
|
}
|
|
if (clear_state.num_presses_and_repeats) {
|
|
control.flags |= SIM_CONTROL_FLAG_CLEAR_ALL;
|
|
}
|
|
if (spawn1_state.num_presses_and_repeats) {
|
|
control.flags |= SIM_CONTROL_FLAG_SPAWN1_TEST;
|
|
}
|
|
if (spawn2_state.num_presses_and_repeats) {
|
|
control.flags |= SIM_CONTROL_FLAG_SPAWN2_TEST;
|
|
}
|
|
if (spawn3_state.num_presses_and_repeats) {
|
|
control.flags |= SIM_CONTROL_FLAG_SPAWN3_TEST;
|
|
}
|
|
if (spawn4_state.num_presses_and_repeats) {
|
|
control.flags |= SIM_CONTROL_FLAG_SPAWN4_TEST;
|
|
}
|
|
if (walls_state.num_presses_and_repeats) {
|
|
control.flags |= SIM_CONTROL_FLAG_WALLS_TEST;
|
|
}
|
|
if (tile_state.num_presses || tile_state.is_held) {
|
|
control.flags |= SIM_CONTROL_FLAG_TILE_TEST;
|
|
}
|
|
if (explode_state.num_presses_and_repeats) {
|
|
control.flags |= SIM_CONTROL_FLAG_EXPLODE_TEST;
|
|
}
|
|
if (teleport_state.num_presses_and_repeats || (G.debug_camera && teleport_state.is_held)) {
|
|
control.flags |= SIM_CONTROL_FLAG_TELEPORT_TEST;
|
|
}
|
|
|
|
if (pause_state.num_presses) {
|
|
atomic_i32_eval_xor(&G.user_paused, 1);
|
|
}
|
|
atomic_i32_eval_add(&G.user_paused_steps, step_state.num_presses_and_repeats);
|
|
|
|
/* Set user sim control */
|
|
{
|
|
struct sys_lock lock = sys_mutex_lock_e(G.user_sim_cmd_mutex);
|
|
|
|
/* Reset flags */
|
|
if (G.user_sim_cmd_gen != G.last_user_sim_cmd_gen) {
|
|
G.user_sim_cmd_control.flags = 0;
|
|
G.last_user_sim_cmd_gen = G.user_sim_cmd_gen;
|
|
}
|
|
|
|
u32 old_flags = G.user_sim_cmd_control.flags;
|
|
G.user_sim_cmd_control = control;
|
|
G.user_sim_cmd_control.flags |= old_flags;
|
|
G.user_hovered_ent = hovered_ent->id;
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
}
|
|
|
|
#if COLLIDER_DEBUG
|
|
/* Gjk steps */
|
|
{
|
|
i64 new_steps = collider_debug_steps;
|
|
new_steps += G.bind_states[USER_BIND_KIND_INCR_COLLIDER_GJK_STEPS].num_presses_and_repeats;
|
|
new_steps -= G.bind_states[USER_BIND_KIND_DECR_COLLIDER_GJK_STEPS].num_presses_and_repeats;
|
|
if (G.bind_states[USER_BIND_KIND_RESET_COLLIDER_GJK_STEPS].num_presses_and_repeats > 0) new_steps = 0;
|
|
collider_debug_steps = (u32)clamp_i64(new_steps, 0, U32_MAX);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
{
|
|
/* Update network usage stats */
|
|
i64 stat_now_ns = sys_time_ns();
|
|
G.net_bytes_read.last_second_end = gstat_get(GSTAT_SOCK_BYTES_RECEIVED);
|
|
G.net_bytes_sent.last_second_end = gstat_get(GSTAT_SOCK_BYTES_SENT);
|
|
if (stat_now_ns - G.last_second_reset_ns > NS_FROM_SECONDS(1)) {
|
|
G.last_second_reset_ns = stat_now_ns;
|
|
G.net_bytes_read.last_second = G.net_bytes_read.last_second_end - G.net_bytes_read.last_second_start;
|
|
G.net_bytes_sent.last_second = G.net_bytes_sent.last_second_end - G.net_bytes_sent.last_second_start;
|
|
G.net_bytes_read.last_second_start = G.net_bytes_read.last_second_end;
|
|
G.net_bytes_sent.last_second_start = G.net_bytes_sent.last_second_end;
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Draw ent debug info
|
|
* ========================== */
|
|
|
|
if (G.debug_draw && hovered_ent->valid) {
|
|
struct sim_ent *ent = hovered_ent;
|
|
|
|
struct v2 pos = v2_add(G.user_cursor, V2(15, 15));
|
|
struct font *font = font_load_async(LIT("font/fixedsys.ttf"), 12.0f);
|
|
if (font) {
|
|
struct arena_temp temp = arena_temp_begin(scratch.arena);
|
|
|
|
struct string dbg_text = ZI;
|
|
dbg_text.text = arena_push_dry(temp.arena, u8);
|
|
dbg_text.len += get_ent_debug_text(temp.arena, ent).len;
|
|
draw_text(G.ui_gp_flow, DRAW_TEXT_PARAMS(.font = font, .pos = pos, .str = dbg_text));
|
|
|
|
arena_temp_end(temp);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Query vram
|
|
* ========================== */
|
|
|
|
struct gp_memory_info vram = gp_query_memory_info();
|
|
|
|
/* ========================== *
|
|
* Draw global debug info
|
|
* ========================== */
|
|
|
|
if (G.debug_draw) {
|
|
struct font *font = font_load_async(LIT("font/fixedsys.ttf"), 12.0f);
|
|
if (font) {
|
|
struct arena_temp temp = arena_temp_begin(scratch.arena);
|
|
struct string text = ZI;
|
|
text.text = arena_push_dry(temp.arena, u8);
|
|
|
|
#if BITBUFF_DEBUG
|
|
text.len += string_format(temp.arena, LIT("(bitbuff debug enabled)")).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
#endif
|
|
|
|
text.len += string_format(temp.arena, LIT("blended world entities: %F/%F"), FMT_UINT(G.ss_blended->num_ents_allocated), FMT_UINT(G.ss_blended->num_ents_reserved)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("blended world tick: %F"), FMT_UINT(G.ss_blended->tick)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("blended world time: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.ss_blended->sim_time_ns))).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("average local sim publish dt: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.average_local_to_user_snapshot_publish_dt_ns))).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("local sim last known tick: %F"), FMT_UINT(G.local_sim_last_known_tick)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("local sim last known time: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.local_sim_last_known_time_ns))).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("local sim predicted time: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.local_sim_predicted_time_ns))).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("render time target: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.render_time_target_ns))).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("render time: %F"), FMT_FLOAT(SECONDS_FROM_NS(G.render_time_ns))).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("local player: [%F]"), FMT_UID(local_player->id.uid)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
struct v2 world_cursor = G.world_cursor;
|
|
text.len += string_format(temp.arena, LIT("cursor world: %F, %F"), FMT_FLOAT(world_cursor.x), FMT_FLOAT(world_cursor.y)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
struct v2i32 world_tile_cursor = sim_world_tile_index_from_pos(world_cursor);
|
|
text.len += string_format(temp.arena, LIT("cursor world tile: %F, %F"), FMT_SINT(world_tile_cursor.x), FMT_SINT(world_tile_cursor.y)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
struct v2i32 local_tile_cursor = sim_local_tile_index_from_world_tile_index(world_tile_cursor);
|
|
text.len += string_format(temp.arena, LIT("cursor local tile: %F, %F"), FMT_SINT(local_tile_cursor.x), FMT_SINT(local_tile_cursor.y)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
struct v2i32 tile_chunk_cursor = sim_tile_chunk_index_from_world_tile_index(world_tile_cursor);
|
|
text.len += string_format(temp.arena, LIT("cursor tile chunk: %F, %F"), FMT_SINT(tile_chunk_cursor.x), FMT_SINT(tile_chunk_cursor.y)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Network read: %F mbit/s"), FMT_FLOAT((f64)G.net_bytes_read.last_second * 8 / 1000 / 1000)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Network write: %F mbit/s"), FMT_FLOAT((f64)G.net_bytes_sent.last_second * 8 / 1000 / 1000)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Ping (real): %F ms"), FMT_FLOAT(SECONDS_FROM_NS(local_player->player_last_rtt_ns) * 1000)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Ping (average): %F ms"), FMT_FLOAT(local_player->player_average_rtt_seconds * 1000)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Memory committed: %F MiB"), FMT_FLOAT((f64)gstat_get(GSTAT_MEMORY_COMMITTED) / 1024 / 1024)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Virtual memory reserved: %F TiB"), FMT_FLOAT((f64)gstat_get(GSTAT_MEMORY_RESERVED) / 1024 / 1024 / 1024 / 1024)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Arenas allocated: %F"), FMT_UINT(gstat_get(GSTAT_NUM_ARENAS))).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
text.len += string_format(temp.arena, LIT("Video memory (GPU): %F MiB"), FMT_FLOAT((f64)vram.local_used / 1024 / 1024)).len;
|
|
text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
text.len += string_format(temp.arena, LIT("Video memory (shared): %F MiB"), FMT_FLOAT((f64)vram.non_local_used / 1024 / 1024)).len;
|
|
//text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
//text.len += string_copy(temp.arena, LIT("\n")).len;
|
|
|
|
#if COLLIDER_DEBUG
|
|
draw_text(G.ui_gp_flow, font, pos, string_format(temp.arena, LIT("collider gjk steps: %F"), FMT_UINT(collider_debug_steps)));
|
|
pos.y += spacing;
|
|
#endif
|
|
|
|
//draw_text(G.ui_gp_flow, font, pos, string_format(temp.arena, LIT("blended world entities: %F/%F"), FMT_UINT(G.ss_blended->num_ents_allocated), FMT_UINT(G.ss_blended->num_ents_reserved)));
|
|
//draw_text(G.ui_gp_flow, font, pos, text);
|
|
|
|
struct v2 pos = V2(10, G.user_size.y);
|
|
enum draw_text_offset_y offset_y = DRAW_TEXT_OFFSET_Y_BOTTOM;
|
|
draw_text(G.ui_gp_flow, DRAW_TEXT_PARAMS(.font = font, .pos = pos, .str = text, .offset_y = offset_y, .color = COLOR_WHITE));
|
|
arena_temp_end(temp);
|
|
}
|
|
}
|
|
|
|
{
|
|
#if DEVELOPER
|
|
b32 console_minimized = !G.debug_console;
|
|
i32 console_level = console_minimized ? LOG_LEVEL_SUCCESS: LOG_LEVEL_DEBUG;
|
|
draw_debug_console(console_level, console_minimized);
|
|
#else
|
|
if (G.debug_draw) {
|
|
draw_debug_console(LOG_LEVEL_INFO, false);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* ========================== *
|
|
* Render
|
|
* ========================== */
|
|
|
|
{
|
|
__profscope(render);
|
|
|
|
struct rect user_viewport = RECT_FROM_V2(V2(0, 0), G.user_size);
|
|
struct v2i32 user_resolution = v2_round_to_int(user_viewport.size);
|
|
struct v2i32 backbuffer_resolution = v2_round_to_int(G.screen_size);
|
|
|
|
/* Allocate user texture */
|
|
if (!G.user_texture.v || !v2i32_eq(gp_texture_get_size(G.user_texture), user_resolution)) {
|
|
if (G.user_texture.v) {
|
|
gp_release(G.user_texture);
|
|
}
|
|
G.user_texture = gp_texture_alloc(GP_TEXTURE_FORMAT_R8G8B8A8_UNORM, GP_TEXTURE_FLAG_TARGETABLE, user_resolution, NULL);
|
|
}
|
|
|
|
/* Render world to user texture */
|
|
{
|
|
struct gp_dispatch_params params = ZI;
|
|
params.flow = G.world_gp_flow;
|
|
params.draw_target = G.user_texture;
|
|
params.draw_target_viewport = user_viewport;
|
|
params.draw_target_view = G.world_to_user_xf;
|
|
params.clear_target = true;
|
|
gp_dispatch(params);
|
|
}
|
|
|
|
/* Render ui to user texture */
|
|
{
|
|
struct gp_dispatch_params params = ZI;
|
|
params.flow = G.ui_gp_flow;
|
|
params.draw_target = G.user_texture;
|
|
params.draw_target_viewport = user_viewport;
|
|
params.draw_target_view = XFORM_IDENT;
|
|
gp_dispatch(params);
|
|
}
|
|
|
|
/* Present user texture */
|
|
gp_present(G.window, backbuffer_resolution, G.user_texture, XFORM_TRS(.t = v2_mul(G.screen_size, 0.5), .s = G.user_size), VSYNC_ENABLED);
|
|
}
|
|
|
|
/* ========================== *
|
|
* End frame cache scopes
|
|
* ========================== */
|
|
|
|
sprite_scope_end(sprite_frame_scope);
|
|
|
|
scratch_end(scratch);
|
|
}
|
|
|
|
/* ========================== *
|
|
* User thread
|
|
* ========================== */
|
|
|
|
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_thread_entry_point, arg)
|
|
{
|
|
(UNUSED)arg;
|
|
i64 last_frame_ns = 0;
|
|
i64 target_dt_ns = NS_FROM_SECONDS(USER_FPS_LIMIT > (0) ? (1.0 / USER_FPS_LIMIT) : 0);
|
|
|
|
while (!atomic_i32_eval(&G.user_thread_shutdown)) {
|
|
__profscope(user_update_w_sleep);
|
|
sleep_frame(last_frame_ns, target_dt_ns);
|
|
last_frame_ns = sys_time_ns();
|
|
user_update();
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Local sim thread
|
|
* ========================== */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
INTERNAL void generate_user_input_cmds(struct sim_client *user_input_client, u64 tick)
|
|
{
|
|
struct sim_snapshot *prev_user_input_ss = sim_snapshot_from_tick(user_input_client, user_input_client->last_tick);
|
|
struct sim_snapshot *user_input_ss = sim_snapshot_alloc(user_input_client, prev_user_input_ss, tick);
|
|
struct sim_ent *user_input_root = sim_ent_from_id(user_input_ss, SIM_ENT_ROOT_ID);
|
|
/* Find / create local control cmd ent */
|
|
struct sim_ent *control_cmd = sim_ent_find_first_match_one(user_input_ss, SEPROP_CMD);
|
|
if (!control_cmd->valid) {
|
|
control_cmd = sim_ent_alloc_sync_src(user_input_root);
|
|
control_cmd->cmd_kind = SIM_CMD_KIND_CONTROL;
|
|
control_cmd->predictor = user_input_client->player_id;
|
|
sim_ent_enable_prop(control_cmd, SEPROP_CMD);
|
|
sim_ent_activate(control_cmd, user_input_ss->tick);
|
|
}
|
|
{
|
|
struct sys_lock lock = sys_mutex_lock_e(G.user_sim_cmd_mutex);
|
|
/* Update control cmd */
|
|
{
|
|
control_cmd->cmd_control = G.user_sim_cmd_control;
|
|
control_cmd->cmd_control_hovered_ent = G.user_hovered_ent;
|
|
}
|
|
#if 0
|
|
/* Create chat cmd */
|
|
if (G.user_sim_cmd_chat.len > 0) {
|
|
struct sim_ent *chat_cmd = sim_ent_alloc_sync_src(user_input_root);
|
|
chat_cmd->cmd_kind = SIM_CMD_KIND_CHAT;
|
|
//chat_cmd->chat_msg = ZI
|
|
}
|
|
#endif
|
|
++G.user_sim_cmd_gen;
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct sim_ss_decode_node {
|
|
struct sim_client *client;
|
|
u64 tick;
|
|
u64 base_tick;
|
|
struct string tmp_encoded;
|
|
struct sim_ss_decode_node *next;
|
|
};
|
|
|
|
struct sim_decode_queue {
|
|
struct sim_ss_decode_node *first;
|
|
struct sim_ss_decode_node *last;
|
|
};
|
|
|
|
|
|
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_local_sim_thread_entry_point, arg)
|
|
{
|
|
#if 0
|
|
struct host_listen_address local_listen_addr = host_listen_address_from_local_name(LIT("LOCAL_SIM"));
|
|
struct host_listen_address net_listen_addr = host_listen_address_from_net_port(12345);
|
|
//struct host *host = host_alloc();
|
|
/* TODO: Host system should allocate & copy string stored in local_listen_addr */
|
|
//host_listen(host, local_listen_addr);
|
|
//host_listen(host, net_listen_addr);
|
|
#endif
|
|
(UNUSED)arg;
|
|
|
|
b32 is_master = false;
|
|
struct host *host;
|
|
if (G.connect_address_str.len > 0) {
|
|
host = host_alloc(0);
|
|
struct sock_address addr = sock_address_from_string(G.connect_address_str);
|
|
host_queue_connect_to_address(host, addr);
|
|
} else {
|
|
host = host_alloc(12345);
|
|
is_master = true;
|
|
}
|
|
|
|
struct bitbuff msg_writer_bb = bitbuff_alloc(GIGABYTE(64));
|
|
struct bitbuff snapshot_writer_bb = bitbuff_alloc(GIGABYTE(64));
|
|
struct sim_accel accel = sim_accel_alloc();
|
|
|
|
struct sim_client_store *store = sim_client_store_alloc();
|
|
struct sim_client *user_input_client = sim_client_alloc(store); /* Stores snapshots containing commands to be published to local client */
|
|
struct sim_client *local_client = sim_client_alloc(store); /* Stores snapshots produced locally */
|
|
struct sim_client *publish_client = sim_client_alloc(store); /* Stores versions of local snapshots that will be published to remote sims */
|
|
|
|
struct sim_client *master_client = sim_client_nil(); /* Stores snapshots received from master */
|
|
struct sim_client *master_blended_client = sim_client_nil(); /* Stores interpolated master snapshots */
|
|
b32 initialized_from_master = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
i64 master_blend_time_ns = 0;
|
|
i64 average_master_receive_dt_ns = 0;
|
|
i64 last_tick_from_master_received_at_ns = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
i64 last_publish_to_user_ns = 0;
|
|
i64 real_time_ns = 0;
|
|
i64 real_dt_ns = 0;
|
|
i64 step_dt_ns = NS_FROM_SECONDS(1) / SIM_TICKS_PER_SECOND;
|
|
f64 compute_timescale = 1.0;
|
|
while (!atomic_i32_eval(&G.local_sim_thread_shutdown)) {
|
|
__profscope(local_sim_loop);
|
|
struct arena_temp scratch = scratch_begin_no_conflict();
|
|
{
|
|
__profscope(local_sim_sleep);
|
|
sleep_frame(real_time_ns, step_dt_ns * compute_timescale);
|
|
}
|
|
real_dt_ns = sys_time_ns() - real_time_ns;
|
|
real_time_ns += real_dt_ns;
|
|
|
|
struct host_event_list host_events = host_update_begin(scratch.arena, host);
|
|
|
|
/* Read net messages */
|
|
struct sim_decode_queue queue = ZI;
|
|
{
|
|
for (struct host_event *event = host_events.first; event; event = event->next) {
|
|
struct host_channel_id channel_id = event->channel_id;
|
|
struct sim_client *client = sim_client_from_channel_id(store, channel_id);
|
|
switch (event->kind) {
|
|
case HOST_EVENT_KIND_CHANNEL_OPENED:
|
|
{
|
|
if (!client->valid) {
|
|
if (is_master) {
|
|
/* Create remote client */
|
|
client = sim_client_alloc(store);
|
|
sim_client_set_channel_id(client, channel_id);
|
|
} else {
|
|
/* Create master client */
|
|
if (!master_client->valid) {
|
|
client = sim_client_alloc(store);
|
|
sim_client_set_channel_id(client, channel_id);
|
|
master_client = client;
|
|
master_blended_client = sim_client_alloc(store);
|
|
} else {
|
|
/* We already have a master client */
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case HOST_EVENT_KIND_MSG:
|
|
{
|
|
if (client->valid) {
|
|
struct bitbuff msg_bb = bitbuff_from_string(event->msg);
|
|
struct bitbuff_reader msg_br = br_from_bitbuff(&msg_bb);
|
|
|
|
u64 ack = br_read_uv(&msg_br);
|
|
u64 double_ack = br_read_uv(&msg_br);
|
|
if (ack > client->ack) {
|
|
client->ack = ack;
|
|
}
|
|
if (double_ack > client->double_ack) {
|
|
client->double_ack = double_ack;
|
|
}
|
|
|
|
/* Read & queue incoming snapshots for decoding */
|
|
u64 tmp_encoded_len = br_read_uv(&msg_br);
|
|
while (tmp_encoded_len > 0) {
|
|
u8 *tmp_encoded_bytes = br_read_bytes_raw(&msg_br, tmp_encoded_len);
|
|
if (!tmp_encoded_bytes) break;
|
|
|
|
struct bitbuff decoder_bb = bitbuff_from_string(STRING(tmp_encoded_len, tmp_encoded_bytes));
|
|
struct bitbuff_reader decoder_br = br_from_bitbuff(&decoder_bb);
|
|
u64 base_tick = br_read_uv(&decoder_br);
|
|
u64 tick = br_read_uv(&decoder_br);
|
|
|
|
struct string tmp_encoded = ZI;
|
|
tmp_encoded.len = br_num_bytes_left(&decoder_br);
|
|
tmp_encoded.text = br_read_bytes_raw(&decoder_br, tmp_encoded.len);
|
|
if (!tmp_encoded.text) tmp_encoded.len = 0;
|
|
|
|
struct sim_snapshot *base_ss = sim_snapshot_from_tick(client, base_tick);
|
|
if (base_ss->tick == base_tick) {
|
|
if (is_master) {
|
|
/* Queue incoming slave client snapshot for decoding */
|
|
//b32 should_decode = tick == client->highest_received_tick + 1 || client->highest_received_tick == 0;
|
|
b32 should_decode = tick > client->highest_received_tick;
|
|
if (should_decode) {
|
|
struct sim_ss_decode_node *node = arena_push(scratch.arena, struct sim_ss_decode_node);
|
|
node->client = client;
|
|
node->tick = tick;
|
|
node->base_tick = base_tick;
|
|
node->tmp_encoded = tmp_encoded;
|
|
if (queue.last) {
|
|
queue.last->next = node;
|
|
} else {
|
|
queue.first = node;
|
|
}
|
|
queue.last = node;
|
|
if (tick > client->highest_received_tick) {
|
|
client->highest_received_tick = tick;
|
|
}
|
|
}
|
|
} else {
|
|
/* Decode incoming master client snapshots for decoding (only the newest one) */
|
|
b32 should_decode = client == master_client && tick > client->highest_received_tick;
|
|
if (should_decode) {
|
|
struct sim_ss_decode_node *node = queue.first ? queue.first : arena_push(scratch.arena, struct sim_ss_decode_node);
|
|
node->client = client;
|
|
node->tick = tick;
|
|
node->base_tick = base_tick;
|
|
node->tmp_encoded = tmp_encoded;
|
|
queue.first = node;
|
|
queue.last = node;
|
|
if (tick > client->highest_received_tick) {
|
|
client->highest_received_tick = tick;
|
|
if (average_master_receive_dt_ns == 0) {
|
|
average_master_receive_dt_ns = NS_FROM_SECONDS(1) / SIM_TICKS_PER_SECOND;
|
|
} else {
|
|
average_master_receive_dt_ns -= average_master_receive_dt_ns / 50;
|
|
average_master_receive_dt_ns += (real_time_ns - last_tick_from_master_received_at_ns) / 50;
|
|
}
|
|
last_tick_from_master_received_at_ns = real_time_ns;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/* We do not have the tick that the incoming delta is based from */
|
|
ASSERT(false);
|
|
}
|
|
|
|
tmp_encoded_len = br_read_uv(&msg_br);
|
|
}
|
|
}
|
|
} break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Decode incoming snapshots */
|
|
for (struct sim_ss_decode_node *n = queue.first; n; n = n->next) {
|
|
struct sim_client *client = n->client;
|
|
u64 base_tick = n->base_tick;
|
|
u64 tick = n->tick;
|
|
struct sim_snapshot *base_ss = sim_snapshot_from_tick(client, base_tick);
|
|
if (base_ss->tick == base_tick) {
|
|
struct bitbuff bb = bitbuff_from_string(n->tmp_encoded);
|
|
struct bitbuff_reader br = br_from_bitbuff(&bb);
|
|
|
|
/* Alloc & decode snapshot */
|
|
struct sim_snapshot *ss = sim_snapshot_alloc(client, base_ss, tick);
|
|
sim_snapshot_decode(&br, ss);
|
|
|
|
/* Assume all incoming ents want to be sync srcs */
|
|
for (u64 i = 0; i < ss->num_ents_reserved; ++i) {
|
|
struct sim_ent *ent = &ss->ents[i];
|
|
if (ent->valid && sim_ent_has_prop(ent, SEPROP_SYNC_DST)) {
|
|
sim_ent_disable_prop(ent, SEPROP_SYNC_DST);
|
|
sim_ent_enable_prop(ent, SEPROP_SYNC_SRC);
|
|
}
|
|
}
|
|
} else {
|
|
/* We do not have the tick that the incoming delta is based from.
|
|
* This decode should never have been queued in the first place. */
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
|
|
if (!is_master && !initialized_from_master) {
|
|
if (master_client->valid && master_client->last_tick > 0) {
|
|
initialized_from_master = true;
|
|
} else {
|
|
goto skip_step;
|
|
}
|
|
}
|
|
|
|
b32 should_step = !atomic_i32_eval(&G.user_paused);
|
|
if (atomic_i32_eval(&G.user_paused_steps) > 0) {
|
|
should_step = true;
|
|
atomic_i32_eval_add(&G.user_paused_steps, -1);
|
|
}
|
|
|
|
if (!should_step) {
|
|
goto skip_step;
|
|
}
|
|
|
|
/* Update networked clients */
|
|
u64 oldest_client_ack = 0;
|
|
for (u64 i = 0; i < store->num_clients_reserved; ++i) {
|
|
struct sim_client *client = &store->clients[i];
|
|
if (client->valid && client != local_client && client != publish_client && client != user_input_client && client != master_client) {
|
|
client->last_rtt_ns = host_get_channel_last_rtt_ns(host, client->channel_id);
|
|
/* Release unneeded received snapshots */
|
|
/* TDOO: Cap how many client snapshots we're willing to retain */
|
|
if (client->double_ack > 0) {
|
|
u64 keep_tick = min_u64(client->double_ack, local_client->last_tick);
|
|
if (keep_tick > 0) {
|
|
sim_snapshot_release_ticks_in_range(client, 0, keep_tick - 1);
|
|
}
|
|
}
|
|
if (client->ack < oldest_client_ack || oldest_client_ack == 0) {
|
|
oldest_client_ack = client->ack;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Release unneeded published snapshots */
|
|
{
|
|
u64 keep_tick = oldest_client_ack;
|
|
if (keep_tick == 0 && publish_client->last_tick > 0) {
|
|
keep_tick = publish_client->last_tick - 1;
|
|
}
|
|
if (keep_tick > 0) {
|
|
--keep_tick;
|
|
}
|
|
sim_snapshot_release_ticks_in_range(publish_client, 0, keep_tick);
|
|
}
|
|
|
|
/* Release old local snapshots */
|
|
{
|
|
u64 keep_range = 50;
|
|
if (local_client->last_tick > keep_range) {
|
|
u64 keep_tick = local_client->last_tick - keep_range;
|
|
sim_snapshot_release_ticks_in_range(local_client, 0, keep_tick);
|
|
}
|
|
}
|
|
|
|
/* Release unneeded user input snapshots */
|
|
sim_snapshot_release_ticks_in_range(user_input_client, 0, local_client->first_tick - 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (is_master) {
|
|
/* Step master */
|
|
u64 prev_tick = local_client->last_tick;
|
|
u64 next_tick = prev_tick + 1;
|
|
struct sim_step_ctx ctx = ZI;
|
|
ctx.is_master = is_master;
|
|
ctx.sim_dt_ns = step_dt_ns;
|
|
ctx.accel = &accel;
|
|
ctx.user_input_client = user_input_client;
|
|
ctx.master_client = master_client;
|
|
ctx.publish_client = publish_client;
|
|
struct sim_snapshot *prev_world = sim_snapshot_from_tick(local_client, prev_tick);
|
|
ctx.world = sim_snapshot_alloc(local_client, prev_world, next_tick);
|
|
generate_user_input_cmds(user_input_client, next_tick);
|
|
sim_step(&ctx);
|
|
} else if (master_client->valid) {
|
|
/* Step client */
|
|
|
|
/* TODO: Eventually determine master tick based on a delay to allow for jitter and also interpolation so we can lower snapshot publish frequency */
|
|
|
|
|
|
b32 master_ss_is_blended = false;
|
|
struct sim_snapshot *master_ss = sim_snapshot_nil();
|
|
{
|
|
/* How along are we between master sim ticks (0 = start of tick, 1 = end of tick) */
|
|
f64 tick_progress = 0;
|
|
i64 next_tick_expected_ns = last_tick_from_master_received_at_ns + average_master_receive_dt_ns;
|
|
if (next_tick_expected_ns > last_tick_from_master_received_at_ns) {
|
|
tick_progress = (f64)(real_time_ns - last_tick_from_master_received_at_ns) / (f64)(next_tick_expected_ns - last_tick_from_master_received_at_ns);
|
|
}
|
|
|
|
/* Predict master sim time based on average snapshot publish dt. */
|
|
struct sim_snapshot *newest_snapshot = sim_snapshot_from_tick(master_client, master_client->last_tick);
|
|
i64 master_sim_predicted_time_ns = newest_snapshot->sim_time_ns + (newest_snapshot->sim_dt_ns * tick_progress);
|
|
|
|
/* Determine blend time */
|
|
i64 master_blend_time_target_ns = master_sim_predicted_time_ns - (SIM_CLIENT_INTERP_RATIO * average_master_receive_dt_ns);
|
|
if (average_master_receive_dt_ns > 0) {
|
|
master_blend_time_ns += real_dt_ns;
|
|
}
|
|
|
|
i64 blend_time_target_diff_ns = master_blend_time_target_ns - master_blend_time_ns;
|
|
if (blend_time_target_diff_ns > NS_FROM_SECONDS(0.100) || blend_time_target_diff_ns < NS_FROM_SECONDS(-0.100)) {
|
|
/* Snap blend time if it gets too far from target blend time */
|
|
master_blend_time_ns = master_blend_time_target_ns;
|
|
}
|
|
u64 master_blend_tick = master_blend_time_ns / newest_snapshot->sim_dt_ns;
|
|
|
|
/* Get snapshot nearest to master blend time */
|
|
/* TODO: Blend */
|
|
struct sim_snapshot *left_snapshot = sim_snapshot_nil();
|
|
struct sim_snapshot *right_snapshot = newest_snapshot;
|
|
{
|
|
struct sim_snapshot *ss = sim_snapshot_from_tick(master_client, master_client->first_tick);
|
|
while (ss->valid) {
|
|
u64 next_tick = ss->next_tick;
|
|
i64 ss_time_ns = ss->sim_time_ns;
|
|
if (ss_time_ns < master_blend_time_ns && ss_time_ns > left_snapshot->sim_time_ns) {
|
|
left_snapshot = ss;
|
|
}
|
|
if (ss_time_ns > master_blend_time_ns && ss_time_ns < right_snapshot->sim_time_ns) {
|
|
right_snapshot = ss;
|
|
}
|
|
ss = sim_snapshot_from_tick(master_client, next_tick);
|
|
}
|
|
}
|
|
|
|
/* Create world from blended master snapshots */
|
|
f64 blend = 0;
|
|
if (left_snapshot->valid && right_snapshot->valid && right_snapshot->tick > left_snapshot->tick) {
|
|
blend = (f64)(master_blend_tick - left_snapshot->tick) / (f64)(right_snapshot->tick - left_snapshot->tick);
|
|
f64 epsilon = 0.001;
|
|
if (blend < epsilon) {
|
|
master_ss_is_blended = false;
|
|
master_ss = left_snapshot;
|
|
} else if (blend > 1 - epsilon) {
|
|
master_ss_is_blended = false;
|
|
master_ss = right_snapshot;
|
|
} else {
|
|
master_ss_is_blended = true;
|
|
master_ss = sim_snapshot_alloc_from_lerp(master_blended_client, left_snapshot, right_snapshot, blend);
|
|
|
|
/* Release unneeded blended master snapshots */
|
|
if (master_ss->tick > 0) {
|
|
sim_snapshot_release_ticks_in_range(master_blended_client, 0, master_ss->tick - 1);
|
|
sim_snapshot_release_ticks_in_range(master_blended_client, master_ss->tick + 1, U64_MAX);
|
|
}
|
|
}
|
|
} else {
|
|
master_ss_is_blended = false;
|
|
master_ss = left_snapshot->valid ? left_snapshot : right_snapshot;
|
|
}
|
|
|
|
/* Release unneeded master snapshots */
|
|
u64 keep_master_tick = min_u64(left_snapshot->tick, master_client->double_ack);
|
|
if (keep_master_tick > 0) {
|
|
sim_snapshot_release_ticks_in_range(master_client, 0, keep_master_tick - 1);
|
|
}
|
|
|
|
#if 0
|
|
DEBUGBREAKABLE;
|
|
logf_debug("*************************************************");
|
|
logf_debug("local_client->last_tick: %F", FMT_UINT(local_client->last_tick));
|
|
logf_debug("master_sim_predicted_time_ns: %F", FMT_SINT(master_sim_predicted_time_ns));
|
|
logf_debug("tick_progress: %F", FMT_FLOAT(tick_progress));
|
|
logf_debug("sim_publish_timescale: %F", FMT_FLOAT(sim_publish_timescale));
|
|
logf_debug("last_tick_from_master_received_at_ns: %F", FMT_SINT(last_tick_from_master_received_at_ns));
|
|
logf_debug("average_master_receive_dt_ns: %F", FMT_SINT(average_master_receive_dt_ns));
|
|
logf_debug("next_tick_expected_ns: %F", FMT_SINT(next_tick_expected_ns));
|
|
logf_debug("master_blend_time_target_ns: %F", FMT_SINT(master_blend_time_target_ns));
|
|
logf_debug("blend_time_target_diff_ns: %F", FMT_SINT(blend_time_target_diff_ns));
|
|
logf_debug("master_blend_time_ns: %F", FMT_SINT(master_blend_time_ns));
|
|
logf_debug("left_snapshot->tick: %F", FMT_UINT(left_snapshot->tick));
|
|
logf_debug("right_snapshot->tick: %F", FMT_UINT(right_snapshot->tick));
|
|
logf_debug("master_ss->tick: %F", FMT_UINT(master_ss->tick));
|
|
#endif
|
|
}
|
|
|
|
if (master_ss->valid) {
|
|
struct sim_ent *master_player = sim_ent_find_first_match_one(master_ss, SEPROP_PLAYER_IS_MASTER);
|
|
|
|
/* Update ent id from master */
|
|
{
|
|
user_input_client->player_id = master_ss->local_player;
|
|
local_client->player_id = master_ss->local_player;
|
|
}
|
|
|
|
/* Check for misprediction */
|
|
u64 mispredicted_tick = 0;
|
|
if (!master_ss_is_blended) {
|
|
/* TODO: Actually check for misprediction rather than triggering mispredict any time a new master snapshot is received */
|
|
mispredicted_tick = master_ss->tick;
|
|
}
|
|
|
|
|
|
u64 step_base_tick = local_client->last_tick;
|
|
u64 step_end_tick = step_base_tick + 1;
|
|
if (mispredicted_tick > 0) {
|
|
step_base_tick = mispredicted_tick;
|
|
if (step_end_tick <= step_base_tick) {
|
|
step_end_tick = step_base_tick + 1;
|
|
}
|
|
}
|
|
|
|
/* We want to simulate the ahead of the server to predict client input.
|
|
* How many ticks ahead we want to simulate is a balance between added latency and the server not receiving our inputs on time.
|
|
* We can take the server's ack minus the server's tick to determine how many cmds of ours the server has buffered.
|
|
*
|
|
* If this buffer gets too low (because we are lagging behind or the connection is unstable), meaning the server is not getting our input on time:
|
|
* - Shorten local compute rate to increase the rate at which we predict ahead & produce cmds, until the server's ack indicates a buffer size within desired range.
|
|
*
|
|
* If this buffer gets too large (because the client predicts too far ahead), meaning unneeded latency is being introduced:
|
|
* - Dilate local compute rate to decrease the rate at which we predict ahead & produce cmds until the server's ack indicates a buffer size within desired range.
|
|
*/
|
|
{
|
|
i64 cmds_ahead_on_master = (i64)master_client->ack - (i64)master_client->last_tick;
|
|
if (cmds_ahead_on_master < -3 || cmds_ahead_on_master > 10) {
|
|
/* Cmds are too far from master time, snap step end tick */
|
|
i64 rtt_ns = master_client->last_rtt_ns;
|
|
f64 rtt_tick_ratio = (f64)(rtt_ns + (step_dt_ns - 1)) / (f64)step_dt_ns;
|
|
i64 num_predict_ticks = math_round_to_int64(rtt_tick_ratio) + 5;
|
|
step_end_tick = master_client->last_tick + num_predict_ticks;
|
|
compute_timescale = 1.1;
|
|
} else if (cmds_ahead_on_master > 2) {
|
|
/* Slow down simulation to dial back how far ahead we are predicting and bring local sim time closer to master sim time */
|
|
compute_timescale = 1.1;
|
|
} else if (cmds_ahead_on_master < 1) {
|
|
/* Speed up simulation rate predict more ticks and give master more inputs to work with */
|
|
compute_timescale = 0.9;
|
|
} else {
|
|
/* Server's cmd buffer is in a healthy range */
|
|
compute_timescale = 1;
|
|
}
|
|
}
|
|
|
|
/* Sync master with local base tick */
|
|
struct sim_snapshot *base_ss = sim_snapshot_from_tick(local_client, step_base_tick);
|
|
if (mispredicted_tick) {
|
|
if (base_ss->valid) {
|
|
sim_snapshot_sync_ents(base_ss, master_ss, master_player->id, 0);
|
|
} else {
|
|
base_ss = sim_snapshot_alloc(local_client, master_ss, step_base_tick);
|
|
}
|
|
}
|
|
|
|
/* Release any existing ticks that are about to be simulated */
|
|
sim_snapshot_release_ticks_in_range(local_client, step_base_tick + 1, U64_MAX);
|
|
|
|
/* Step */
|
|
generate_user_input_cmds(user_input_client, step_end_tick);
|
|
{
|
|
struct sim_step_ctx ctx = ZI;
|
|
ctx.is_master = is_master;
|
|
ctx.sim_dt_ns = step_dt_ns;
|
|
ctx.accel = &accel;
|
|
ctx.user_input_client = user_input_client;
|
|
ctx.master_client = master_client;
|
|
ctx.publish_client = publish_client;
|
|
|
|
u64 step_tick = step_base_tick + 1;
|
|
struct sim_snapshot *prev_ss = base_ss;
|
|
while (step_tick <= step_end_tick) {
|
|
ctx.world = sim_snapshot_alloc(local_client, prev_ss, step_tick);
|
|
if (!mispredicted_tick && step_tick == step_end_tick) {
|
|
sim_snapshot_sync_ents(ctx.world, master_ss, master_player->id, SIM_SYNC_FLAG_NOSYNC_PREDICTABLES);
|
|
}
|
|
sim_step(&ctx);
|
|
prev_ss = ctx.world;
|
|
++step_tick;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Publish snapshot to remote clients */
|
|
for (u64 i = 0; i < store->num_clients_reserved; ++i) {
|
|
struct sim_client *client = &store->clients[i];
|
|
if (client->valid && client != user_input_client && client != local_client && client != publish_client) {
|
|
struct bitbuff_writer msg_bw = bw_from_bitbuff(&msg_writer_bb);
|
|
|
|
bw_write_uv(&msg_bw, client->highest_received_tick); /* ack */
|
|
bw_write_uv(&msg_bw, client->ack); /* double ack */
|
|
|
|
struct sim_snapshot *base_ss = sim_snapshot_from_tick(publish_client, client->ack);
|
|
struct sim_snapshot *publish_ss;
|
|
if (client == master_client) {
|
|
/* If sending to master, start sending all snapshots since last ack */
|
|
publish_ss = sim_snapshot_from_closest_tick_gte(publish_client, base_ss->tick + 1);
|
|
} else {
|
|
/* If sending to slave, only send latest snapshot */
|
|
publish_ss = sim_snapshot_from_tick(publish_client, publish_client->last_tick);
|
|
}
|
|
|
|
while (publish_ss->valid) {
|
|
struct bitbuff_writer snapshot_bw = bw_from_bitbuff(&snapshot_writer_bb);
|
|
struct string tmp_snapshot_encoded = ZI;
|
|
{
|
|
bw_write_uv(&snapshot_bw, base_ss->tick);
|
|
bw_write_uv(&snapshot_bw, publish_ss->tick);
|
|
sim_snapshot_encode(&snapshot_bw, client, base_ss, publish_ss);
|
|
tmp_snapshot_encoded.len = bw_num_bytes_written(&snapshot_bw);
|
|
tmp_snapshot_encoded.text = bw_get_written_raw(&snapshot_bw);
|
|
}
|
|
bw_write_uv(&msg_bw, tmp_snapshot_encoded.len);
|
|
bw_write_bytes(&msg_bw, tmp_snapshot_encoded);
|
|
publish_ss = sim_snapshot_from_tick(publish_client, publish_ss->tick + 1);
|
|
}
|
|
bw_write_uv(&msg_bw, 0);
|
|
|
|
struct string encoded = ZI;
|
|
encoded.len = bw_num_bytes_written(&msg_bw);
|
|
encoded.text = bw_get_written_raw(&msg_bw);
|
|
host_queue_write(host, client->channel_id, encoded, 0);
|
|
}
|
|
}
|
|
|
|
/* Copy local snapshot to user client */
|
|
{
|
|
struct sim_snapshot *local_ss = sim_snapshot_from_tick(local_client, local_client->last_tick);
|
|
if (local_ss->valid) {
|
|
/* TODO: Double buffer */
|
|
struct sys_lock lock = sys_mutex_lock_e(G.local_to_user_client_mutex);
|
|
sim_snapshot_alloc(G.local_to_user_client, local_ss, local_ss->tick);
|
|
i64 publish_ns = sys_time_ns();
|
|
G.local_to_user_client_publish_dt_ns = publish_ns - last_publish_to_user_ns;
|
|
G.local_to_user_client_publish_time_ns = publish_ns;
|
|
last_publish_to_user_ns = publish_ns;
|
|
sim_snapshot_release_ticks_in_range(G.local_to_user_client, 0, local_ss->tick - 1);
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
}
|
|
|
|
skip_step:
|
|
|
|
/* Send host messages */
|
|
host_update_end(host);
|
|
__profframe("Local sim");
|
|
|
|
scratch_end(scratch);
|
|
}
|
|
|
|
sim_client_store_release(store);
|
|
sim_accel_release(&accel);
|
|
bitbuff_release(&snapshot_writer_bb);
|
|
bitbuff_release(&msg_writer_bb);
|
|
host_release(host);
|
|
}
|