#include "pxl8_world.h" #include #include "pxl8_hal.h" #ifdef PXL8_ASYNC_THREADS #include #include "pxl8_queue.h" #endif #include "pxl8_bsp_render.h" #include "pxl8_io.h" #include "pxl8_gfx3d.h" #include "pxl8_log.h" #include "pxl8_mem.h" #include "pxl8_sim.h" #define PXL8_WORLD_ENTITY_CAPACITY 256 struct pxl8_world { pxl8_world_chunk* active_chunk; pxl8_world_chunk_cache* chunk_cache; pxl8_entity_pool* entities; pxl8_bsp_render_state* bsp_render_state; pxl8_sim_entity local_player; u64 client_tick; pxl8_vec2 pointer_motion; pxl8_sim_config sim_config; #ifdef PXL8_ASYNC_THREADS pxl8_sim_entity render_state[2]; atomic_uint active_buffer; pxl8_thread* sim_thread; atomic_bool sim_running; atomic_bool sim_paused; pxl8_net* net; pxl8_queue input_queue; f32 sim_accumulator; #endif }; pxl8_world* pxl8_world_create(void) { pxl8_world* world = pxl8_calloc(1, sizeof(pxl8_world)); if (!world) return NULL; world->chunk_cache = pxl8_world_chunk_cache_create(); world->entities = pxl8_entity_pool_create(PXL8_WORLD_ENTITY_CAPACITY); world->sim_config = (pxl8_sim_config){ .move_speed = 180.0f, .ground_accel = 10.0f, .air_accel = 1.0f, .stop_speed = 100.0f, .friction = 6.0f, .gravity = 800.0f, .jump_velocity = 200.0f, .player_radius = 16.0f, .player_height = 72.0f, .max_pitch = 1.5f, }; if (!world->chunk_cache || !world->entities) { pxl8_world_destroy(world); return NULL; } return world; } void pxl8_world_destroy(pxl8_world* world) { if (!world) return; pxl8_world_chunk_cache_destroy(world->chunk_cache); pxl8_entity_pool_destroy(world->entities); pxl8_bsp_render_state_destroy(world->bsp_render_state); pxl8_free(world); } pxl8_world_chunk_cache* pxl8_world_get_chunk_cache(pxl8_world* world) { if (!world) return NULL; return world->chunk_cache; } pxl8_world_chunk* pxl8_world_active_chunk(pxl8_world* world) { if (!world) return NULL; return world->active_chunk; } void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_world_chunk* chunk) { if (!world) return; world->active_chunk = chunk; } pxl8_entity_pool* pxl8_world_entities(pxl8_world* world) { if (!world) return NULL; return world->entities; } pxl8_entity pxl8_world_spawn(pxl8_world* world) { if (!world || !world->entities) return PXL8_ENTITY_INVALID; return pxl8_entity_spawn(world->entities); } pxl8_sim_world pxl8_world_sim_world(const pxl8_world* world, pxl8_vec3 pos) { (void)pos; pxl8_sim_world sim = {0}; if (world->active_chunk && world->active_chunk->bsp) { sim.bsp = world->active_chunk->bsp; } return sim; } static void entity_to_userdata(const pxl8_sim_entity* ent, u8* userdata) { u8* p = userdata; memcpy(p, &ent->pos.x, 4); p += 4; memcpy(p, &ent->pos.y, 4); p += 4; memcpy(p, &ent->pos.z, 4); p += 4; memcpy(p, &ent->yaw, 4); p += 4; memcpy(p, &ent->pitch, 4); p += 4; memcpy(p, &ent->vel.x, 4); p += 4; memcpy(p, &ent->vel.y, 4); p += 4; memcpy(p, &ent->vel.z, 4); p += 4; memcpy(p, &ent->flags, 4); p += 4; memcpy(p, &ent->kind, 2); } static void userdata_to_entity(const u8* userdata, pxl8_sim_entity* ent) { const u8* p = userdata; memcpy(&ent->pos.x, p, 4); p += 4; memcpy(&ent->pos.y, p, 4); p += 4; memcpy(&ent->pos.z, p, 4); p += 4; memcpy(&ent->yaw, p, 4); p += 4; memcpy(&ent->pitch, p, 4); p += 4; memcpy(&ent->vel.x, p, 4); p += 4; memcpy(&ent->vel.y, p, 4); p += 4; memcpy(&ent->vel.z, p, 4); p += 4; memcpy(&ent->flags, p, 4); p += 4; memcpy(&ent->kind, p, 2); } bool pxl8_world_point_solid(const pxl8_world* world, f32 x, f32 y, f32 z) { if (!world) return false; if (world->active_chunk && world->active_chunk->bsp) { return pxl8_bsp_point_solid(world->active_chunk->bsp, (pxl8_vec3){x, y, z}); } return false; } pxl8_ray pxl8_world_ray(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to) { pxl8_ray result = {0}; if (!world) return result; pxl8_vec3 dir = pxl8_vec3_sub(to, from); f32 length = pxl8_vec3_length(dir); if (length < 0.001f) return result; f32 step_size = 1.0f; f32 traveled = 0.0f; while (traveled < length) { f32 t = traveled / length; pxl8_vec3 pos = { from.x + dir.x * t, from.y + dir.y * t, from.z + dir.z * t }; if (pxl8_world_point_solid(world, pos.x, pos.y, pos.z)) { result.hit = true; result.fraction = t; result.point = pos; f32 eps = 0.1f; bool sx_neg = pxl8_world_point_solid(world, pos.x - eps, pos.y, pos.z); bool sx_pos = pxl8_world_point_solid(world, pos.x + eps, pos.y, pos.z); bool sy_neg = pxl8_world_point_solid(world, pos.x, pos.y - eps, pos.z); bool sy_pos = pxl8_world_point_solid(world, pos.x, pos.y + eps, pos.z); bool sz_neg = pxl8_world_point_solid(world, pos.x, pos.y, pos.z - eps); bool sz_pos = pxl8_world_point_solid(world, pos.x, pos.y, pos.z + eps); result.normal = (pxl8_vec3){ (sx_neg && !sx_pos) ? 1.0f : (!sx_neg && sx_pos) ? -1.0f : 0.0f, (sy_neg && !sy_pos) ? 1.0f : (!sy_neg && sy_pos) ? -1.0f : 0.0f, (sz_neg && !sz_pos) ? 1.0f : (!sz_neg && sz_pos) ? -1.0f : 0.0f }; f32 nl = pxl8_vec3_length(result.normal); if (nl > 0.001f) { result.normal = pxl8_vec3_scale(result.normal, 1.0f / nl); } else { result.normal = (pxl8_vec3){0, 1, 0}; } return result; } traveled += step_size; } return result; } pxl8_ray pxl8_world_sweep(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) { pxl8_ray result = {0}; if (!world) return result; f32 diag = radius * 0.707f; bool dest_blocked = pxl8_world_point_solid(world, to.x, to.y, to.z) || pxl8_world_point_solid(world, to.x + radius, to.y, to.z) || pxl8_world_point_solid(world, to.x - radius, to.y, to.z) || pxl8_world_point_solid(world, to.x, to.y, to.z + radius) || pxl8_world_point_solid(world, to.x, to.y, to.z - radius) || pxl8_world_point_solid(world, to.x + diag, to.y, to.z + diag) || pxl8_world_point_solid(world, to.x + diag, to.y, to.z - diag) || pxl8_world_point_solid(world, to.x - diag, to.y, to.z + diag) || pxl8_world_point_solid(world, to.x - diag, to.y, to.z - diag); if (dest_blocked) { result.hit = true; result.fraction = 0; result.point = from; pxl8_vec3 dir = pxl8_vec3_sub(to, from); f32 length = pxl8_vec3_length(dir); if (length > 0.001f) { result.normal = pxl8_vec3_scale(dir, -1.0f / length); } else { result.normal = (pxl8_vec3){0, 1, 0}; } } return result; } void pxl8_world_update(pxl8_world* world, f32 dt) { (void)dt; if (!world) return; pxl8_world_chunk_cache_tick(world->chunk_cache); } void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) { if (!world || !gfx) return; if (world->active_chunk && world->active_chunk->bsp) { pxl8_3d_set_bsp(gfx, world->active_chunk->bsp); pxl8_bsp_render(gfx, world->active_chunk->bsp, world->bsp_render_state, camera_pos); } else { pxl8_3d_set_bsp(gfx, NULL); } } void pxl8_world_sync(pxl8_world* world, pxl8_net* net) { if (!world || !net) return; u32 chunk_id = pxl8_net_chunk_id(net); if (chunk_id != 0) { pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_bsp(world->chunk_cache, chunk_id); if (chunk && chunk->bsp) { if (world->active_chunk != chunk) { world->active_chunk = chunk; pxl8_debug("[CLIENT] Synced BSP chunk id=%u as active (verts=%u faces=%u)", chunk_id, chunk->bsp->num_vertices, chunk->bsp->num_faces); if (world->bsp_render_state) { pxl8_bsp_render_state_destroy(world->bsp_render_state); world->bsp_render_state = NULL; } world->bsp_render_state = pxl8_bsp_render_state_create(chunk->bsp->num_faces); } } } else if (world->active_chunk != NULL) { world->active_chunk = NULL; if (world->bsp_render_state) { pxl8_bsp_render_state_destroy(world->bsp_render_state); world->bsp_render_state = NULL; } } } static void ensure_bsp_render_state(pxl8_world* world) { if (!world || world->bsp_render_state) return; if (!world->active_chunk) return; if (!world->active_chunk->bsp) return; world->bsp_render_state = pxl8_bsp_render_state_create(world->active_chunk->bsp->num_faces); } void pxl8_world_set_bsp_material(pxl8_world* world, u16 material_id, const pxl8_gfx_material* material) { if (!world || !material) return; ensure_bsp_render_state(world); if (!world->bsp_render_state) return; pxl8_bsp_set_material(world->bsp_render_state, material_id, material); } void pxl8_world_set_sim_config(pxl8_world* world, const pxl8_sim_config* config) { if (!world || !config) return; world->sim_config = *config; } void pxl8_world_init_local_player(pxl8_world* world, f32 x, f32 y, f32 z) { if (!world) return; world->local_player.pos = (pxl8_vec3){x, y, z}; world->local_player.vel = (pxl8_vec3){0, 0, 0}; world->local_player.yaw = 0; world->local_player.pitch = 0; world->local_player.flags = PXL8_SIM_FLAG_ALIVE | PXL8_SIM_FLAG_PLAYER | PXL8_SIM_FLAG_GROUNDED; world->local_player.kind = 0; world->client_tick = 0; world->pointer_motion = (pxl8_vec2){0}; #ifdef PXL8_ASYNC_THREADS world->render_state[0] = world->local_player; world->render_state[1] = world->local_player; #endif } void pxl8_world_set_look(pxl8_world* world, f32 yaw, f32 pitch) { if (!world) return; world->local_player.yaw = yaw; world->local_player.pitch = pitch; world->pointer_motion = (pxl8_vec2){.yaw = yaw, .pitch = pitch}; #ifdef PXL8_ASYNC_THREADS world->render_state[0].yaw = yaw; world->render_state[0].pitch = pitch; world->render_state[1].yaw = yaw; world->render_state[1].pitch = pitch; #endif } pxl8_sim_entity* pxl8_world_local_player(pxl8_world* world) { if (!world) return NULL; #ifdef PXL8_ASYNC_THREADS const pxl8_sim_entity* state = pxl8_world_get_render_state(world); if (!state) return NULL; if (!(state->flags & PXL8_SIM_FLAG_ALIVE)) return NULL; return (pxl8_sim_entity*)state; #else if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return NULL; return &world->local_player; #endif } void pxl8_world_predict(pxl8_world* world, pxl8_net* net, const pxl8_input_msg* input, f32 dt) { if (!world || !net || !input) return; if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return; pxl8_sim_world sim = pxl8_world_sim_world(world, world->local_player.pos); pxl8_sim_move_player(&world->local_player, input, &sim, &world->sim_config, dt); world->client_tick++; entity_to_userdata(&world->local_player, pxl8_net_predicted_state(net)); pxl8_net_predicted_tick_set(net, world->client_tick); } void pxl8_world_reconcile(pxl8_world* world, pxl8_net* net, f32 dt) { if (!world || !net) return; if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return; if (!pxl8_net_needs_correction(net)) return; u64 player_id = pxl8_net_player_id(net); const u8* server_state = pxl8_net_entity_userdata(net, player_id); if (!server_state) return; pxl8_sim_entity server_player = {0}; userdata_to_entity(server_state, &server_player); if (!(server_player.flags & PXL8_SIM_FLAG_ALIVE)) { return; } world->local_player = server_player; const pxl8_snapshot_header* snap = pxl8_net_snapshot(net); u64 server_tick = snap ? snap->tick : 0; for (u64 tick = server_tick + 1; tick <= world->client_tick; tick++) { const pxl8_input_msg* input = pxl8_net_input_at(net, tick); if (!input) continue; pxl8_sim_world sim = pxl8_world_sim_world(world, world->local_player.pos); pxl8_sim_move_player(&world->local_player, input, &sim, &world->sim_config, dt); } entity_to_userdata(&world->local_player, pxl8_net_predicted_state(net)); } #ifdef PXL8_ASYNC_THREADS #define SIM_TIMESTEP (1.0f / 60.0f) static void pxl8_world_sim_tick(pxl8_world* world, f32 dt) { bool alive = (world->local_player.flags & PXL8_SIM_FLAG_ALIVE) != 0; pxl8_input_msg merged = {0}; pxl8_input_msg* input = NULL; while ((input = pxl8_queue_pop(&world->input_queue))) { merged.look_dx += input->look_dx; merged.look_dy += input->look_dy; merged.move_x = input->move_x; merged.move_y = input->move_y; merged.buttons |= input->buttons; pxl8_free(input); } if (!alive) { return; } const f32 MAX_LOOK_DELTA = 100.0f; if (merged.look_dx > MAX_LOOK_DELTA) merged.look_dx = MAX_LOOK_DELTA; if (merged.look_dx < -MAX_LOOK_DELTA) merged.look_dx = -MAX_LOOK_DELTA; if (merged.look_dy > MAX_LOOK_DELTA) merged.look_dy = MAX_LOOK_DELTA; if (merged.look_dy < -MAX_LOOK_DELTA) merged.look_dy = -MAX_LOOK_DELTA; merged.tick = world->client_tick; merged.timestamp = pxl8_get_ticks_ns(); merged.yaw = world->pointer_motion.yaw; if (world->net) { pxl8_net_send_input(world->net, &merged); } world->local_player.yaw = world->pointer_motion.yaw; world->local_player.pitch = world->pointer_motion.pitch; merged.look_dx = 0; merged.look_dy = 0; pxl8_sim_world sim = pxl8_world_sim_world(world, world->local_player.pos); pxl8_sim_move_player(&world->local_player, &merged, &sim, &world->sim_config, dt); if (world->net) { entity_to_userdata(&world->local_player, pxl8_net_predicted_state(world->net)); pxl8_net_predicted_tick_set(world->net, world->client_tick); } world->client_tick++; } static void pxl8_world_swap_buffers(pxl8_world* world) { u32 back = atomic_load(&world->active_buffer) ^ 1; world->render_state[back] = world->local_player; atomic_store(&world->active_buffer, back); } static int pxl8_world_sim_thread(void* data) { pxl8_world* world = (pxl8_world*)data; u64 last_time = pxl8_get_ticks_ns(); while (atomic_load(&world->sim_running)) { if (atomic_load(&world->sim_paused)) { last_time = pxl8_get_ticks_ns(); pxl8_sleep_ms(1); continue; } u64 now = pxl8_get_ticks_ns(); f32 dt = (f32)(now - last_time) / 1e9f; last_time = now; if (dt > 0.1f) dt = 0.1f; if (dt < 0.0001f) dt = 0.0001f; world->sim_accumulator += dt; while (world->sim_accumulator >= SIM_TIMESTEP) { pxl8_world_chunk_cache_tick(world->chunk_cache); if (world->net) { pxl8_packet* pkt; while ((pkt = pxl8_net_pop_packet(world->net))) { pxl8_net_process_packet(world->net, pkt); pxl8_net_packet_free(pkt); } pxl8_world_sync(world, world->net); pxl8_world_reconcile(world, world->net, SIM_TIMESTEP); } pxl8_world_sim_tick(world, SIM_TIMESTEP); world->sim_accumulator -= SIM_TIMESTEP; } pxl8_world_swap_buffers(world); pxl8_sleep_ms(1); } return 0; } void pxl8_world_start_sim_thread(pxl8_world* world, pxl8_net* net) { if (!world || world->sim_thread) return; world->net = net; pxl8_queue_init(&world->input_queue); atomic_store(&world->active_buffer, 0); atomic_store(&world->sim_running, true); world->sim_accumulator = 0.0f; world->render_state[0] = world->local_player; world->render_state[1] = world->local_player; world->sim_thread = pxl8_thread_create(pxl8_world_sim_thread, "pxl8_sim", world); } void pxl8_world_stop_sim_thread(pxl8_world* world) { if (!world || !world->sim_thread) return; atomic_store(&world->sim_running, false); pxl8_thread_wait(world->sim_thread, NULL); world->sim_thread = NULL; pxl8_input_msg* input; while ((input = pxl8_queue_pop(&world->input_queue))) { pxl8_free(input); } } void pxl8_world_pause_sim(pxl8_world* world, bool paused) { if (!world) return; if (paused) { atomic_store(&world->sim_paused, true); } else { pxl8_input_msg* input; while ((input = pxl8_queue_pop(&world->input_queue))) { pxl8_free(input); } world->sim_accumulator = 0.0f; atomic_store(&world->sim_paused, false); } } void pxl8_world_push_input(pxl8_world* world, const pxl8_input_msg* input) { if (!world || !input) return; world->pointer_motion.yaw -= input->look_dx * 0.008f; f32 pitch = world->pointer_motion.pitch - input->look_dy * 0.008f; if (pitch > world->sim_config.max_pitch) pitch = world->sim_config.max_pitch; if (pitch < -world->sim_config.max_pitch) pitch = -world->sim_config.max_pitch; world->pointer_motion.pitch = pitch; pxl8_input_msg* copy = pxl8_malloc(sizeof(pxl8_input_msg)); if (copy) { *copy = *input; if (!pxl8_queue_push(&world->input_queue, copy)) { pxl8_free(copy); } } } const pxl8_sim_entity* pxl8_world_get_render_state(const pxl8_world* world) { if (!world) return NULL; u32 front = atomic_load(&((pxl8_world*)world)->active_buffer); return &world->render_state[front]; } f32 pxl8_world_get_interp_alpha(const pxl8_world* world) { if (!world) return 1.0f; return world->sim_accumulator / SIM_TIMESTEP; } #endif