2025-11-13 07:15:41 -06:00
|
|
|
#include "pxl8_world.h"
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
#include <string.h>
|
2025-10-07 10:32:48 -05:00
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
#include "pxl8_hal.h"
|
|
|
|
|
|
|
|
|
|
#ifdef PXL8_ASYNC_THREADS
|
|
|
|
|
#include <stdatomic.h>
|
|
|
|
|
#include "pxl8_queue.h"
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include "pxl8_bsp_render.h"
|
|
|
|
|
#include "pxl8_io.h"
|
|
|
|
|
#include "pxl8_gfx3d.h"
|
2025-12-02 11:02:23 -06:00
|
|
|
#include "pxl8_log.h"
|
2026-01-21 23:19:50 -06:00
|
|
|
#include "pxl8_mem.h"
|
2026-01-31 09:31:17 -06:00
|
|
|
#include "pxl8_sim.h"
|
|
|
|
|
#include "pxl8_vxl.h"
|
|
|
|
|
#include "pxl8_vxl_render.h"
|
2025-10-07 10:32:48 -05:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
#define PXL8_WORLD_ENTITY_CAPACITY 256
|
2026-01-31 09:31:17 -06:00
|
|
|
#define VOXEL_SCALE 16.0f
|
|
|
|
|
#define VOXEL_CHUNK_SIZE 32.0f
|
|
|
|
|
#define WORLD_CHUNK_SIZE (VOXEL_CHUNK_SIZE * VOXEL_SCALE)
|
2026-01-25 09:26:30 -06:00
|
|
|
|
2025-10-07 10:32:48 -05:00
|
|
|
struct pxl8_world {
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_world_chunk* active_chunk;
|
|
|
|
|
pxl8_vxl_block_registry* block_registry;
|
|
|
|
|
pxl8_world_chunk_cache* chunk_cache;
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_entity_pool* entities;
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_bsp_render_state* bsp_render_state;
|
|
|
|
|
pxl8_vxl_render_state* vxl_render_state;
|
|
|
|
|
i32 render_distance;
|
|
|
|
|
i32 sim_distance;
|
|
|
|
|
|
|
|
|
|
pxl8_sim_entity local_player;
|
|
|
|
|
u64 client_tick;
|
|
|
|
|
|
2026-02-02 17:48:25 -06:00
|
|
|
pxl8_vec2 pointer_motion;
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
#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
|
2025-10-07 10:32:48 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pxl8_world* pxl8_world_create(void) {
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_world* world = pxl8_calloc(1, sizeof(pxl8_world));
|
|
|
|
|
if (!world) return NULL;
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
world->block_registry = pxl8_vxl_block_registry_create();
|
|
|
|
|
world->chunk_cache = pxl8_world_chunk_cache_create();
|
2026-01-25 09:26:30 -06:00
|
|
|
world->entities = pxl8_entity_pool_create(PXL8_WORLD_ENTITY_CAPACITY);
|
2026-01-31 09:31:17 -06:00
|
|
|
world->vxl_render_state = pxl8_vxl_render_state_create();
|
|
|
|
|
world->render_distance = 3;
|
|
|
|
|
world->sim_distance = 4;
|
2026-01-25 09:26:30 -06:00
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
if (!world->block_registry || !world->chunk_cache || !world->entities || !world->vxl_render_state) {
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_world_destroy(world);
|
2025-10-07 10:32:48 -05:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return world;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_world_destroy(pxl8_world* world) {
|
|
|
|
|
if (!world) return;
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_vxl_block_registry_destroy(world->block_registry);
|
|
|
|
|
pxl8_world_chunk_cache_destroy(world->chunk_cache);
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_entity_pool_destroy(world->entities);
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_bsp_render_state_destroy(world->bsp_render_state);
|
|
|
|
|
pxl8_vxl_render_state_destroy(world->vxl_render_state);
|
2026-01-21 23:19:50 -06:00
|
|
|
pxl8_free(world);
|
2025-10-07 10:32:48 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_world_chunk_cache* pxl8_world_get_chunk_cache(pxl8_world* world) {
|
2026-01-25 09:26:30 -06:00
|
|
|
if (!world) return NULL;
|
|
|
|
|
return world->chunk_cache;
|
2025-11-11 21:24:53 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_world_chunk* pxl8_world_active_chunk(pxl8_world* world) {
|
2026-01-25 09:26:30 -06:00
|
|
|
if (!world) return NULL;
|
|
|
|
|
return world->active_chunk;
|
2025-11-21 16:44:42 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_world_chunk* chunk) {
|
2026-01-25 09:26:30 -06:00
|
|
|
if (!world) return;
|
|
|
|
|
world->active_chunk = chunk;
|
2025-11-21 16:44:42 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_vxl_block_registry* pxl8_world_block_registry(pxl8_world* world) {
|
2026-01-25 09:26:30 -06:00
|
|
|
if (!world) return NULL;
|
|
|
|
|
return world->block_registry;
|
|
|
|
|
}
|
2025-11-11 21:24:53 -06:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_entity_pool* pxl8_world_entities(pxl8_world* world) {
|
|
|
|
|
if (!world) return NULL;
|
|
|
|
|
return world->entities;
|
|
|
|
|
}
|
2025-11-09 06:30:17 -06:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
pxl8_entity pxl8_world_spawn(pxl8_world* world) {
|
|
|
|
|
if (!world || !world->entities) return PXL8_ENTITY_INVALID;
|
|
|
|
|
return pxl8_entity_spawn(world->entities);
|
2025-11-09 06:30:17 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
static bool voxel_point_solid(pxl8_world_chunk_cache* cache, f32 x, f32 y, f32 z) {
|
|
|
|
|
i32 cx = (i32)floorf(x / WORLD_CHUNK_SIZE);
|
|
|
|
|
i32 cy = (i32)floorf(y / WORLD_CHUNK_SIZE);
|
|
|
|
|
i32 cz = (i32)floorf(z / WORLD_CHUNK_SIZE);
|
|
|
|
|
|
|
|
|
|
f32 local_x = (x - cx * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
|
|
|
|
|
f32 local_y = (y - cy * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
|
|
|
|
|
f32 local_z = (z - cz * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
|
|
|
|
|
|
|
|
|
|
i32 lx = (i32)floorf(local_x);
|
|
|
|
|
i32 ly = (i32)floorf(local_y);
|
|
|
|
|
i32 lz = (i32)floorf(local_z);
|
|
|
|
|
|
|
|
|
|
lx = lx < 0 ? 0 : (lx >= 32 ? 31 : lx);
|
|
|
|
|
ly = ly < 0 ? 0 : (ly >= 32 ? 31 : ly);
|
|
|
|
|
lz = lz < 0 ? 0 : (lz >= 32 ? 31 : lz);
|
|
|
|
|
|
|
|
|
|
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_vxl(cache, cx, cy, cz);
|
|
|
|
|
if (!chunk || !chunk->voxels) {
|
|
|
|
|
return ly < 8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pxl8_vxl_block_get(chunk->voxels, lx, ly, lz) != PXL8_VXL_BLOCK_AIR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static pxl8_sim_world make_sim_world(const pxl8_world* world, pxl8_vec3 pos) {
|
|
|
|
|
pxl8_sim_world sim = {0};
|
|
|
|
|
|
|
|
|
|
if (world->active_chunk && world->active_chunk->type == PXL8_WORLD_CHUNK_BSP) {
|
|
|
|
|
sim.bsp = world->active_chunk->bsp;
|
|
|
|
|
return sim;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (world->chunk_cache) {
|
|
|
|
|
i32 cx = (i32)floorf(pos.x / WORLD_CHUNK_SIZE);
|
|
|
|
|
i32 cy = (i32)floorf(pos.y / WORLD_CHUNK_SIZE);
|
|
|
|
|
i32 cz = (i32)floorf(pos.z / WORLD_CHUNK_SIZE);
|
|
|
|
|
|
|
|
|
|
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_vxl(world->chunk_cache, cx, cy, cz);
|
|
|
|
|
if (chunk && chunk->voxels) {
|
|
|
|
|
sim.vxl = chunk->voxels;
|
|
|
|
|
sim.vxl_cx = cx;
|
|
|
|
|
sim.vxl_cy = cy;
|
|
|
|
|
sim.vxl_cz = cz;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
if (world->active_chunk->type == PXL8_WORLD_CHUNK_BSP && world->active_chunk->bsp) {
|
|
|
|
|
return pxl8_bsp_point_solid(world->active_chunk->bsp, (pxl8_vec3){x, y, z});
|
|
|
|
|
}
|
|
|
|
|
} else if (world->chunk_cache) {
|
|
|
|
|
return voxel_point_solid(world->chunk_cache, x, y, z);
|
2025-10-07 10:32:48 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-21 16:44:42 -06:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-10-07 10:32:48 -05:00
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_ray pxl8_world_ray(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to) {
|
|
|
|
|
pxl8_ray result = {0};
|
2025-11-21 16:44:42 -06:00
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
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;
|
2025-10-07 10:32:48 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
return result;
|
2026-01-25 09:26:30 -06:00
|
|
|
}
|
2025-10-07 10:32:48 -05:00
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
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, const pxl8_input_state* input, f32 dt) {
|
2026-01-25 09:26:30 -06:00
|
|
|
if (!world) return;
|
2025-11-21 16:44:42 -06:00
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_world_chunk_cache_tick(world->chunk_cache);
|
|
|
|
|
|
|
|
|
|
if (input && (world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) {
|
|
|
|
|
pxl8_input_msg msg = {0};
|
|
|
|
|
msg.move_x = (pxl8_key_down(input, "d") ? 1.0f : 0.0f) - (pxl8_key_down(input, "a") ? 1.0f : 0.0f);
|
|
|
|
|
msg.move_y = (pxl8_key_down(input, "w") ? 1.0f : 0.0f) - (pxl8_key_down(input, "s") ? 1.0f : 0.0f);
|
|
|
|
|
msg.look_dx = (f32)pxl8_mouse_dx(input);
|
|
|
|
|
msg.look_dy = (f32)pxl8_mouse_dy(input);
|
|
|
|
|
msg.buttons = pxl8_key_down(input, "space") ? 1 : 0;
|
2026-02-02 17:48:25 -06:00
|
|
|
msg.tick = world->client_tick;
|
|
|
|
|
msg.timestamp = pxl8_get_ticks_ns();
|
|
|
|
|
|
|
|
|
|
if (world->net) {
|
|
|
|
|
pxl8_net_send_input(world->net, &msg);
|
|
|
|
|
}
|
2026-01-31 09:31:17 -06:00
|
|
|
|
|
|
|
|
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
|
|
|
|
|
pxl8_sim_move_player(&world->local_player, &msg, &sim, dt);
|
2026-02-02 17:48:25 -06:00
|
|
|
world->client_tick++;
|
2026-01-31 09:31:17 -06:00
|
|
|
}
|
2026-01-25 09:26:30 -06:00
|
|
|
}
|
2025-11-21 16:44:42 -06:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
|
2026-01-31 09:31:17 -06:00
|
|
|
if (!world || !gfx) return;
|
2025-11-21 16:44:42 -06:00
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
if (world->active_chunk) {
|
|
|
|
|
if (world->active_chunk->type == PXL8_WORLD_CHUNK_BSP && 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);
|
|
|
|
|
i32 cx = (i32)floorf(camera_pos.x / WORLD_CHUNK_SIZE);
|
|
|
|
|
i32 cy = (i32)floorf(camera_pos.y / WORLD_CHUNK_SIZE);
|
|
|
|
|
i32 cz = (i32)floorf(camera_pos.z / WORLD_CHUNK_SIZE);
|
|
|
|
|
|
|
|
|
|
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
|
|
|
|
|
|
|
|
|
|
pxl8_gfx_material mat = {
|
|
|
|
|
.texture_id = 0,
|
|
|
|
|
.dynamic_lighting = true,
|
|
|
|
|
.alpha = 255,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
i32 r = world->render_distance;
|
|
|
|
|
for (i32 dy = -r; dy <= r; dy++) {
|
|
|
|
|
for (i32 dz = -r; dz <= r; dz++) {
|
|
|
|
|
for (i32 dx = -r; dx <= r; dx++) {
|
|
|
|
|
i32 chunk_cx = cx + dx;
|
|
|
|
|
i32 chunk_cy = cy + dy;
|
|
|
|
|
i32 chunk_cz = cz + dz;
|
|
|
|
|
f32 chunk_x = chunk_cx * WORLD_CHUNK_SIZE;
|
|
|
|
|
f32 chunk_y = chunk_cy * WORLD_CHUNK_SIZE;
|
|
|
|
|
f32 chunk_z = chunk_cz * WORLD_CHUNK_SIZE;
|
|
|
|
|
|
|
|
|
|
if (frustum) {
|
|
|
|
|
pxl8_vec3 aabb_min = {chunk_x, chunk_y, chunk_z};
|
|
|
|
|
pxl8_vec3 aabb_max = {chunk_x + WORLD_CHUNK_SIZE, chunk_y + WORLD_CHUNK_SIZE, chunk_z + WORLD_CHUNK_SIZE};
|
|
|
|
|
if (!pxl8_frustum_test_aabb(frustum, aabb_min, aabb_max)) continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_vxl_mesh_config config = PXL8_VXL_MESH_CONFIG_DEFAULT;
|
|
|
|
|
config.chunk_x = chunk_cx;
|
|
|
|
|
config.chunk_y = chunk_cy;
|
|
|
|
|
config.chunk_z = chunk_cz;
|
|
|
|
|
|
|
|
|
|
pxl8_mesh* mesh = pxl8_world_chunk_cache_get_mesh(
|
|
|
|
|
world->chunk_cache,
|
|
|
|
|
chunk_cx, chunk_cy, chunk_cz,
|
|
|
|
|
world->block_registry, &config);
|
|
|
|
|
if (mesh && mesh->index_count > 0) {
|
|
|
|
|
pxl8_mat4 scale = pxl8_mat4_scale(VOXEL_SCALE, VOXEL_SCALE, VOXEL_SCALE);
|
|
|
|
|
pxl8_mat4 translate = pxl8_mat4_translate(chunk_x, chunk_y, chunk_z);
|
|
|
|
|
pxl8_mat4 model = pxl8_mat4_multiply(translate, scale);
|
|
|
|
|
pxl8_3d_draw_mesh(gfx, mesh, &model, &mat);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-21 16:44:42 -06:00
|
|
|
}
|
2026-01-25 09:26:30 -06:00
|
|
|
}
|
2025-11-21 16:44:42 -06:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
void pxl8_world_sync(pxl8_world* world, pxl8_net* net) {
|
|
|
|
|
if (!world || !net) return;
|
2025-11-21 16:44:42 -06:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
u8 chunk_type = pxl8_net_chunk_type(net);
|
|
|
|
|
u32 chunk_id = pxl8_net_chunk_id(net);
|
2025-11-21 16:44:42 -06:00
|
|
|
|
2026-01-25 09:26:30 -06:00
|
|
|
if (chunk_type == PXL8_CHUNK_TYPE_BSP && chunk_id != 0) {
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_bsp(world->chunk_cache, chunk_id);
|
2026-01-25 09:26:30 -06:00
|
|
|
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);
|
2026-02-02 17:48:25 -06:00
|
|
|
|
|
|
|
|
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);
|
2025-11-21 16:44:42 -06:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-31 09:31:17 -06:00
|
|
|
} else if (chunk_id == 0 && world->active_chunk != NULL) {
|
|
|
|
|
world->active_chunk = NULL;
|
2026-02-02 17:48:25 -06:00
|
|
|
if (world->bsp_render_state) {
|
|
|
|
|
pxl8_bsp_render_state_destroy(world->bsp_render_state);
|
|
|
|
|
world->bsp_render_state = NULL;
|
|
|
|
|
}
|
2025-11-21 16:44:42 -06:00
|
|
|
}
|
2025-10-07 10:32:48 -05:00
|
|
|
}
|
2026-01-31 09:31:17 -06:00
|
|
|
|
|
|
|
|
static void ensure_bsp_render_state(pxl8_world* world) {
|
|
|
|
|
if (!world || world->bsp_render_state) return;
|
2026-02-02 17:48:25 -06:00
|
|
|
if (!world->active_chunk) return;
|
|
|
|
|
if (world->active_chunk->type != PXL8_WORLD_CHUNK_BSP) return;
|
2026-01-31 09:31:17 -06:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i32 pxl8_world_get_render_distance(const pxl8_world* world) {
|
|
|
|
|
if (!world) return 3;
|
|
|
|
|
return world->render_distance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_world_set_render_distance(pxl8_world* world, i32 distance) {
|
|
|
|
|
if (!world) return;
|
|
|
|
|
if (distance < 1) distance = 1;
|
|
|
|
|
if (distance > 8) distance = 8;
|
|
|
|
|
world->render_distance = distance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i32 pxl8_world_get_sim_distance(const pxl8_world* world) {
|
|
|
|
|
if (!world) return 4;
|
|
|
|
|
return world->sim_distance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pxl8_world_set_sim_distance(pxl8_world* world, i32 distance) {
|
|
|
|
|
if (!world) return;
|
|
|
|
|
if (distance < 1) distance = 1;
|
|
|
|
|
if (distance > 8) distance = 8;
|
|
|
|
|
world->sim_distance = distance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
#ifdef PXL8_ASYNC_THREADS
|
|
|
|
|
world->render_state[0] = world->local_player;
|
|
|
|
|
world->render_state[1] = world->local_player;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pxl8_sim_entity* pxl8_world_local_player(pxl8_world* world) {
|
|
|
|
|
if (!world) return NULL;
|
2026-02-02 17:48:25 -06:00
|
|
|
#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
|
2026-01-31 09:31:17 -06:00
|
|
|
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return NULL;
|
|
|
|
|
return &world->local_player;
|
2026-02-02 17:48:25 -06:00
|
|
|
#endif
|
2026-01-31 09:31:17 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 = make_sim_world(world, world->local_player.pos);
|
|
|
|
|
pxl8_sim_move_player(&world->local_player, input, &sim, 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;
|
|
|
|
|
|
2026-02-02 17:48:25 -06:00
|
|
|
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;
|
2026-01-31 09:31:17 -06:00
|
|
|
|
|
|
|
|
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 = make_sim_world(world, world->local_player.pos);
|
|
|
|
|
pxl8_sim_move_player(&world->local_player, input, &sim, 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) {
|
2026-02-02 17:48:25 -06:00
|
|
|
bool alive = (world->local_player.flags & PXL8_SIM_FLAG_ALIVE) != 0;
|
2026-01-31 09:31:17 -06:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 17:48:25 -06:00
|
|
|
if (!alive) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
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;
|
|
|
|
|
|
2026-02-02 17:48:25 -06:00
|
|
|
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;
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
|
|
|
|
|
pxl8_sim_move_player(&world->local_player, &merged, &sim, 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;
|
|
|
|
|
|
2026-02-02 17:48:25 -06:00
|
|
|
world->pointer_motion.yaw -= input->look_dx * 0.008f;
|
|
|
|
|
f32 pitch = world->pointer_motion.pitch - input->look_dy * 0.008f;
|
|
|
|
|
if (pitch > PXL8_SIM_MAX_PITCH) pitch = PXL8_SIM_MAX_PITCH;
|
|
|
|
|
if (pitch < -PXL8_SIM_MAX_PITCH) pitch = -PXL8_SIM_MAX_PITCH;
|
|
|
|
|
world->pointer_motion.pitch = pitch;
|
|
|
|
|
|
2026-01-31 09:31:17 -06:00
|
|
|
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
|