stream world data from pxl8d to pxl8

This commit is contained in:
asrael 2026-01-25 09:26:30 -06:00
parent 39ee0fefb7
commit a71a9840b2
55 changed files with 5290 additions and 2131 deletions

View file

@ -11,7 +11,7 @@
(local bob-amount 4.0) (local bob-amount 4.0)
(local bob-speed 8.0) (local bob-speed 8.0)
(local cam-smoothing 0.25) (local cam-smoothing 0.25)
(local cell-size 64) (local chunk-size 64)
(local cursor-sensitivity 0.010) (local cursor-sensitivity 0.010)
(local gravity -800) (local gravity -800)
(local grid-size 64) (local grid-size 64)
@ -32,25 +32,29 @@
(var auto-run-cancel-key nil) (var auto-run-cancel-key nil)
(var bob-time 0) (var bob-time 0)
(var cam-pitch 0) (var cam-pitch 0)
(var cam-x 1000) (var cam-x 416)
(var cam-y 64) (var cam-y 64)
(var cam-yaw 0) (var cam-yaw 0)
(var cam-z 1000) (var cam-z 416)
(var camera nil) (var camera nil)
(var grounded? true) (var ceiling-tex nil)
(var land-squash 0) (var fireball-mesh nil)
(var light-time 0) (var floor-tex nil)
(var real-time 0)
(var network nil)
(var smooth-cam-x 1000)
(var smooth-cam-z 1000)
(var velocity-y 0)
(var world nil)
(var fps-avg 0) (var fps-avg 0)
(var fps-sample-count 0) (var fps-sample-count 0)
(var fireball-mesh nil) (var grounded? true)
(var land-squash 0)
(var last-dt 0.016) (var last-dt 0.016)
(var light-time 0)
(var lights nil) (var lights nil)
(var materials-setup false)
(var network nil)
(var real-time 0)
(var smooth-cam-x 416)
(var smooth-cam-z 416)
(var velocity-y 0)
(var wall-tex nil)
(var world nil)
(local cursor-look? true) (local cursor-look? true)
(local FIREBALL_COLOR 218) (local FIREBALL_COLOR 218)
@ -247,43 +251,54 @@
(pxl8.set_palette_rgb (+ FIREBALL_COLOR i) r g b))) (pxl8.set_palette_rgb (+ FIREBALL_COLOR i) r g b)))
(sky.update-gradient 1 2 6 6 10 18) (sky.update-gradient 1 2 6 6 10 18)
(pxl8.update_palette_deps) (pxl8.update_palette_deps)
(set camera (pxl8.create_camera_3d))
(set world (pxl8.create_world)) (when (not camera)
(set lights (pxl8.create_lights)) (set camera (pxl8.create_camera_3d)))
(when (not lights)
(set lights (pxl8.create_lights)))
(when (not fireball-mesh)
(create-fireball-mesh))
(sky.generate-stars 12345) (sky.generate-stars 12345)
(create-fireball-mesh)
(set network (net.Net.new {:port 7777})) (when (not network)
(set network (net.get))
(when network (when network
(network:connect) (network:spawn cam-x cam-y cam-z cam-yaw cam-pitch)))
(network:spawn cam-x cam-y cam-z cam-yaw cam-pitch))
(let [result (world:generate { (when (not world)
:type pxl8.PROCGEN_ROOMS (set world (pxl8.get_world)))
:width 64
:height 64
:seed 42
:min_room_size 5
:max_room_size 10
:num_rooms 20})]
(if (< result 0)
(pxl8.error (.. "Failed to generate rooms - result: " result))
(let [floor-tex (textures.mossy-cobblestone 44444 STONE_FLOOR_START MOSS_COLOR)
wall-tex (textures.ashlar-wall 55555 STONE_WALL_START MOSS_COLOR)
sky-tex (pxl8.create_texture [0] 1 1)]
(let [result (world:apply_textures [ (when (not floor-tex)
{:name "floor" (set floor-tex (textures.mossy-cobblestone 44444 STONE_FLOOR_START MOSS_COLOR)))
:texture_id floor-tex (when (not wall-tex)
:rule (fn [normal] (> normal.y 0.7))} (set wall-tex (textures.ashlar-wall 55555 STONE_WALL_START MOSS_COLOR)))
{:name "ceiling" (when (not ceiling-tex)
:texture_id sky-tex (set ceiling-tex (pxl8.create_texture [0] 1 1)))
:rule (fn [normal] (< normal.y -0.7))}
{:name "wall" )
:texture_id wall-tex
:rule (fn [normal] (and (<= normal.y 0.7) (>= normal.y -0.7)))}])] (fn setup-materials []
(when (< result 0) (when (and world (not materials-setup))
(pxl8.error (.. "Failed to apply textures - result: " result)))))))) (let [chunk (world:active_chunk)]
(when (and chunk (chunk:ready))
(let [bsp (chunk:bsp)]
(when bsp
(let [floor-mat (pxl8.create_material {:texture floor-tex :lighting true})
wall-mat (pxl8.create_material {:texture wall-tex :lighting true})
ceiling-mat (pxl8.create_material {:texture ceiling-tex})]
(bsp:set_material 0 floor-mat)
(bsp:set_material 1 wall-mat)
(bsp:set_material 2 ceiling-mat)
(for [i 0 (- (bsp:face_count) 1)]
(let [n (bsp:face_normal i)]
(bsp:face_set_material i
(if (> n.y 0.7) 0
(< n.y -0.7) 2
1))))
(set materials-setup true))))))))
(fn sample-input [] (fn sample-input []
(var move-forward 0) (var move-forward 0)
@ -328,9 +343,10 @@
(let [input (get-pending-input t) (let [input (get-pending-input t)
hist (get-position t)] hist (get-position t)]
(when (and input hist) (when (and input hist)
(let [(new-x new-z) (apply-movement cam-x cam-z hist.yaw input)] (let [(new-x new-z) (apply-movement cam-x cam-z hist.yaw input)
(set cam-x new-x) (resolved-x _ resolved-z) (world:resolve_collision cam-x cam-y cam-z new-x cam-y new-z 5)]
(set cam-z new-z) (set cam-x resolved-x)
(set cam-z resolved-z)
(store-position t cam-x cam-z hist.yaw)))))))))) (store-position t cam-x cam-z hist.yaw))))))))))
(fn update [dt] (fn update [dt]
@ -343,9 +359,12 @@
(set fps-sample-count 0) (set fps-sample-count 0)
(set fps-avg 0))) (set fps-avg 0)))
(when (world:is_loaded) (setup-materials)
(let [chunk (world:active_chunk)]
(when (and chunk (chunk:ready))
(let [input (sample-input) (let [input (sample-input)
grid-max (* grid-size cell-size) grid-max (* grid-size chunk-size)
movement-yaw cam-yaw] movement-yaw cam-yaw]
(set time-accumulator (+ time-accumulator dt)) (set time-accumulator (+ time-accumulator dt))
@ -388,8 +407,6 @@
:look_dy input.look_dy :look_dy input.look_dy
:yaw movement-yaw :yaw movement-yaw
:tick client-tick}) :tick client-tick})
(network:update dt)
(when (network:poll)
(let [snapshot (network:snapshot)] (let [snapshot (network:snapshot)]
(when (and snapshot (> snapshot.tick last-processed-tick)) (when (and snapshot (> snapshot.tick last-processed-tick))
(set last-processed-tick snapshot.tick) (set last-processed-tick snapshot.tick)
@ -399,7 +416,7 @@
(when curr (when curr
(let [srv-x (pxl8.unpack_f32_be curr 0) (let [srv-x (pxl8.unpack_f32_be curr 0)
srv-z (pxl8.unpack_f32_be curr 8)] srv-z (pxl8.unpack_f32_be curr 8)]
(reconcile snapshot.tick srv-x srv-z)))))))))))] (reconcile snapshot.tick srv-x srv-z))))))))))]
(when (not ok) (when (not ok)
(pxl8.error (.. "Network error: " err))))) (pxl8.error (.. "Network error: " err)))))
@ -430,7 +447,7 @@
(set bob-time (+ (* bob-time 0.8) (* target-phase 0.2)))))) (set bob-time (+ (* bob-time 0.8) (* target-phase 0.2))))))
(set light-time (+ light-time (* dt 0.5))) (set light-time (+ light-time (* dt 0.5)))
(set real-time (+ real-time dt))))) (set real-time (+ real-time dt))))))
(fn frame [] (fn frame []
(pxl8.clear 1) (pxl8.clear 1)
@ -441,10 +458,11 @@
(when (not world) (when (not world)
(pxl8.error "world is nil!")) (pxl8.error "world is nil!"))
(when (and world (not (world:is_loaded))) (let [chunk (when world (world:active_chunk))]
(pxl8.text "World not loaded yet..." 5 30 12)) (when (and world (or (not chunk) (not (chunk:ready))))
(pxl8.text "Waiting for world data..." 5 30 12))
(when (and camera world (world:is_loaded)) (when (and camera world chunk (chunk:ready))
(let [bob-offset (* (math.sin bob-time) bob-amount) (let [bob-offset (* (math.sin bob-time) bob-amount)
eye-y (+ cam-y bob-offset land-squash) eye-y (+ cam-y bob-offset land-squash)
forward-x (- (math.sin cam-yaw)) forward-x (- (math.sin cam-yaw))
@ -459,8 +477,8 @@
[0 1 0]) [0 1 0])
(camera:set_perspective 1.047 aspect 1.0 4096.0) (camera:set_perspective 1.047 aspect 1.0 4096.0)
(let [light-x (+ 1000 (* 50 (math.cos light-time))) (let [light-x (+ 384 (* 50 (math.cos light-time)))
light-z (+ 940 (* 50 (math.sin light-time))) light-z (+ 324 (* 50 (math.sin light-time)))
light-y 80 light-y 80
phase (+ (* light-x 0.01) 1.7) phase (+ (* light-x 0.01) 1.7)
f1 (* 0.08 (math.sin (+ (* real-time 2.5) phase))) f1 (* 0.08 (math.sin (+ (* real-time 2.5) phase)))
@ -484,7 +502,10 @@
(sky.render smooth-cam-x eye-y smooth-cam-z (menu.is-wireframe)) (sky.render smooth-cam-x eye-y smooth-cam-z (menu.is-wireframe))
(pxl8.clear_depth) (pxl8.clear_depth)
(world:set_wireframe (menu.is-wireframe) 15) (when chunk
(let [bsp (chunk:bsp)]
(when bsp
(bsp:set_wireframe (menu.is-wireframe)))))
(world:render [smooth-cam-x eye-y smooth-cam-z]) (world:render [smooth-cam-x eye-y smooth-cam-z])
(when fireball-mesh (when fireball-mesh
@ -509,7 +530,7 @@
(pxl8.text (.. "fps: " (string.format "%.0f" fps-avg)) 5 5 text-color) (pxl8.text (.. "fps: " (string.format "%.0f" fps-avg)) 5 5 text-color)
(pxl8.text (.. "pos: " (string.format "%.0f" cam-x) "," (pxl8.text (.. "pos: " (string.format "%.0f" cam-x) ","
(string.format "%.0f" cam-y) "," (string.format "%.0f" cam-y) ","
(string.format "%.0f" cam-z)) 5 15 text-color))))) (string.format "%.0f" cam-z)) 5 15 text-color))))))
{:init init {:init init
:update update :update update

View file

@ -8,8 +8,8 @@
(local NUM_RANDOM_STARS 300) (local NUM_RANDOM_STARS 300)
(local NUM_TINY_STARS 7000) (local NUM_TINY_STARS 7000)
(local STAR_CYCLE_PERIOD 86400)
(local STAR_SEED 0xDEADBEEF) (local STAR_SEED 0xDEADBEEF)
(local STAR_CYCLE_PERIOD 7200)
;; Use existing bright palette colors ;; Use existing bright palette colors
;; Silver/white: indices 14-15 (brightest grays) ;; Silver/white: indices 14-15 (brightest grays)
@ -102,15 +102,21 @@
(generate-sky-gradient zenith-r zenith-g zenith-b horizon-r horizon-g horizon-b) (generate-sky-gradient zenith-r zenith-g zenith-b horizon-r horizon-g horizon-b)
(set last-gradient-key key)))) (set last-gradient-key key))))
(fn galactic-band-factor [dx dy dz] (fn band-factor [dx dy dz bx by bz width]
(let [band-len (math.sqrt (+ (* 0.6 0.6) (* 0.3 0.3) (* 0.742 0.742))) (let [dist (math.abs (+ (* dx bx) (* dy by) (* dz bz)))
bx (/ 0.6 band-len) in-band (- 1 (math.min (* dist width) 1))]
by (/ 0.3 band-len)
bz (/ 0.742 band-len)
dist-from-band (math.abs (+ (* dx bx) (* dy by) (* dz bz)))
in-band (- 1 (math.min (* dist-from-band 3) 1))]
(* in-band in-band))) (* in-band in-band)))
(fn galactic-band-factor [dx dy dz]
(let [;; Main galactic band - crosses zenith
band-len (math.sqrt (+ (* 0.6 0.6) (* 0.3 0.3) (* 0.742 0.742)))
b1 (band-factor dx dy dz (/ 0.6 band-len) (/ 0.3 band-len) (/ 0.742 band-len) 3)
;; Secondary band - lower angle, different orientation
b2 (band-factor dx dy dz 0.8 0.15 0.58 3.5)
;; Tertiary band - opposite side
b3 (band-factor dx dy dz -0.7 0.2 0.69 4)]
(math.max b1 b2 b3)))
(fn generate-stars [] (fn generate-stars []
(set random-stars []) (set random-stars [])
(set tiny-stars []) (set tiny-stars [])
@ -205,8 +211,9 @@
(let [star (. tiny-stars (+ i 1)) (let [star (. tiny-stars (+ i 1))
int (math.floor (* star.brightness fade-in))] int (math.floor (* star.brightness fade-in))]
(when (> int 8) (when (> int 8)
(star-glows:add (math.floor screen.x) (math.floor screen.y) (let [px (math.floor (+ screen.x 0.5))
1 int star.color pxl8.GLOW_CIRCLE)))))) py (math.floor (+ screen.y 0.5))]
(star-glows:add px py 1 int star.color pxl8.GLOW_CIRCLE)))))))
(for [i 0 (- (length random-stars) 1)] (for [i 0 (- (length random-stars) 1)]
(let [screen (. star-projected (+ tiny-count i))] (let [screen (. star-projected (+ tiny-count i))]
@ -215,8 +222,8 @@
phase (+ (* (+ i 1) 2.137) (* time-factor 3.0)) phase (+ (* (+ i 1) 2.137) (* time-factor 3.0))
twinkle (+ 0.75 (* 0.25 (math.sin (* phase 6.28)))) twinkle (+ 0.75 (* 0.25 (math.sin (* phase 6.28))))
int (math.floor (* star.brightness fade-in twinkle)) int (math.floor (* star.brightness fade-in twinkle))
sx (math.floor screen.x) sx (math.floor (+ screen.x 0.5))
sy (math.floor screen.y)] sy (math.floor (+ screen.y 0.5))]
(if (> star.brightness 220) (if (> star.brightness 220)
(do (do
(star-glows:add sx sy 3 (math.floor (* int 1.5)) star.color pxl8.GLOW_DIAMOND) (star-glows:add sx sy 3 (math.floor (* int 1.5)) star.color pxl8.GLOW_DIAMOND)

85
pxl8.sh
View file

@ -58,20 +58,20 @@ build_luajit() {
build_server() { build_server() {
local mode="$1" local mode="$1"
if [[ -d "server" ]]; then if [[ -d "pxl8d" ]]; then
print_info "Building server ($mode mode)" print_info "Building pxl8d ($mode mode)"
cd server cd pxl8d
if [[ "$mode" == "release" ]]; then if [[ "$mode" == "release" ]]; then
cargo build --release cargo build --release --quiet
else else
cargo build cargo build --quiet
fi fi
local status=$? local status=$?
cd - > /dev/null cd - > /dev/null
if [[ $status -eq 0 ]]; then if [[ $status -eq 0 ]]; then
print_info "Built server" print_info "Built pxl8d"
else else
print_error "Server build failed" print_error "pxl8d build failed"
fi fi
fi fi
} }
@ -80,9 +80,9 @@ start_server() {
local mode="$1" local mode="$1"
local server_bin local server_bin
if [[ "$mode" == "release" ]]; then if [[ "$mode" == "release" ]]; then
server_bin="server/target/release/pxl8-server" server_bin="pxl8d/target/release/pxl8d"
else else
server_bin="server/target/debug/pxl8-server" server_bin="pxl8d/target/debug/pxl8d"
fi fi
print_info "Server mode: $mode, binary: $server_bin" print_info "Server mode: $mode, binary: $server_bin"
if [[ -f "$server_bin" ]]; then if [[ -f "$server_bin" ]]; then
@ -92,8 +92,8 @@ start_server() {
print_info "Server started with PID $SERVER_PID" print_info "Server started with PID $SERVER_PID"
sleep 0.5 sleep 0.5
else else
print_error "Server binary not found: $server_bin" print_error "pxl8d binary not found: $server_bin"
print_error "Build the server first with: cd server && cargo build" print_error "Build pxl8d first with: cd pxl8d && cargo build"
fi fi
} }
@ -105,6 +105,15 @@ stop_server() {
fi fi
} }
clean_server() {
if [[ -d "pxl8d" ]]; then
print_info "Cleaning pxl8d"
cd pxl8d
cargo clean 2>/dev/null || true
cd - > /dev/null
fi
}
build_sdl() { build_sdl() {
if [[ ! -f "lib/SDL/build/libSDL3.so" ]] && [[ ! -f "lib/SDL/build/libSDL3.a" ]] && [[ ! -f "lib/SDL/build/libSDL3.dylib" ]]; then if [[ ! -f "lib/SDL/build/libSDL3.so" ]] && [[ ! -f "lib/SDL/build/libSDL3.a" ]] && [[ ! -f "lib/SDL/build/libSDL3.dylib" ]]; then
print_info "Building SDL3" print_info "Building SDL3"
@ -160,9 +169,9 @@ print_usage() {
echo echo
echo -e "${BOLD}OPTIONS:${NC}" echo -e "${BOLD}OPTIONS:${NC}"
echo " --all Clean both build artifacts and dependencies" echo " --all Clean both build artifacts and dependencies"
echo " --cache Clear ccache (use with clean)"
echo " --deps Clean only dependencies" echo " --deps Clean only dependencies"
echo " --release Build/run/clean in release mode (default: debug)" echo " --release Build/run/clean in release mode (default: debug)"
echo " --server Start game server before running (for networked games)"
} }
setup_sdl3() { setup_sdl3() {
@ -364,6 +373,8 @@ case "$COMMAND" in
build_sdl build_sdl
fi fi
build_server "$MODE"
setup_sdl3 setup_sdl3
print_info "Building pxl8 ($MODE mode)" print_info "Building pxl8 ($MODE mode)"
@ -380,16 +391,19 @@ case "$COMMAND" in
LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/miniz/miniz.c" LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/miniz/miniz.c"
PXL8_SOURCE_FILES=" PXL8_SOURCE_FILES="
src/asset/pxl8_ase.c
src/asset/pxl8_cart.c
src/asset/pxl8_save.c
src/core/pxl8.c src/core/pxl8.c
src/core/pxl8_bytes.c src/core/pxl8_bytes.c
src/core/pxl8_io.c src/core/pxl8_io.c
src/core/pxl8_log.c src/core/pxl8_log.c
src/core/pxl8_replay.c
src/core/pxl8_rng.c src/core/pxl8_rng.c
src/math/pxl8_math.c src/gfx/pxl8_3d_camera.c
src/gfx/pxl8_anim.c src/gfx/pxl8_anim.c
src/gfx/pxl8_atlas.c src/gfx/pxl8_atlas.c
src/gfx/pxl8_blit.c src/gfx/pxl8_blit.c
src/gfx/pxl8_3d_camera.c
src/gfx/pxl8_colormap.c src/gfx/pxl8_colormap.c
src/gfx/pxl8_cpu.c src/gfx/pxl8_cpu.c
src/gfx/pxl8_dither.c src/gfx/pxl8_dither.c
@ -404,22 +418,23 @@ case "$COMMAND" in
src/gfx/pxl8_tilemap.c src/gfx/pxl8_tilemap.c
src/gfx/pxl8_tilesheet.c src/gfx/pxl8_tilesheet.c
src/gfx/pxl8_transition.c src/gfx/pxl8_transition.c
src/sfx/pxl8_sfx.c src/gui/pxl8_gui.c
src/script/pxl8_repl.c
src/script/pxl8_script.c
src/hal/pxl8_hal_sdl3.c src/hal/pxl8_hal_sdl3.c
src/hal/pxl8_mem_sdl3.c src/hal/pxl8_mem_sdl3.c
src/world/pxl8_bsp.c src/math/pxl8_math.c
src/world/pxl8_gen.c
src/world/pxl8_world.c
src/procgen/pxl8_graph.c
src/asset/pxl8_ase.c
src/asset/pxl8_cart.c
src/asset/pxl8_save.c
src/gui/pxl8_gui.c
src/core/pxl8_replay.c
src/net/pxl8_net.c src/net/pxl8_net.c
src/net/pxl8_protocol.c src/net/pxl8_protocol.c
src/procgen/pxl8_graph.c
src/script/pxl8_repl.c
src/script/pxl8_script.c
src/sfx/pxl8_sfx.c
src/world/pxl8_bsp.c
src/world/pxl8_chunk.c
src/world/pxl8_chunk_cache.c
src/world/pxl8_entity.c
src/world/pxl8_gen.c
src/world/pxl8_voxel.c
src/world/pxl8_world.c
" "
LUAJIT_LIB="lib/luajit/src/libluajit.a" LUAJIT_LIB="lib/luajit/src/libluajit.a"
@ -450,7 +465,8 @@ case "$COMMAND" in
NEEDS_REBUILD=false NEEDS_REBUILD=false
if [[ "$src_file" -nt "$obj_file" ]] || \ if [[ "$src_file" -nt "$obj_file" ]] || \
[[ "src/core/pxl8_types.h" -nt "$obj_file" ]] || \ [[ "src/core/pxl8_types.h" -nt "$obj_file" ]] || \
[[ "src/core/pxl8_macros.h" -nt "$obj_file" ]]; then [[ "src/core/pxl8_macros.h" -nt "$obj_file" ]] || \
[[ "src/net/pxl8_protocol.h" -nt "$obj_file" ]]; then
NEEDS_REBUILD=true NEEDS_REBUILD=true
fi fi
@ -489,24 +505,18 @@ case "$COMMAND" in
CART="" CART=""
EXTRA_ARGS="" EXTRA_ARGS=""
RUN_SERVER=false
for arg in "$@"; do for arg in "$@"; do
if [[ "$arg" == "--release" ]]; then if [[ "$arg" == "--release" ]]; then
MODE="release" MODE="release"
elif [[ "$arg" == "--repl" ]]; then elif [[ "$arg" == "--repl" ]]; then
EXTRA_ARGS="$EXTRA_ARGS --repl" EXTRA_ARGS="$EXTRA_ARGS --repl"
elif [[ "$arg" == "--server" ]]; then
RUN_SERVER=true
elif [[ -z "$CART" ]]; then elif [[ -z "$CART" ]]; then
CART="$arg" CART="$arg"
fi fi
done done
if [[ "$RUN_SERVER" == true ]]; then
build_server "$MODE"
start_server "$MODE" start_server "$MODE"
trap stop_server EXIT trap stop_server EXIT
fi
if [[ -z "$CART" ]]; then if [[ -z "$CART" ]]; then
"$BINDIR/pxl8" $EXTRA_ARGS "$BINDIR/pxl8" $EXTRA_ARGS
@ -517,17 +527,24 @@ case "$COMMAND" in
clean) clean)
CLEAN_ALL=false CLEAN_ALL=false
CLEAN_CACHE=false
CLEAN_DEPS=false CLEAN_DEPS=false
CLEAN_RELEASE=false CLEAN_RELEASE=false
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--all) CLEAN_ALL=true ;; --all) CLEAN_ALL=true ;;
--cache) CLEAN_CACHE=true ;;
--deps) CLEAN_DEPS=true ;; --deps) CLEAN_DEPS=true ;;
--release) CLEAN_RELEASE=true ;; --release) CLEAN_RELEASE=true ;;
esac esac
done done
if [[ "$CLEAN_CACHE" == true ]] && command -v ccache >/dev/null 2>&1; then
print_info "Clearing ccache"
ccache -C >/dev/null
fi
if [[ "$CLEAN_RELEASE" == true ]]; then if [[ "$CLEAN_RELEASE" == true ]]; then
BUILD_PATH=".build/release" BUILD_PATH=".build/release"
BIN_PATH="bin/release" BIN_PATH="bin/release"
@ -541,6 +558,7 @@ case "$COMMAND" in
if [[ "$CLEAN_ALL" == true ]]; then if [[ "$CLEAN_ALL" == true ]]; then
print_info "Removing build artifacts and dependencies" print_info "Removing build artifacts and dependencies"
rm -rf "$BUILD_PATH" "$BIN_PATH" lib rm -rf "$BUILD_PATH" "$BIN_PATH" lib
clean_server
print_info "Cleaned all" print_info "Cleaned all"
elif [[ "$CLEAN_DEPS" == true ]]; then elif [[ "$CLEAN_DEPS" == true ]]; then
print_info "Removing dependencies" print_info "Removing dependencies"
@ -549,6 +567,7 @@ case "$COMMAND" in
else else
print_info "Removing build artifacts" print_info "Removing build artifacts"
rm -rf "$BUILD_PATH" "$BIN_PATH" rm -rf "$BUILD_PATH" "$BIN_PATH"
clean_server
print_info "Cleaned" print_info "Cleaned"
fi fi
;; ;;

263
pxl8d/Cargo.lock generated Normal file
View file

@ -0,0 +1,263 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "cc"
version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pxl8d"
version = "0.1.0"
dependencies = [
"bindgen",
"cc",
"libc",
"libm",
"windows-sys",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]

View file

@ -1,13 +1,16 @@
[package] [package]
name = "pxl8-server" name = "pxl8d"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[lib]
name = "pxl8d"
[build-dependencies] [build-dependencies]
bindgen = "0.72" bindgen = "0.72"
cc = "1.2"
[dependencies] [dependencies]
bevy_ecs = { version = "0.18", default-features = false }
libc = { version = "0.2", default-features = false } libc = { version = "0.2", default-features = false }
libm = { version = "0.2", default-features = false } libm = { version = "0.2", default-features = false }
@ -20,15 +23,18 @@ windows-sys = { version = "0.61", default-features = false, features = [
] } ] }
[[bin]] [[bin]]
name = "pxl8-server" name = "pxl8d"
path = "src/main.rs" path = "src/main.rs"
[profile.dev] [profile.dev]
opt-level = 2
panic = "abort" panic = "abort"
[profile.dev.package."*"]
opt-level = 3
[profile.release] [profile.release]
panic = "abort" panic = "abort"
lto = true
[features] codegen-units = 1
default = [] strip = true
std = ["bevy_ecs/std"]

View file

@ -5,10 +5,19 @@ fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let pxl8_src = PathBuf::from(&manifest_dir).join("../src"); let pxl8_src = PathBuf::from(&manifest_dir).join("../src");
println!("cargo:rerun-if-changed=../src/net/pxl8_protocol.h"); println!("cargo:rerun-if-changed=../src/core/pxl8_log.c");
println!("cargo:rerun-if-changed=../src/core/pxl8_log.h");
println!("cargo:rerun-if-changed=../src/core/pxl8_types.h"); println!("cargo:rerun-if-changed=../src/core/pxl8_types.h");
println!("cargo:rerun-if-changed=../src/net/pxl8_protocol.h");
cc::Build::new()
.file(pxl8_src.join("core/pxl8_log.c"))
.include(pxl8_src.join("core"))
.define("PXL8_SERVER", None)
.compile("pxl8_log");
let bindings = bindgen::Builder::default() let bindings = bindgen::Builder::default()
.header(pxl8_src.join("core/pxl8_log.h").to_str().unwrap())
.header(pxl8_src.join("net/pxl8_protocol.h").to_str().unwrap()) .header(pxl8_src.join("net/pxl8_protocol.h").to_str().unwrap())
.clang_arg(format!("-I{}", pxl8_src.join("core").display())) .clang_arg(format!("-I{}", pxl8_src.join("core").display()))
.use_core() .use_core()

173
pxl8d/src/bsp.rs Normal file
View file

@ -0,0 +1,173 @@
extern crate alloc;
use alloc::vec::Vec;
use crate::math::Vec3;
#[derive(Clone, Copy, Default)]
pub struct BspVertex {
pub position: Vec3,
}
#[derive(Clone, Copy, Default)]
pub struct BspEdge {
pub vertex: [u16; 2],
}
#[derive(Clone, Copy, Default)]
pub struct BspFace {
pub first_edge: u32,
pub lightmap_offset: u32,
pub num_edges: u16,
pub plane_id: u16,
pub side: u16,
pub styles: [u8; 4],
pub material_id: u16,
pub aabb_min: Vec3,
pub aabb_max: Vec3,
}
#[derive(Clone, Copy, Default)]
pub struct BspPlane {
pub normal: Vec3,
pub dist: f32,
pub plane_type: i32,
}
#[derive(Clone, Copy, Default)]
pub struct BspNode {
pub children: [i32; 2],
pub first_face: u16,
pub maxs: [i16; 3],
pub mins: [i16; 3],
pub num_faces: u16,
pub plane_id: u32,
}
#[derive(Clone, Copy, Default)]
pub struct BspLeaf {
pub ambient_level: [u8; 4],
pub contents: i32,
pub first_marksurface: u16,
pub maxs: [i16; 3],
pub mins: [i16; 3],
pub num_marksurfaces: u16,
pub visofs: i32,
}
#[derive(Clone, Copy, Default)]
pub struct BspPortal {
pub x0: f32,
pub z0: f32,
pub x1: f32,
pub z1: f32,
pub target_leaf: u32,
}
#[derive(Clone, Default)]
pub struct BspCellPortals {
pub portals: [BspPortal; 4],
pub num_portals: u8,
}
pub struct Bsp {
pub vertices: Vec<BspVertex>,
pub edges: Vec<BspEdge>,
pub surfedges: Vec<i32>,
pub planes: Vec<BspPlane>,
pub faces: Vec<BspFace>,
pub nodes: Vec<BspNode>,
pub leafs: Vec<BspLeaf>,
pub marksurfaces: Vec<u16>,
pub cell_portals: Vec<BspCellPortals>,
pub visdata: Vec<u8>,
pub vertex_lights: Vec<u32>,
}
impl Bsp {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
edges: Vec::new(),
surfedges: Vec::new(),
planes: Vec::new(),
faces: Vec::new(),
nodes: Vec::new(),
leafs: Vec::new(),
marksurfaces: Vec::new(),
cell_portals: Vec::new(),
visdata: Vec::new(),
vertex_lights: Vec::new(),
}
}
pub fn find_leaf(&self, pos: Vec3) -> i32 {
if self.nodes.is_empty() {
return -1;
}
let mut node_id: i32 = 0;
while node_id >= 0 {
let node = &self.nodes[node_id as usize];
let plane = &self.planes[node.plane_id as usize];
let dist = pos.dot(plane.normal) - plane.dist;
node_id = node.children[if dist < 0.0 { 1 } else { 0 }];
}
-(node_id + 1)
}
pub fn point_solid(&self, pos: Vec3) -> bool {
let leaf = self.find_leaf(pos);
if leaf < 0 || leaf as usize >= self.leafs.len() {
return true;
}
self.leafs[leaf as usize].contents == -1
}
fn point_clear(&self, x: f32, y: f32, z: f32, radius: f32) -> bool {
if self.point_solid(Vec3::new(x, y, z)) { return false; }
if self.point_solid(Vec3::new(x + radius, y, z)) { return false; }
if self.point_solid(Vec3::new(x - radius, y, z)) { return false; }
if self.point_solid(Vec3::new(x, y, z + radius)) { return false; }
if self.point_solid(Vec3::new(x, y, z - radius)) { return false; }
true
}
pub fn trace(&self, from: Vec3, to: Vec3, radius: f32) -> Vec3 {
if self.nodes.is_empty() {
return to;
}
if self.point_clear(to.x, to.y, to.z, radius) {
return to;
}
let x_ok = self.point_clear(to.x, from.y, from.z, radius);
let z_ok = self.point_clear(from.x, from.y, to.z, radius);
if x_ok && z_ok {
let dx = to.x - from.x;
let dz = to.z - from.z;
if dx * dx > dz * dz {
Vec3::new(to.x, from.y, from.z)
} else {
Vec3::new(from.x, from.y, to.z)
}
} else if x_ok {
Vec3::new(to.x, from.y, from.z)
} else if z_ok {
Vec3::new(from.x, from.y, to.z)
} else {
from
}
}
}
impl Default for Bsp {
fn default() -> Self {
Self::new()
}
}

55
pxl8d/src/chunk.rs Normal file
View file

@ -0,0 +1,55 @@
extern crate alloc;
pub mod stream;
use crate::bsp::Bsp;
use crate::math::Vec3;
use crate::voxel::VoxelChunk;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum ChunkId {
Bsp(u32),
Vxl(i32, i32, i32),
}
pub enum Chunk {
Bsp { id: u32, bsp: Bsp, version: u32 },
Vxl { cx: i32, cy: i32, cz: i32, data: VoxelChunk, version: u32 },
}
impl Chunk {
pub fn id(&self) -> ChunkId {
match self {
Chunk::Bsp { id, .. } => ChunkId::Bsp(*id),
Chunk::Vxl { cx, cy, cz, .. } => ChunkId::Vxl(*cx, *cy, *cz),
}
}
pub fn version(&self) -> u32 {
match self {
Chunk::Bsp { version, .. } => *version,
Chunk::Vxl { version, .. } => *version,
}
}
pub fn trace(&self, from: Vec3, to: Vec3, radius: f32) -> Vec3 {
match self {
Chunk::Bsp { bsp, .. } => bsp.trace(from, to, radius),
Chunk::Vxl { data, .. } => data.trace(from, to, radius),
}
}
pub fn as_bsp(&self) -> Option<&Bsp> {
match self {
Chunk::Bsp { bsp, .. } => Some(bsp),
_ => None,
}
}
pub fn as_vxl(&self) -> Option<&VoxelChunk> {
match self {
Chunk::Vxl { data, .. } => Some(data),
_ => None,
}
}
}

86
pxl8d/src/chunk/stream.rs Normal file
View file

@ -0,0 +1,86 @@
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::chunk::ChunkId;
use crate::math::Vec3;
use crate::transport::ChunkMessage;
use crate::voxel::VoxelWorld;
pub struct ClientChunkState {
known: BTreeMap<ChunkId, u32>,
pending: Vec<ChunkId>,
pending_messages: Vec<ChunkMessage>,
}
impl ClientChunkState {
pub fn new() -> Self {
Self {
known: BTreeMap::new(),
pending: Vec::new(),
pending_messages: Vec::new(),
}
}
pub fn request(&mut self, id: ChunkId) {
if !self.known.contains_key(&id) && !self.pending.contains(&id) {
self.pending.push(id);
}
}
pub fn request_vxl_radius(&mut self, pos: Vec3, radius: i32, world: &VoxelWorld) {
let cx = VoxelWorld::world_to_chunk(pos.x);
let cy = VoxelWorld::world_to_chunk(pos.y);
let cz = VoxelWorld::world_to_chunk(pos.z);
for dz in -radius..=radius {
for dy in -radius..=radius {
for dx in -radius..=radius {
if world.get_chunk(cx + dx, cy + dy, cz + dz).is_some() {
self.request(ChunkId::Vxl(cx + dx, cy + dy, cz + dz));
}
}
}
}
}
pub fn next_pending(&mut self) -> Option<ChunkId> {
self.pending.pop()
}
pub fn queue_message(&mut self, msg: ChunkMessage) {
self.pending_messages.push(msg);
}
pub fn queue_messages(&mut self, msgs: Vec<ChunkMessage>) {
self.pending_messages.extend(msgs);
}
pub fn next_message(&mut self) -> Option<ChunkMessage> {
if self.pending_messages.is_empty() {
None
} else {
Some(self.pending_messages.remove(0))
}
}
pub fn mark_sent(&mut self, id: ChunkId, version: u32) {
self.known.insert(id, version);
}
pub fn has_pending(&self) -> bool {
!self.pending.is_empty() || !self.pending_messages.is_empty()
}
pub fn clear_pending(&mut self) {
self.pending.clear();
self.pending_messages.clear();
}
}
impl Default for ClientChunkState {
fn default() -> Self {
Self::new()
}
}

40
pxl8d/src/lib.rs Normal file
View file

@ -0,0 +1,40 @@
#![no_std]
extern crate alloc;
mod allocator;
pub mod bsp;
pub mod chunk;
pub mod log;
pub mod math;
pub mod procgen;
pub mod sim;
pub mod transport;
pub mod voxel;
pub mod world;
use core::panic::PanicInfo;
#[global_allocator]
static ALLOCATOR: allocator::Allocator = allocator::Allocator;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[allow(dead_code, non_camel_case_types, non_snake_case, non_upper_case_globals)]
pub mod protocol {
include!(concat!(env!("OUT_DIR"), "/protocol.rs"));
}
pub use bsp::*;
pub use chunk::*;
pub use chunk::stream::*;
pub use math::*;
pub use procgen::{ProcgenParams, generate, generate_rooms};
pub use protocol::*;
pub use sim::*;
pub use transport::*;
pub use voxel::*;
pub use world::*;

139
pxl8d/src/log.rs Normal file
View file

@ -0,0 +1,139 @@
use crate::protocol::{pxl8_log, pxl8_log_init};
use core::ffi::c_char;
static mut G_LOG: pxl8_log = pxl8_log {
handler: None,
level: crate::protocol::pxl8_log_level::PXL8_LOG_LEVEL_DEBUG,
};
pub fn init() {
unsafe {
pxl8_log_init(&raw mut G_LOG);
}
}
#[macro_export]
macro_rules! pxl8_debug {
($($arg:tt)*) => {{
use ::core::fmt::Write;
let mut buf = $crate::log::LogBuffer::new();
let _ = ::core::write!(&mut buf, $($arg)*);
buf.push(0);
unsafe {
$crate::protocol::pxl8_log_write_debug(
concat!(file!(), "\0").as_ptr() as *const ::core::ffi::c_char,
line!() as ::core::ffi::c_int,
c"%s".as_ptr() as *const ::core::ffi::c_char,
buf.as_ptr(),
);
}
}};
}
#[macro_export]
macro_rules! pxl8_error {
($($arg:tt)*) => {{
use ::core::fmt::Write;
let mut buf = $crate::log::LogBuffer::new();
let _ = ::core::write!(&mut buf, $($arg)*);
buf.push(0);
unsafe {
$crate::protocol::pxl8_log_write_error(
concat!(file!(), "\0").as_ptr() as *const ::core::ffi::c_char,
line!() as ::core::ffi::c_int,
c"%s".as_ptr() as *const ::core::ffi::c_char,
buf.as_ptr(),
);
}
}};
}
#[macro_export]
macro_rules! pxl8_info {
($($arg:tt)*) => {{
use ::core::fmt::Write;
let mut buf = $crate::log::LogBuffer::new();
let _ = ::core::write!(&mut buf, $($arg)*);
buf.push(0);
unsafe {
$crate::protocol::pxl8_log_write_info(
c"%s".as_ptr() as *const ::core::ffi::c_char,
buf.as_ptr(),
);
}
}};
}
#[macro_export]
macro_rules! pxl8_trace {
($($arg:tt)*) => {{
use ::core::fmt::Write;
let mut buf = $crate::log::LogBuffer::new();
let _ = ::core::write!(&mut buf, $($arg)*);
buf.push(0);
unsafe {
$crate::protocol::pxl8_log_write_trace(
concat!(file!(), "\0").as_ptr() as *const ::core::ffi::c_char,
line!() as ::core::ffi::c_int,
c"%s".as_ptr() as *const ::core::ffi::c_char,
buf.as_ptr(),
);
}
}};
}
#[macro_export]
macro_rules! pxl8_warn {
($($arg:tt)*) => {{
use ::core::fmt::Write;
let mut buf = $crate::log::LogBuffer::new();
let _ = ::core::write!(&mut buf, $($arg)*);
buf.push(0);
unsafe {
$crate::protocol::pxl8_log_write_warn(
concat!(file!(), "\0").as_ptr() as *const ::core::ffi::c_char,
line!() as ::core::ffi::c_int,
c"%s".as_ptr() as *const ::core::ffi::c_char,
buf.as_ptr(),
);
}
}};
}
pub struct LogBuffer {
buf: [u8; 1024],
pos: usize,
}
impl LogBuffer {
pub const fn new() -> Self {
Self {
buf: [0; 1024],
pos: 0,
}
}
pub fn as_ptr(&self) -> *const c_char {
self.buf.as_ptr() as *const c_char
}
pub fn push(&mut self, b: u8) {
if self.pos < self.buf.len() {
self.buf[self.pos] = b;
self.pos += 1;
}
}
}
impl core::fmt::Write for LogBuffer {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
for b in s.bytes() {
if self.pos >= self.buf.len() - 1 {
break;
}
self.buf[self.pos] = b;
self.pos += 1;
}
Ok(())
}
}

325
pxl8d/src/main.rs Normal file
View file

@ -0,0 +1,325 @@
#![no_std]
#![no_main]
extern crate alloc;
use pxl8d::*;
use pxl8d::chunk::ChunkId;
use pxl8d::chunk::stream::ClientChunkState;
const TICK_RATE: u64 = 30;
const TICK_NS: u64 = 1_000_000_000 / TICK_RATE;
#[cfg(unix)]
fn get_time_ns() -> u64 {
let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
(ts.tv_sec as u64) * 1_000_000_000 + (ts.tv_nsec as u64)
}
#[cfg(windows)]
fn get_time_ns() -> u64 {
use windows_sys::Win32::System::Performance::*;
static mut FREQ: i64 = 0;
unsafe {
if FREQ == 0 {
QueryPerformanceFrequency(&mut FREQ);
}
let mut count: i64 = 0;
QueryPerformanceCounter(&mut count);
((count as u128 * 1_000_000_000) / FREQ as u128) as u64
}
}
#[cfg(unix)]
fn sleep_ms(ms: u64) {
let ts = libc::timespec {
tv_sec: (ms / 1000) as i64,
tv_nsec: ((ms % 1000) * 1_000_000) as i64,
};
unsafe { libc::nanosleep(&ts, core::ptr::null_mut()) };
}
#[cfg(windows)]
fn sleep_ms(ms: u64) {
use windows_sys::Win32::System::Threading::Sleep;
unsafe { Sleep(ms as u32) };
}
fn extract_spawn_position(payload: &[u8]) -> (f32, f32, f32, f32, f32) {
let x = f32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let y = f32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
let z = f32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
let yaw = f32::from_be_bytes([payload[12], payload[13], payload[14], payload[15]]);
let pitch = f32::from_be_bytes([payload[16], payload[17], payload[18], payload[19]]);
(x, y, z, yaw, pitch)
}
#[unsafe(no_mangle)]
pub extern "C" fn main(_argc: i32, _argv: *const *const u8) -> i32 {
log::init();
let mut transport = match transport::Transport::bind(transport::DEFAULT_PORT) {
Some(t) => {
pxl8_info!("[SERVER] Bound to port 7777");
t
}
None => {
pxl8_error!("[SERVER] Failed to bind to port 7777");
return 1;
}
};
let mut sim = Simulation::new();
let mut player_id: Option<u64> = None;
let mut last_client_tick: u64 = 0;
let mut client_chunks = ClientChunkState::new();
let mut sequence: u32 = 0;
let mut last_tick = get_time_ns();
let mut entities_buf = [protocol::pxl8_entity_state {
entity_id: 0,
userdata: [0u8; 56],
}; 64];
let mut inputs_buf: [protocol::pxl8_input_msg; 16] = unsafe { core::mem::zeroed() };
pxl8_debug!("[SERVER] Entering main loop");
loop {
let now = get_time_ns();
let elapsed = now.saturating_sub(last_tick);
if elapsed >= TICK_NS {
last_tick = now;
let dt = (elapsed as f32) / 1_000_000_000.0;
let mut latest_input: Option<protocol::pxl8_input_msg> = None;
while let Some(msg_type) = transport.recv() {
match msg_type {
x if x == protocol::pxl8_msg_type::PXL8_MSG_INPUT as u8 => {
latest_input = Some(transport.get_input());
}
x if x == protocol::pxl8_msg_type::PXL8_MSG_COMMAND as u8 => {
let cmd = transport.get_command();
if cmd.cmd_type == protocol::pxl8_cmd_type::PXL8_CMD_SPAWN_ENTITY as u16 {
let (x, y, z, _yaw, _pitch) = extract_spawn_position(&cmd.payload);
player_id = Some(sim.spawn_player(x, y, z) as u64);
pxl8_debug!("[SERVER] Spawn command received, generating BSP...");
sim.world.generate_bsp(1, None);
sim.world.set_active(ChunkId::Bsp(1));
client_chunks.request(ChunkId::Bsp(1));
pxl8_debug!("[SERVER] Sending CHUNK_ENTER for BSP id=1");
transport.send_chunk_enter(1, transport::CHUNK_TYPE_BSP, sequence);
sequence = sequence.wrapping_add(1);
}
}
_ => {}
}
}
if let Some(input) = latest_input {
last_client_tick = input.tick;
inputs_buf[0] = input;
sim.step(&inputs_buf[..1], dt);
} else {
sim.step(&[], dt);
}
let mut count = 0;
sim.generate_snapshot(player_id.unwrap_or(u64::MAX), |state| {
if count < entities_buf.len() {
entities_buf[count] = *state;
count += 1;
}
});
let header = protocol::pxl8_snapshot_header {
entity_count: count as u16,
event_count: 0,
player_id: player_id.unwrap_or(0),
tick: last_client_tick,
time: sim.time,
};
transport.send_snapshot(&header, &entities_buf[..count], sequence);
sequence = sequence.wrapping_add(1);
if let Some(pid) = player_id {
if let Some(player) = sim.get_player_position(pid) {
let pos = Vec3 { x: player.0, y: player.1, z: player.2 };
if sim.world.active().is_some() {
while let Some(chunk_id) = client_chunks.next_pending() {
match chunk_id {
ChunkId::Bsp(id) => {
pxl8_debug!("[SERVER] Processing pending BSP chunk");
if let Some(chunk) = sim.world.get(&chunk_id) {
if let Some(bsp) = chunk.as_bsp() {
let msgs = bsp_to_messages(bsp, id, chunk.version());
pxl8_debug!("[SERVER] BSP serialized, queueing messages");
client_chunks.queue_messages(msgs);
client_chunks.mark_sent(chunk_id, chunk.version());
}
}
}
ChunkId::Vxl(cx, cy, cz) => {
if let Some(chunk) = sim.voxels.get_chunk(cx, cy, cz) {
let msgs = transport::ChunkMessage::from_voxel(chunk, 1);
client_chunks.queue_messages(msgs);
client_chunks.mark_sent(chunk_id, 1);
}
}
}
}
for _ in 0..4 {
if let Some(msg) = client_chunks.next_message() {
transport.send_chunk(&msg, sequence);
sequence = sequence.wrapping_add(1);
}
}
} else {
client_chunks.request_vxl_radius(pos, 2, &sim.voxels);
for _ in 0..2 {
if let Some(chunk_id) = client_chunks.next_pending() {
if let ChunkId::Vxl(cx, cy, cz) = chunk_id {
if let Some(chunk) = sim.voxels.get_chunk(cx, cy, cz) {
let msgs = transport::ChunkMessage::from_voxel(chunk, 1);
for msg in &msgs {
transport.send_chunk(msg, sequence);
sequence = sequence.wrapping_add(1);
}
client_chunks.mark_sent(chunk_id, 1);
}
}
}
}
}
}
}
}
sleep_ms(1);
}
}
fn bsp_to_messages(bsp: &bsp::Bsp, id: u32, version: u32) -> alloc::vec::Vec<transport::ChunkMessage> {
use alloc::vec::Vec;
pxl8_debug!("[SERVER] bsp_to_messages: serializing BSP");
let mut data = Vec::new();
let num_verts = bsp.vertices.len();
let num_edges = bsp.edges.len();
let num_faces = bsp.faces.len();
let num_planes = bsp.planes.len();
let num_nodes = bsp.nodes.len();
let num_leafs = bsp.leafs.len();
let num_surfedges = bsp.surfedges.len();
data.extend_from_slice(&(num_verts as u32).to_be_bytes());
data.extend_from_slice(&(num_edges as u32).to_be_bytes());
data.extend_from_slice(&(num_faces as u32).to_be_bytes());
data.extend_from_slice(&(num_planes as u32).to_be_bytes());
data.extend_from_slice(&(num_nodes as u32).to_be_bytes());
data.extend_from_slice(&(num_leafs as u32).to_be_bytes());
data.extend_from_slice(&(num_surfedges as u32).to_be_bytes());
data.extend_from_slice(&(bsp.marksurfaces.len() as u32).to_be_bytes());
data.extend_from_slice(&(bsp.cell_portals.len() as u32).to_be_bytes());
data.extend_from_slice(&(bsp.visdata.len() as u32).to_be_bytes());
data.extend_from_slice(&(bsp.vertex_lights.len() as u32).to_be_bytes());
for v in &bsp.vertices {
data.extend_from_slice(&v.position.x.to_be_bytes());
data.extend_from_slice(&v.position.y.to_be_bytes());
data.extend_from_slice(&v.position.z.to_be_bytes());
}
for e in &bsp.edges {
data.extend_from_slice(&e.vertex[0].to_be_bytes());
data.extend_from_slice(&e.vertex[1].to_be_bytes());
}
for se in &bsp.surfedges {
data.extend_from_slice(&se.to_be_bytes());
}
for p in &bsp.planes {
data.extend_from_slice(&p.normal.x.to_be_bytes());
data.extend_from_slice(&p.normal.y.to_be_bytes());
data.extend_from_slice(&p.normal.z.to_be_bytes());
data.extend_from_slice(&p.dist.to_be_bytes());
data.extend_from_slice(&p.plane_type.to_be_bytes());
}
for f in &bsp.faces {
data.extend_from_slice(&f.first_edge.to_be_bytes());
data.extend_from_slice(&f.lightmap_offset.to_be_bytes());
data.extend_from_slice(&f.num_edges.to_be_bytes());
data.extend_from_slice(&f.plane_id.to_be_bytes());
data.extend_from_slice(&f.side.to_be_bytes());
data.extend_from_slice(&f.styles);
data.extend_from_slice(&f.material_id.to_be_bytes());
data.extend_from_slice(&f.aabb_min.x.to_be_bytes());
data.extend_from_slice(&f.aabb_min.y.to_be_bytes());
data.extend_from_slice(&f.aabb_min.z.to_be_bytes());
data.extend_from_slice(&f.aabb_max.x.to_be_bytes());
data.extend_from_slice(&f.aabb_max.y.to_be_bytes());
data.extend_from_slice(&f.aabb_max.z.to_be_bytes());
}
for n in &bsp.nodes {
data.extend_from_slice(&n.children[0].to_be_bytes());
data.extend_from_slice(&n.children[1].to_be_bytes());
data.extend_from_slice(&n.first_face.to_be_bytes());
data.extend_from_slice(&n.maxs[0].to_be_bytes());
data.extend_from_slice(&n.maxs[1].to_be_bytes());
data.extend_from_slice(&n.maxs[2].to_be_bytes());
data.extend_from_slice(&n.mins[0].to_be_bytes());
data.extend_from_slice(&n.mins[1].to_be_bytes());
data.extend_from_slice(&n.mins[2].to_be_bytes());
data.extend_from_slice(&n.num_faces.to_be_bytes());
data.extend_from_slice(&n.plane_id.to_be_bytes());
}
for l in &bsp.leafs {
data.extend_from_slice(&l.ambient_level);
data.extend_from_slice(&l.contents.to_be_bytes());
data.extend_from_slice(&l.first_marksurface.to_be_bytes());
data.extend_from_slice(&l.maxs[0].to_be_bytes());
data.extend_from_slice(&l.maxs[1].to_be_bytes());
data.extend_from_slice(&l.maxs[2].to_be_bytes());
data.extend_from_slice(&l.mins[0].to_be_bytes());
data.extend_from_slice(&l.mins[1].to_be_bytes());
data.extend_from_slice(&l.mins[2].to_be_bytes());
data.extend_from_slice(&l.num_marksurfaces.to_be_bytes());
data.extend_from_slice(&l.visofs.to_be_bytes());
}
for ms in &bsp.marksurfaces {
data.extend_from_slice(&ms.to_be_bytes());
}
for cp in &bsp.cell_portals {
data.push(cp.num_portals);
data.push(0);
data.push(0);
data.push(0);
for i in 0..4 {
data.extend_from_slice(&cp.portals[i].x0.to_be_bytes());
data.extend_from_slice(&cp.portals[i].z0.to_be_bytes());
data.extend_from_slice(&cp.portals[i].x1.to_be_bytes());
data.extend_from_slice(&cp.portals[i].z1.to_be_bytes());
data.extend_from_slice(&cp.portals[i].target_leaf.to_be_bytes());
}
}
data.extend_from_slice(&bsp.visdata);
for vl in &bsp.vertex_lights {
data.extend_from_slice(&vl.to_be_bytes());
}
transport::ChunkMessage::from_bsp(data, id, version)
}

42
pxl8d/src/math.rs Normal file
View file

@ -0,0 +1,42 @@
use core::ops::{Add, Mul, Sub};
#[derive(Clone, Copy, Default)]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vec3 {
pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0 };
pub const Y: Self = Self { x: 0.0, y: 1.0, z: 0.0 };
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
pub fn dot(self, rhs: Self) -> f32 {
self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
}
}
impl Add for Vec3 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
}
}
impl Sub for Vec3 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
}
}
impl Mul<f32> for Vec3 {
type Output = Self;
fn mul(self, rhs: f32) -> Self {
Self::new(self.x * rhs, self.y * rhs, self.z * rhs)
}
}

806
pxl8d/src/procgen.rs Normal file
View file

@ -0,0 +1,806 @@
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use libm::sqrtf;
use crate::bsp::{Bsp, BspVertex, BspEdge, BspFace, BspPlane, BspNode, BspLeaf, BspPortal, BspCellPortals};
use crate::math::Vec3;
pub const CELL_SIZE: f32 = 64.0;
pub const WALL_HEIGHT: f32 = 128.0;
pub const PVS_MAX_DEPTH: u32 = 64;
#[derive(Clone, Copy)]
pub struct LightSource {
pub position: Vec3,
pub intensity: f32,
pub radius: f32,
}
#[derive(Clone, Copy)]
pub struct ProcgenParams {
pub width: i32,
pub height: i32,
pub seed: u32,
pub min_room_size: i32,
pub max_room_size: i32,
pub num_rooms: i32,
}
impl Default for ProcgenParams {
fn default() -> Self {
Self {
width: 16,
height: 16,
seed: 12345,
min_room_size: 3,
max_room_size: 6,
num_rooms: 8,
}
}
}
struct RoomGrid {
cells: Vec<u8>,
width: i32,
height: i32,
}
impl RoomGrid {
fn new(width: i32, height: i32) -> Self {
Self {
cells: vec![0; (width * height) as usize],
width,
height,
}
}
fn get(&self, x: i32, y: i32) -> u8 {
if x < 0 || x >= self.width || y < 0 || y >= self.height {
return 1;
}
self.cells[(y * self.width + x) as usize]
}
fn set(&mut self, x: i32, y: i32, value: u8) {
if x >= 0 && x < self.width && y >= 0 && y < self.height {
self.cells[(y * self.width + x) as usize] = value;
}
}
fn fill(&mut self, value: u8) {
for cell in &mut self.cells {
*cell = value;
}
}
}
struct Rng {
state: u32,
}
impl Rng {
fn new(seed: u32) -> Self {
Self { state: seed }
}
fn next(&mut self) -> u32 {
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
(self.state >> 16) & 0x7FFF
}
}
#[derive(Clone, Copy)]
struct Bounds {
x: i32,
y: i32,
w: i32,
h: i32,
}
impl Bounds {
fn intersects(&self, other: &Bounds) -> bool {
!(self.x + self.w <= other.x || other.x + other.w <= self.x ||
self.y + self.h <= other.y || other.y + other.h <= self.y)
}
}
struct BspBuildContext<'a> {
bsp: &'a mut Bsp,
grid: &'a RoomGrid,
node_count: u32,
plane_offset: u32,
}
fn carve_corridor_h(grid: &mut RoomGrid, x1: i32, x2: i32, y: i32) {
let start = x1.min(x2);
let end = x1.max(x2);
for x in start..=end {
grid.set(x, y, 0);
grid.set(x, y - 1, 0);
grid.set(x, y + 1, 0);
}
}
fn carve_corridor_v(grid: &mut RoomGrid, y1: i32, y2: i32, x: i32) {
let start = y1.min(y2);
let end = y1.max(y2);
for y in start..=end {
grid.set(x, y, 0);
grid.set(x - 1, y, 0);
grid.set(x + 1, y, 0);
}
}
fn compute_face_aabb(face: &mut BspFace, verts: &[BspVertex], vert_idx: usize) {
face.aabb_min = Vec3::new(1e30, 1e30, 1e30);
face.aabb_max = Vec3::new(-1e30, -1e30, -1e30);
for i in 0..4 {
let v = verts[vert_idx + i].position;
if v.x < face.aabb_min.x { face.aabb_min.x = v.x; }
if v.x > face.aabb_max.x { face.aabb_max.x = v.x; }
if v.y < face.aabb_min.y { face.aabb_min.y = v.y; }
if v.y > face.aabb_max.y { face.aabb_max.y = v.y; }
if v.z < face.aabb_min.z { face.aabb_min.z = v.z; }
if v.z > face.aabb_max.z { face.aabb_max.z = v.z; }
}
}
fn build_bsp_node(ctx: &mut BspBuildContext, x0: i32, y0: i32, x1: i32, y1: i32, depth: i32) -> i32 {
if x1 - x0 == 1 && y1 - y0 == 1 {
let leaf_idx = y0 * ctx.grid.width + x0;
return -(leaf_idx + 1);
}
let node_idx = ctx.node_count;
ctx.node_count += 1;
let plane_idx = ctx.plane_offset;
ctx.plane_offset += 1;
if depth % 2 == 0 {
let mid_x = (x0 + x1) / 2;
let split_pos = mid_x as f32 * CELL_SIZE;
ctx.bsp.planes[plane_idx as usize] = BspPlane {
normal: Vec3::new(1.0, 0.0, 0.0),
dist: split_pos,
plane_type: 0,
};
let child0 = build_bsp_node(ctx, mid_x, y0, x1, y1, depth + 1);
let child1 = build_bsp_node(ctx, x0, y0, mid_x, y1, depth + 1);
ctx.bsp.nodes[node_idx as usize] = BspNode {
plane_id: plane_idx,
children: [child0, child1],
..Default::default()
};
} else {
let mid_y = (y0 + y1) / 2;
let split_pos = mid_y as f32 * CELL_SIZE;
ctx.bsp.planes[plane_idx as usize] = BspPlane {
normal: Vec3::new(0.0, 0.0, 1.0),
dist: split_pos,
plane_type: 0,
};
let child0 = build_bsp_node(ctx, x0, mid_y, x1, y1, depth + 1);
let child1 = build_bsp_node(ctx, x0, y0, x1, mid_y, depth + 1);
ctx.bsp.nodes[node_idx as usize] = BspNode {
plane_id: plane_idx,
children: [child0, child1],
..Default::default()
};
}
node_idx as i32
}
fn build_cell_portals(grid: &RoomGrid) -> Vec<BspCellPortals> {
let total_cells = (grid.width * grid.height) as usize;
let mut portals = vec![BspCellPortals::default(); total_cells];
for y in 0..grid.height {
for x in 0..grid.width {
if grid.get(x, y) != 0 {
continue;
}
let c = (y * grid.width + x) as usize;
let cx = x as f32 * CELL_SIZE;
let cz = y as f32 * CELL_SIZE;
if x > 0 && grid.get(x - 1, y) == 0 {
let p = &mut portals[c];
let idx = p.num_portals as usize;
p.portals[idx] = BspPortal {
x0: cx,
z0: cz,
x1: cx,
z1: cz + CELL_SIZE,
target_leaf: (y * grid.width + (x - 1)) as u32,
};
p.num_portals += 1;
}
if x < grid.width - 1 && grid.get(x + 1, y) == 0 {
let p = &mut portals[c];
let idx = p.num_portals as usize;
p.portals[idx] = BspPortal {
x0: cx + CELL_SIZE,
z0: cz + CELL_SIZE,
x1: cx + CELL_SIZE,
z1: cz,
target_leaf: (y * grid.width + (x + 1)) as u32,
};
p.num_portals += 1;
}
if y > 0 && grid.get(x, y - 1) == 0 {
let p = &mut portals[c];
let idx = p.num_portals as usize;
p.portals[idx] = BspPortal {
x0: cx + CELL_SIZE,
z0: cz,
x1: cx,
z1: cz,
target_leaf: ((y - 1) * grid.width + x) as u32,
};
p.num_portals += 1;
}
if y < grid.height - 1 && grid.get(x, y + 1) == 0 {
let p = &mut portals[c];
let idx = p.num_portals as usize;
p.portals[idx] = BspPortal {
x0: cx,
z0: cz + CELL_SIZE,
x1: cx + CELL_SIZE,
z1: cz + CELL_SIZE,
target_leaf: ((y + 1) * grid.width + x) as u32,
};
p.num_portals += 1;
}
}
}
portals
}
#[derive(Clone, Copy)]
struct FloodEntry {
leaf: u32,
depth: u32,
}
fn portal_flood_bfs(
start_leaf: u32,
portals: &[BspCellPortals],
leafs: &[BspLeaf],
pvs: &mut [u8],
num_leafs: u32,
) {
let pvs_bytes = ((num_leafs + 7) / 8) as usize;
let mut visited = vec![0u8; pvs_bytes];
let mut queue = Vec::with_capacity(num_leafs as usize);
pvs[(start_leaf >> 3) as usize] |= 1 << (start_leaf & 7);
visited[(start_leaf >> 3) as usize] |= 1 << (start_leaf & 7);
queue.push(FloodEntry { leaf: start_leaf, depth: 0 });
let mut head = 0;
while head < queue.len() {
let e = queue[head];
head += 1;
if e.depth >= PVS_MAX_DEPTH {
continue;
}
if leafs[e.leaf as usize].contents == -1 {
continue;
}
let cp = &portals[e.leaf as usize];
for i in 0..cp.num_portals {
let target = cp.portals[i as usize].target_leaf;
let byte = (target >> 3) as usize;
let bit = target & 7;
if visited[byte] & (1 << bit) != 0 {
continue;
}
visited[byte] |= 1 << bit;
if leafs[target as usize].contents == -1 {
continue;
}
pvs[byte] |= 1 << bit;
queue.push(FloodEntry { leaf: target, depth: e.depth + 1 });
}
}
}
fn compute_leaf_pvs(
start_leaf: u32,
portals: &[BspCellPortals],
leafs: &[BspLeaf],
num_leafs: u32,
) -> Vec<u8> {
let pvs_bytes = ((num_leafs + 7) / 8) as usize;
let mut pvs = vec![0u8; pvs_bytes];
portal_flood_bfs(start_leaf, portals, leafs, &mut pvs, num_leafs);
pvs
}
fn rle_compress_pvs(pvs: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut i = 0;
while i < pvs.len() {
if pvs[i] != 0 {
out.push(pvs[i]);
i += 1;
} else {
let mut count = 0u8;
while i < pvs.len() && pvs[i] == 0 && count < 255 {
count += 1;
i += 1;
}
out.push(0);
out.push(count);
}
}
out
}
fn build_pvs_data(bsp: &mut Bsp, portals: &[BspCellPortals]) {
let num_leafs = bsp.leafs.len() as u32;
let mut visdata = Vec::new();
for leaf in 0..num_leafs {
if bsp.leafs[leaf as usize].contents == -1 {
bsp.leafs[leaf as usize].visofs = -1;
continue;
}
let pvs = compute_leaf_pvs(leaf, portals, &bsp.leafs, num_leafs);
let compressed = rle_compress_pvs(&pvs);
bsp.leafs[leaf as usize].visofs = visdata.len() as i32;
visdata.extend_from_slice(&compressed);
}
bsp.visdata = visdata;
}
fn compute_vertex_light(
pos: Vec3,
normal: Vec3,
lights: &[LightSource],
ambient: f32,
) -> f32 {
let mut total = ambient;
for light in lights {
let to_light = Vec3::new(
light.position.x - pos.x,
light.position.y - pos.y,
light.position.z - pos.z,
);
let dist_sq = to_light.x * to_light.x + to_light.y * to_light.y + to_light.z * to_light.z;
let dist = sqrtf(dist_sq).max(1.0);
if dist > light.radius {
continue;
}
let inv_dist = 1.0 / dist;
let light_dir = Vec3::new(
to_light.x * inv_dist,
to_light.y * inv_dist,
to_light.z * inv_dist,
);
let ndotl = (normal.x * light_dir.x + normal.y * light_dir.y + normal.z * light_dir.z).max(0.0);
let attenuation = (1.0 - dist / light.radius).max(0.0);
let attenuation = attenuation * attenuation;
total += light.intensity * ndotl * attenuation;
}
total.min(1.0)
}
fn compute_bsp_vertex_lighting(bsp: &mut Bsp, lights: &[LightSource], ambient: f32) {
if bsp.vertices.is_empty() {
return;
}
bsp.vertex_lights = vec![0u32; bsp.vertices.len()];
for f in 0..bsp.faces.len() {
let face = &bsp.faces[f];
let normal = if (face.plane_id as usize) < bsp.planes.len() {
bsp.planes[face.plane_id as usize].normal
} else {
Vec3::new(0.0, 1.0, 0.0)
};
for e in 0..face.num_edges {
let surfedge_idx = (face.first_edge + e as u32) as usize;
if surfedge_idx >= bsp.surfedges.len() {
continue;
}
let edge_idx = bsp.surfedges[surfedge_idx];
let vert_idx = if edge_idx >= 0 {
let ei = edge_idx as usize;
if ei >= bsp.edges.len() { continue; }
bsp.edges[ei].vertex[0] as usize
} else {
let ei = (-edge_idx) as usize;
if ei >= bsp.edges.len() { continue; }
bsp.edges[ei].vertex[1] as usize
};
if vert_idx >= bsp.vertices.len() {
continue;
}
let pos = bsp.vertices[vert_idx].position;
let light = compute_vertex_light(pos, normal, lights, ambient);
let light_byte = (light * 255.0) as u8;
bsp.vertex_lights[vert_idx] = ((light_byte as u32) << 24) | 0x00FFFFFF;
}
}
}
fn grid_to_bsp(bsp: &mut Bsp, grid: &RoomGrid) {
let mut face_count = 0;
let mut floor_ceiling_count = 0;
for y in 0..grid.height {
for x in 0..grid.width {
if grid.get(x, y) == 0 {
if grid.get(x - 1, y) == 1 { face_count += 1; }
if grid.get(x + 1, y) == 1 { face_count += 1; }
if grid.get(x, y - 1) == 1 { face_count += 1; }
if grid.get(x, y + 1) == 1 { face_count += 1; }
floor_ceiling_count += 1;
}
}
}
face_count += floor_ceiling_count;
let vertex_count = face_count * 4;
let total_cells = (grid.width * grid.height) as usize;
let max_nodes = 2 * total_cells;
let total_planes = face_count + max_nodes;
bsp.vertices = vec![BspVertex::default(); vertex_count];
bsp.faces = vec![BspFace::default(); face_count];
bsp.planes = vec![BspPlane::default(); total_planes];
bsp.edges = vec![BspEdge::default(); vertex_count];
bsp.surfedges = vec![0i32; vertex_count];
bsp.nodes = vec![BspNode::default(); max_nodes];
let mut face_cell = vec![0u32; face_count];
let mut vert_idx = 0usize;
let mut face_idx = 0usize;
let mut edge_idx = 0usize;
for y in 0..grid.height {
for x in 0..grid.width {
if grid.get(x, y) == 0 {
let fx = x as f32 * CELL_SIZE;
let fy = y as f32 * CELL_SIZE;
let cell_idx = (y * grid.width + x) as u32;
if grid.get(x - 1, y) == 1 {
bsp.vertices[vert_idx + 0].position = Vec3::new(fx, 0.0, fy);
bsp.vertices[vert_idx + 1].position = Vec3::new(fx, WALL_HEIGHT, fy);
bsp.vertices[vert_idx + 2].position = Vec3::new(fx, WALL_HEIGHT, fy + CELL_SIZE);
bsp.vertices[vert_idx + 3].position = Vec3::new(fx, 0.0, fy + CELL_SIZE);
bsp.planes[face_idx].normal = Vec3::new(1.0, 0.0, 0.0);
bsp.planes[face_idx].dist = fx;
bsp.faces[face_idx].plane_id = face_idx as u16;
bsp.faces[face_idx].num_edges = 4;
bsp.faces[face_idx].first_edge = edge_idx as u32;
bsp.faces[face_idx].material_id = 0;
for i in 0..4 {
bsp.edges[edge_idx + i].vertex[0] = (vert_idx + i) as u16;
bsp.edges[edge_idx + i].vertex[1] = (vert_idx + ((i + 1) % 4)) as u16;
bsp.surfedges[edge_idx + i] = (edge_idx + i) as i32;
}
compute_face_aabb(&mut bsp.faces[face_idx], &bsp.vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx += 1;
}
if grid.get(x + 1, y) == 1 {
bsp.vertices[vert_idx + 0].position = Vec3::new(fx + CELL_SIZE, 0.0, fy);
bsp.vertices[vert_idx + 1].position = Vec3::new(fx + CELL_SIZE, 0.0, fy + CELL_SIZE);
bsp.vertices[vert_idx + 2].position = Vec3::new(fx + CELL_SIZE, WALL_HEIGHT, fy + CELL_SIZE);
bsp.vertices[vert_idx + 3].position = Vec3::new(fx + CELL_SIZE, WALL_HEIGHT, fy);
bsp.planes[face_idx].normal = Vec3::new(-1.0, 0.0, 0.0);
bsp.planes[face_idx].dist = -(fx + CELL_SIZE);
bsp.faces[face_idx].plane_id = face_idx as u16;
bsp.faces[face_idx].num_edges = 4;
bsp.faces[face_idx].first_edge = edge_idx as u32;
bsp.faces[face_idx].material_id = 0;
for i in 0..4 {
bsp.edges[edge_idx + i].vertex[0] = (vert_idx + i) as u16;
bsp.edges[edge_idx + i].vertex[1] = (vert_idx + ((i + 1) % 4)) as u16;
bsp.surfedges[edge_idx + i] = (edge_idx + i) as i32;
}
compute_face_aabb(&mut bsp.faces[face_idx], &bsp.vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx += 1;
}
if grid.get(x, y - 1) == 1 {
bsp.vertices[vert_idx + 0].position = Vec3::new(fx, 0.0, fy);
bsp.vertices[vert_idx + 1].position = Vec3::new(fx + CELL_SIZE, 0.0, fy);
bsp.vertices[vert_idx + 2].position = Vec3::new(fx + CELL_SIZE, WALL_HEIGHT, fy);
bsp.vertices[vert_idx + 3].position = Vec3::new(fx, WALL_HEIGHT, fy);
bsp.planes[face_idx].normal = Vec3::new(0.0, 0.0, 1.0);
bsp.planes[face_idx].dist = fy;
bsp.faces[face_idx].plane_id = face_idx as u16;
bsp.faces[face_idx].num_edges = 4;
bsp.faces[face_idx].first_edge = edge_idx as u32;
bsp.faces[face_idx].material_id = 0;
for i in 0..4 {
bsp.edges[edge_idx + i].vertex[0] = (vert_idx + i) as u16;
bsp.edges[edge_idx + i].vertex[1] = (vert_idx + ((i + 1) % 4)) as u16;
bsp.surfedges[edge_idx + i] = (edge_idx + i) as i32;
}
compute_face_aabb(&mut bsp.faces[face_idx], &bsp.vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx += 1;
}
if grid.get(x, y + 1) == 1 {
bsp.vertices[vert_idx + 0].position = Vec3::new(fx, 0.0, fy + CELL_SIZE);
bsp.vertices[vert_idx + 1].position = Vec3::new(fx, WALL_HEIGHT, fy + CELL_SIZE);
bsp.vertices[vert_idx + 2].position = Vec3::new(fx + CELL_SIZE, WALL_HEIGHT, fy + CELL_SIZE);
bsp.vertices[vert_idx + 3].position = Vec3::new(fx + CELL_SIZE, 0.0, fy + CELL_SIZE);
bsp.planes[face_idx].normal = Vec3::new(0.0, 0.0, -1.0);
bsp.planes[face_idx].dist = -(fy + CELL_SIZE);
bsp.faces[face_idx].plane_id = face_idx as u16;
bsp.faces[face_idx].num_edges = 4;
bsp.faces[face_idx].first_edge = edge_idx as u32;
bsp.faces[face_idx].material_id = 0;
for i in 0..4 {
bsp.edges[edge_idx + i].vertex[0] = (vert_idx + i) as u16;
bsp.edges[edge_idx + i].vertex[1] = (vert_idx + ((i + 1) % 4)) as u16;
bsp.surfedges[edge_idx + i] = (edge_idx + i) as i32;
}
compute_face_aabb(&mut bsp.faces[face_idx], &bsp.vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx += 1;
}
}
}
}
for y in 0..grid.height {
for x in 0..grid.width {
if grid.get(x, y) == 0 {
let fx = x as f32 * CELL_SIZE;
let fy = y as f32 * CELL_SIZE;
let cell_idx = (y * grid.width + x) as u32;
bsp.vertices[vert_idx + 0].position = Vec3::new(fx, 0.0, fy);
bsp.vertices[vert_idx + 1].position = Vec3::new(fx, 0.0, fy + CELL_SIZE);
bsp.vertices[vert_idx + 2].position = Vec3::new(fx + CELL_SIZE, 0.0, fy + CELL_SIZE);
bsp.vertices[vert_idx + 3].position = Vec3::new(fx + CELL_SIZE, 0.0, fy);
bsp.planes[face_idx].normal = Vec3::new(0.0, 1.0, 0.0);
bsp.planes[face_idx].dist = 0.0;
bsp.faces[face_idx].plane_id = face_idx as u16;
bsp.faces[face_idx].num_edges = 4;
bsp.faces[face_idx].first_edge = edge_idx as u32;
bsp.faces[face_idx].material_id = 0;
for i in 0..4 {
bsp.edges[edge_idx + i].vertex[0] = (vert_idx + i) as u16;
bsp.edges[edge_idx + i].vertex[1] = (vert_idx + ((i + 1) % 4)) as u16;
bsp.surfedges[edge_idx + i] = (edge_idx + i) as i32;
}
compute_face_aabb(&mut bsp.faces[face_idx], &bsp.vertices, vert_idx);
face_cell[face_idx] = cell_idx;
vert_idx += 4;
edge_idx += 4;
face_idx += 1;
}
}
}
bsp.vertices.truncate(vert_idx);
bsp.faces.truncate(face_idx);
bsp.edges.truncate(edge_idx);
bsp.surfedges.truncate(edge_idx);
bsp.leafs = vec![BspLeaf::default(); total_cells];
bsp.marksurfaces = vec![0u16; face_idx];
let mut faces_per_cell = vec![0u32; total_cells];
let mut cell_offset = vec![0u32; total_cells];
let mut cell_cursor = vec![0u32; total_cells];
for i in 0..face_idx {
faces_per_cell[face_cell[i] as usize] += 1;
}
let mut offset = 0u32;
for c in 0..total_cells {
cell_offset[c] = offset;
offset += faces_per_cell[c];
}
for i in 0..face_idx {
let c = face_cell[i] as usize;
bsp.marksurfaces[(cell_offset[c] + cell_cursor[c]) as usize] = i as u16;
cell_cursor[c] += 1;
}
for y in 0..grid.height {
for x in 0..grid.width {
let c = (y * grid.width + x) as usize;
let leaf = &mut bsp.leafs[c];
let fx = x as f32 * CELL_SIZE;
let fz = y as f32 * CELL_SIZE;
leaf.mins[0] = fx as i16;
leaf.mins[1] = 0;
leaf.mins[2] = fz as i16;
leaf.maxs[0] = (fx + CELL_SIZE) as i16;
leaf.maxs[1] = WALL_HEIGHT as i16;
leaf.maxs[2] = (fz + CELL_SIZE) as i16;
if grid.get(x, y) == 0 {
leaf.contents = -2;
leaf.first_marksurface = cell_offset[c] as u16;
leaf.num_marksurfaces = faces_per_cell[c] as u16;
} else {
leaf.contents = -1;
leaf.first_marksurface = 0;
leaf.num_marksurfaces = 0;
}
leaf.visofs = -1;
}
}
let face_count_final = face_idx;
let mut ctx = BspBuildContext {
bsp,
grid,
node_count: 0,
plane_offset: face_count_final as u32,
};
build_bsp_node(&mut ctx, 0, 0, grid.width, grid.height, 0);
let node_count = ctx.node_count as usize;
let plane_count = ctx.plane_offset as usize;
ctx.bsp.nodes.truncate(node_count);
ctx.bsp.planes.truncate(plane_count);
let portals = build_cell_portals(grid);
build_pvs_data(ctx.bsp, &portals);
ctx.bsp.cell_portals = portals;
}
pub fn generate_rooms(params: &ProcgenParams) -> Bsp {
let mut rng = Rng::new(params.seed);
let mut grid = RoomGrid::new(params.width, params.height);
grid.fill(1);
let mut rooms: Vec<Bounds> = Vec::new();
let max_attempts = params.num_rooms * 10;
for _ in 0..max_attempts {
if rooms.len() >= params.num_rooms as usize {
break;
}
let w = params.min_room_size + (rng.next() % (params.max_room_size - params.min_room_size + 1) as u32) as i32;
let h = params.min_room_size + (rng.next() % (params.max_room_size - params.min_room_size + 1) as u32) as i32;
let x = 1 + (rng.next() % (params.width - w - 2) as u32) as i32;
let y = 1 + (rng.next() % (params.height - h - 2) as u32) as i32;
let new_room = Bounds { x, y, w, h };
let overlaps = rooms.iter().any(|r| new_room.intersects(r));
if !overlaps {
for ry in y..(y + h) {
for rx in x..(x + w) {
grid.set(rx, ry, 0);
}
}
if !rooms.is_empty() {
let prev = rooms.last().unwrap();
let new_cx = x + w / 2;
let new_cy = y + h / 2;
let prev_cx = prev.x + prev.w / 2;
let prev_cy = prev.y + prev.h / 2;
if rng.next() % 2 == 0 {
carve_corridor_h(&mut grid, prev_cx, new_cx, prev_cy);
carve_corridor_v(&mut grid, prev_cy, new_cy, new_cx);
} else {
carve_corridor_v(&mut grid, prev_cy, new_cy, prev_cx);
carve_corridor_h(&mut grid, prev_cx, new_cx, new_cy);
}
}
rooms.push(new_room);
}
}
let mut bsp = Bsp::new();
grid_to_bsp(&mut bsp, &grid);
let light_height = 80.0;
let lights: Vec<LightSource> = rooms.iter().map(|room| {
let cx = (room.x as f32 + room.w as f32 / 2.0) * CELL_SIZE;
let cy = (room.y as f32 + room.h as f32 / 2.0) * CELL_SIZE;
LightSource {
position: Vec3::new(cx, light_height, cy),
intensity: 0.8,
radius: 300.0,
}
}).collect();
compute_bsp_vertex_lighting(&mut bsp, &lights, 0.1);
bsp
}
pub fn generate(params: &ProcgenParams) -> Bsp {
generate_rooms(params)
}

277
pxl8d/src/sim.rs Normal file
View file

@ -0,0 +1,277 @@
extern crate alloc;
use alloc::vec::Vec;
use libm::{cosf, sinf, sqrtf};
use crate::math::Vec3;
use crate::protocol::*;
use crate::voxel::VoxelWorld;
use crate::world::World;
const ALIVE: u32 = 1 << 0;
const PLAYER: u32 = 1 << 1;
const GROUNDED: u32 = 1 << 2;
const MAX_ENTITIES: usize = 1024;
#[derive(Clone, Copy)]
pub struct Entity {
pub flags: u32,
pub kind: u16,
pub pos: Vec3,
pub vel: Vec3,
pub yaw: f32,
pub pitch: f32,
}
impl Default for Entity {
fn default() -> Self {
Self {
flags: 0,
kind: 0,
pos: Vec3::ZERO,
vel: Vec3::ZERO,
yaw: 0.0,
pitch: 0.0,
}
}
}
pub struct Simulation {
pub entities: Vec<Entity>,
pub free_list: Vec<u32>,
pub player: Option<u32>,
pub tick: u64,
pub time: f32,
pub voxels: VoxelWorld,
pub world: World,
}
impl Simulation {
pub fn new() -> Self {
let mut entities = Vec::with_capacity(MAX_ENTITIES);
entities.resize(MAX_ENTITIES, Entity::default());
let mut free_list = Vec::with_capacity(MAX_ENTITIES);
for i in (0..MAX_ENTITIES).rev() {
free_list.push(i as u32);
}
Self {
entities,
free_list,
player: None,
tick: 0,
time: 0.0,
voxels: VoxelWorld::new(12345),
world: World::new(12345),
}
}
pub fn spawn(&mut self) -> Option<u32> {
let idx = self.free_list.pop()?;
self.entities[idx as usize] = Entity {
flags: ALIVE,
..Default::default()
};
Some(idx)
}
pub fn despawn(&mut self, id: u32) {
if (id as usize) < self.entities.len() {
self.entities[id as usize].flags = 0;
self.free_list.push(id);
}
}
pub fn spawn_player(&mut self, x: f32, y: f32, z: f32) -> u32 {
let id = self.spawn().unwrap_or(0);
let ent = &mut self.entities[id as usize];
ent.flags = ALIVE | PLAYER;
ent.pos = Vec3::new(x, y, z);
self.player = Some(id);
self.voxels.load_chunks_around(ent.pos, 2);
id
}
pub fn step(&mut self, inputs: &[pxl8_input_msg], dt: f32) {
self.tick += 1;
self.time += dt;
for input in inputs {
self.move_player(input, dt);
}
self.integrate(dt);
}
fn trace(&self, from: Vec3, to: Vec3, radius: f32) -> Vec3 {
if self.world.active().is_some() {
return self.world.trace(from, to, radius);
}
self.voxels.trace(from, to, radius)
}
fn integrate(&mut self, dt: f32) {
const GRAVITY: f32 = 800.0;
const FRICTION: f32 = 6.0;
const RADIUS: f32 = 16.0;
for i in 0..self.entities.len() {
let ent = &self.entities[i];
if ent.flags & ALIVE == 0 || ent.flags & PLAYER != 0 {
continue;
}
let mut vel = ent.vel;
let pos = ent.pos;
let flags = ent.flags;
vel.y = vel.y - GRAVITY * dt;
if flags & GROUNDED != 0 {
let speed = sqrtf(vel.x * vel.x + vel.z * vel.z);
if speed > 0.0 {
let drop = speed * FRICTION * dt;
let scale = (speed - drop).max(0.0) / speed;
vel.x = vel.x * scale;
vel.z = vel.z * scale;
}
}
let target = pos + vel * dt;
let new_pos = self.trace(pos, target, RADIUS);
let ent = &mut self.entities[i];
ent.vel = vel;
ent.pos = new_pos;
}
}
fn move_player(&mut self, input: &pxl8_input_msg, dt: f32) {
let Some(id) = self.player else { return };
let ent = &mut self.entities[id as usize];
if ent.flags & ALIVE == 0 {
return;
}
const MOVE_SPEED: f32 = 320.0;
const GROUND_ACCEL: f32 = 10.0;
const AIR_ACCEL: f32 = 1.0;
const STOP_SPEED: f32 = 100.0;
const FRICTION: f32 = 6.0;
const RADIUS: f32 = 16.0;
let sin_yaw = sinf(input.yaw);
let cos_yaw = cosf(input.yaw);
let input_len = sqrtf(input.move_x * input.move_x + input.move_y * input.move_y);
let (move_dir, target_speed) = if input_len > 0.0 {
let nx = input.move_x / input_len;
let ny = input.move_y / input_len;
let dir = Vec3::new(
cos_yaw * nx - sin_yaw * ny,
0.0,
-sin_yaw * nx - cos_yaw * ny,
);
(dir, MOVE_SPEED)
} else {
(Vec3::ZERO, 0.0)
};
let grounded = ent.flags & GROUNDED != 0;
if grounded {
let speed = sqrtf(ent.vel.x * ent.vel.x + ent.vel.z * ent.vel.z);
if speed > 0.0 {
let control = speed.max(STOP_SPEED);
let drop = control * FRICTION * dt;
let scale = (speed - drop).max(0.0) / speed;
ent.vel.x *= scale;
ent.vel.z *= scale;
}
}
if target_speed > 0.0 {
let accel = if grounded { GROUND_ACCEL } else { AIR_ACCEL };
let current = ent.vel.x * move_dir.x + ent.vel.z * move_dir.z;
let add = target_speed - current;
if add > 0.0 {
let amount = (accel * target_speed * dt).min(add);
ent.vel.x += move_dir.x * amount;
ent.vel.z += move_dir.z * amount;
}
}
ent.yaw = input.yaw;
ent.pitch = (ent.pitch - input.look_dy * 0.008).clamp(-1.5, 1.5);
let pos = ent.pos;
let vel = ent.vel;
let target = pos + vel * dt;
let new_pos = self.trace(pos, target, RADIUS);
let ground_check = self.trace(new_pos, new_pos - Vec3::Y * 2.0, RADIUS);
let ent = &mut self.entities[id as usize];
ent.pos = new_pos;
if ground_check.y < new_pos.y - 1.0 {
ent.flags &= !GROUNDED;
} else {
ent.flags |= GROUNDED;
}
}
pub fn generate_snapshot<F>(&self, _player_id: u64, mut writer: F)
where
F: FnMut(&pxl8_entity_state),
{
for (i, ent) in self.entities.iter().enumerate() {
if ent.flags & ALIVE == 0 {
continue;
}
let mut state = pxl8_entity_state {
entity_id: i as u64,
userdata: [0u8; 56],
};
let bytes = &mut state.userdata;
bytes[0..4].copy_from_slice(&ent.pos.x.to_be_bytes());
bytes[4..8].copy_from_slice(&ent.pos.y.to_be_bytes());
bytes[8..12].copy_from_slice(&ent.pos.z.to_be_bytes());
bytes[12..16].copy_from_slice(&ent.yaw.to_be_bytes());
bytes[16..20].copy_from_slice(&ent.pitch.to_be_bytes());
bytes[20..24].copy_from_slice(&ent.vel.x.to_be_bytes());
bytes[24..28].copy_from_slice(&ent.vel.y.to_be_bytes());
bytes[28..32].copy_from_slice(&ent.vel.z.to_be_bytes());
bytes[32..36].copy_from_slice(&ent.flags.to_be_bytes());
bytes[36..38].copy_from_slice(&ent.kind.to_be_bytes());
writer(&state);
}
}
pub fn entity_count(&self) -> usize {
self.entities.iter().filter(|e| e.flags & ALIVE != 0).count()
}
pub fn get_player_position(&self, player_id: u64) -> Option<(f32, f32, f32)> {
let idx = player_id as usize;
if idx < self.entities.len() {
let ent = &self.entities[idx];
if ent.flags & ALIVE != 0 && ent.flags & PLAYER != 0 {
return Some((ent.pos.x, ent.pos.y, ent.pos.z));
}
}
None
}
}
impl Default for Simulation {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,6 +1,120 @@
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use crate::protocol::*; use crate::protocol::*;
use crate::protocol::pxl8_msg_type::*;
use crate::voxel::VoxelChunk;
pub const DEFAULT_PORT: u16 = 7777; pub const DEFAULT_PORT: u16 = 7777;
pub const CHUNK_MAX_PAYLOAD: usize = 1400;
pub const CHUNK_FLAG_RLE: u8 = 0x01;
pub const CHUNK_FLAG_FINAL: u8 = 0x04;
pub const CHUNK_TYPE_VXL: u8 = 0;
pub const CHUNK_TYPE_BSP: u8 = 1;
pub struct ChunkMessage {
pub chunk_type: u8,
pub id: u32,
pub cx: i32,
pub cy: i32,
pub cz: i32,
pub version: u32,
pub flags: u8,
pub fragment_idx: u8,
pub fragment_count: u8,
pub payload: Vec<u8>,
}
impl ChunkMessage {
pub fn from_voxel(chunk: &VoxelChunk, version: u32) -> Vec<ChunkMessage> {
let rle_data = chunk.rle_encode();
let total_size = rle_data.len();
if total_size <= CHUNK_MAX_PAYLOAD {
return vec![ChunkMessage {
chunk_type: CHUNK_TYPE_VXL,
id: 0,
cx: chunk.cx,
cy: chunk.cy,
cz: chunk.cz,
version,
flags: CHUNK_FLAG_RLE | CHUNK_FLAG_FINAL,
fragment_idx: 0,
fragment_count: 1,
payload: rle_data,
}];
}
let fragment_count = (total_size + CHUNK_MAX_PAYLOAD - 1) / CHUNK_MAX_PAYLOAD;
let mut messages = Vec::new();
for i in 0..fragment_count {
let start = i * CHUNK_MAX_PAYLOAD;
let end = ((i + 1) * CHUNK_MAX_PAYLOAD).min(total_size);
let is_final = i == fragment_count - 1;
messages.push(ChunkMessage {
chunk_type: CHUNK_TYPE_VXL,
id: 0,
cx: chunk.cx,
cy: chunk.cy,
cz: chunk.cz,
version,
flags: CHUNK_FLAG_RLE | if is_final { CHUNK_FLAG_FINAL } else { 0 },
fragment_idx: i as u8,
fragment_count: fragment_count as u8,
payload: rle_data[start..end].to_vec(),
});
}
messages
}
pub fn from_bsp(data: Vec<u8>, chunk_id: u32, version: u32) -> Vec<ChunkMessage> {
let total_size = data.len();
if total_size <= CHUNK_MAX_PAYLOAD {
return vec![ChunkMessage {
chunk_type: CHUNK_TYPE_BSP,
id: chunk_id,
cx: 0,
cy: 0,
cz: 0,
version,
flags: CHUNK_FLAG_FINAL,
fragment_idx: 0,
fragment_count: 1,
payload: data,
}];
}
let fragment_count = (total_size + CHUNK_MAX_PAYLOAD - 1) / CHUNK_MAX_PAYLOAD;
let mut messages = Vec::new();
for i in 0..fragment_count {
let start = i * CHUNK_MAX_PAYLOAD;
let end = ((i + 1) * CHUNK_MAX_PAYLOAD).min(total_size);
let is_final = i == fragment_count - 1;
messages.push(ChunkMessage {
chunk_type: CHUNK_TYPE_BSP,
id: chunk_id,
cx: 0,
cy: 0,
cz: 0,
version,
flags: if is_final { CHUNK_FLAG_FINAL } else { 0 },
fragment_idx: i as u8,
fragment_count: fragment_count as u8,
payload: data[start..end].to_vec(),
});
}
messages
}
}
#[cfg(unix)] #[cfg(unix)]
mod sys { mod sys {
@ -129,8 +243,8 @@ type SockAddr = windows_sys::Win32::Networking::WinSock::SOCKADDR_IN;
pub struct Transport { pub struct Transport {
client_addr: SockAddr, client_addr: SockAddr,
has_client: bool, has_client: bool,
recv_buf: [u8; 4096], recv_buf: [u8; 512],
send_buf: [u8; 4096], send_buf: [u8; 2048],
socket: sys::RawSocket, socket: sys::RawSocket,
} }
@ -154,8 +268,8 @@ impl Transport {
Some(Self { Some(Self {
client_addr: unsafe { core::mem::zeroed() }, client_addr: unsafe { core::mem::zeroed() },
has_client: false, has_client: false,
recv_buf: [0u8; 4096], recv_buf: [0u8; 512],
send_buf: [0u8; 4096], send_buf: [0u8; 2048],
socket: sock, socket: sock,
}) })
} }
@ -198,7 +312,7 @@ impl Transport {
let msg_header = pxl8_msg_header { let msg_header = pxl8_msg_header {
sequence, sequence,
size: 0, size: 0,
type_: pxl8_msg_type::PXL8_MSG_SNAPSHOT as u8, type_: PXL8_MSG_SNAPSHOT as u8,
version: PXL8_PROTOCOL_VERSION as u8, version: PXL8_PROTOCOL_VERSION as u8,
}; };
offset += self.serialize_header(&msg_header, offset); offset += self.serialize_header(&msg_header, offset);
@ -211,6 +325,72 @@ impl Transport {
sys::sendto(self.socket, &self.send_buf[..offset], &self.client_addr); sys::sendto(self.socket, &self.send_buf[..offset], &self.client_addr);
} }
pub fn send_chunk(&mut self, msg: &ChunkMessage, sequence: u32) {
if !self.has_client {
return;
}
let mut offset = 0;
let msg_header = pxl8_msg_header {
sequence,
size: 0,
type_: PXL8_MSG_CHUNK as u8,
version: PXL8_PROTOCOL_VERSION as u8,
};
offset += self.serialize_header(&msg_header, offset);
offset += self.serialize_chunk_msg_header(msg, offset);
let payload_len = msg.payload.len().min(self.send_buf.len() - offset);
self.send_buf[offset..offset + payload_len].copy_from_slice(&msg.payload[..payload_len]);
offset += payload_len;
sys::sendto(self.socket, &self.send_buf[..offset], &self.client_addr);
}
pub fn send_chunk_enter(&mut self, chunk_id: u32, chunk_type: u8, sequence: u32) {
if !self.has_client {
return;
}
let mut offset = 0;
let msg_header = pxl8_msg_header {
sequence,
size: 0,
type_: PXL8_MSG_CHUNK_ENTER as u8,
version: PXL8_PROTOCOL_VERSION as u8,
};
offset += self.serialize_header(&msg_header, offset);
let buf = &mut self.send_buf[offset..];
buf[0..4].copy_from_slice(&chunk_id.to_be_bytes());
buf[4] = chunk_type;
buf[5] = 0;
buf[6] = 0;
buf[7] = 0;
offset += 8;
sys::sendto(self.socket, &self.send_buf[..offset], &self.client_addr);
}
fn serialize_chunk_msg_header(&mut self, msg: &ChunkMessage, offset: usize) -> usize {
let buf = &mut self.send_buf[offset..];
buf[0] = msg.chunk_type;
buf[1] = msg.flags;
buf[2] = msg.fragment_idx;
buf[3] = msg.fragment_count;
buf[4..8].copy_from_slice(&msg.id.to_be_bytes());
buf[8..12].copy_from_slice(&(msg.cx as u32).to_be_bytes());
buf[12..16].copy_from_slice(&(msg.cy as u32).to_be_bytes());
buf[16..20].copy_from_slice(&(msg.cz as u32).to_be_bytes());
buf[20..24].copy_from_slice(&msg.version.to_be_bytes());
let payload_size = msg.payload.len() as u16;
buf[24..26].copy_from_slice(&payload_size.to_be_bytes());
buf[26..28].copy_from_slice(&[0u8; 2]);
28
}
fn serialize_header(&mut self, h: &pxl8_msg_header, offset: usize) -> usize { fn serialize_header(&mut self, h: &pxl8_msg_header, offset: usize) -> usize {
let buf = &mut self.send_buf[offset..]; let buf = &mut self.send_buf[offset..];
buf[0..4].copy_from_slice(&h.sequence.to_be_bytes()); buf[0..4].copy_from_slice(&h.sequence.to_be_bytes());

318
pxl8d/src/voxel.rs Normal file
View file

@ -0,0 +1,318 @@
extern crate alloc;
use alloc::vec::Vec;
use libm::floorf;
use crate::math::Vec3;
pub const CHUNK_SIZE: usize = 32;
pub const CHUNK_VOLUME: usize = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE;
pub const AIR: u8 = 0;
pub const STONE: u8 = 1;
pub const DIRT: u8 = 2;
pub const GRASS: u8 = 3;
#[derive(Clone)]
pub struct VoxelChunk {
pub blocks: [u8; CHUNK_VOLUME],
pub cx: i32,
pub cy: i32,
pub cz: i32,
}
impl VoxelChunk {
pub fn new(cx: i32, cy: i32, cz: i32) -> Self {
Self {
blocks: [AIR; CHUNK_VOLUME],
cx,
cy,
cz,
}
}
pub fn trace(&self, from: Vec3, to: Vec3, radius: f32) -> Vec3 {
if !self.is_solid_radius(to.x, to.y, to.z, radius) {
return to;
}
let x_ok = !self.is_solid_radius(to.x, from.y, from.z, radius);
let y_ok = !self.is_solid_radius(from.x, to.y, from.z, radius);
let z_ok = !self.is_solid_radius(from.x, from.y, to.z, radius);
let mut result = from;
if x_ok { result.x = to.x; }
if y_ok { result.y = to.y; }
if z_ok { result.z = to.z; }
result
}
fn world_to_local(x: f32, chunk_coord: i32) -> usize {
let chunk_base = chunk_coord as f32 * CHUNK_SIZE as f32;
let local = (x - chunk_base) as usize;
local.min(CHUNK_SIZE - 1)
}
fn is_solid_at(&self, x: f32, y: f32, z: f32) -> bool {
let lx = Self::world_to_local(x, self.cx);
let ly = Self::world_to_local(y, self.cy);
let lz = Self::world_to_local(z, self.cz);
self.get(lx, ly, lz) != AIR
}
fn is_solid_radius(&self, x: f32, y: f32, z: f32, radius: f32) -> bool {
self.is_solid_at(x - radius, y, z) ||
self.is_solid_at(x + radius, y, z) ||
self.is_solid_at(x, y - radius, z) ||
self.is_solid_at(x, y + radius, z) ||
self.is_solid_at(x, y, z - radius) ||
self.is_solid_at(x, y, z + radius)
}
pub fn index(x: usize, y: usize, z: usize) -> usize {
x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE
}
pub fn get(&self, x: usize, y: usize, z: usize) -> u8 {
if x >= CHUNK_SIZE || y >= CHUNK_SIZE || z >= CHUNK_SIZE {
return AIR;
}
self.blocks[Self::index(x, y, z)]
}
pub fn set(&mut self, x: usize, y: usize, z: usize, block: u8) {
if x < CHUNK_SIZE && y < CHUNK_SIZE && z < CHUNK_SIZE {
self.blocks[Self::index(x, y, z)] = block;
}
}
pub fn rle_encode(&self) -> Vec<u8> {
let mut result = Vec::new();
let mut i = 0;
while i < CHUNK_VOLUME {
let block = self.blocks[i];
let mut run_len = 1usize;
while i + run_len < CHUNK_VOLUME
&& self.blocks[i + run_len] == block
&& run_len < 256
{
run_len += 1;
}
result.push(block);
result.push((run_len - 1) as u8);
i += run_len;
}
result
}
}
pub struct VoxelWorld {
pub chunks: Vec<VoxelChunk>,
pub seed: u64,
}
impl VoxelWorld {
pub fn new(seed: u64) -> Self {
Self {
chunks: Vec::new(),
seed,
}
}
pub fn get_chunk(&self, cx: i32, cy: i32, cz: i32) -> Option<&VoxelChunk> {
self.chunks.iter().find(|c| c.cx == cx && c.cy == cy && c.cz == cz)
}
pub fn get_chunk_mut(&mut self, cx: i32, cy: i32, cz: i32) -> Option<&mut VoxelChunk> {
self.chunks.iter_mut().find(|c| c.cx == cx && c.cy == cy && c.cz == cz)
}
pub fn ensure_chunk(&mut self, cx: i32, cy: i32, cz: i32) -> &mut VoxelChunk {
if self.get_chunk(cx, cy, cz).is_none() {
let mut chunk = VoxelChunk::new(cx, cy, cz);
generate_chunk(&mut chunk, self.seed);
self.chunks.push(chunk);
}
self.get_chunk_mut(cx, cy, cz).unwrap()
}
pub fn world_to_chunk(x: f32) -> i32 {
floorf(x / CHUNK_SIZE as f32) as i32
}
pub fn world_to_local(x: f32) -> usize {
let chunk = floorf(x / CHUNK_SIZE as f32);
let local = (x - chunk * CHUNK_SIZE as f32) as usize;
local.min(CHUNK_SIZE - 1)
}
pub fn is_solid(&self, x: f32, y: f32, z: f32) -> bool {
let cx = Self::world_to_chunk(x);
let cy = Self::world_to_chunk(y);
let cz = Self::world_to_chunk(z);
let lx = Self::world_to_local(x);
let ly = Self::world_to_local(y);
let lz = Self::world_to_local(z);
match self.get_chunk(cx, cy, cz) {
Some(chunk) => chunk.get(lx, ly, lz) != AIR,
None => false,
}
}
pub fn is_solid_radius(&self, x: f32, y: f32, z: f32, radius: f32) -> bool {
self.is_solid(x - radius, y, z) ||
self.is_solid(x + radius, y, z) ||
self.is_solid(x, y - radius, z) ||
self.is_solid(x, y + radius, z) ||
self.is_solid(x, y, z - radius) ||
self.is_solid(x, y, z + radius)
}
pub fn trace(&self, from: Vec3, to: Vec3, radius: f32) -> Vec3 {
if !self.is_solid_radius(to.x, to.y, to.z, radius) {
return to;
}
let x_ok = !self.is_solid_radius(to.x, from.y, from.z, radius);
let y_ok = !self.is_solid_radius(from.x, to.y, from.z, radius);
let z_ok = !self.is_solid_radius(from.x, from.y, to.z, radius);
let mut result = from;
if x_ok {
result.x = to.x;
}
if y_ok {
result.y = to.y;
}
if z_ok {
result.z = to.z;
}
result
}
pub fn chunks_near(&self, pos: Vec3, radius: i32) -> Vec<(i32, i32, i32)> {
let cx = Self::world_to_chunk(pos.x);
let cy = Self::world_to_chunk(pos.y);
let cz = Self::world_to_chunk(pos.z);
let mut result = Vec::new();
for dz in -radius..=radius {
for dy in -radius..=radius {
for dx in -radius..=radius {
result.push((cx + dx, cy + dy, cz + dz));
}
}
}
result
}
pub fn load_chunks_around(&mut self, pos: Vec3, radius: i32) {
let coords = self.chunks_near(pos, radius);
for (cx, cy, cz) in coords {
self.ensure_chunk(cx, cy, cz);
}
}
}
fn hash(mut x: u64) -> u64 {
x ^= x >> 33;
x = x.wrapping_mul(0xff51afd7ed558ccd);
x ^= x >> 33;
x = x.wrapping_mul(0xc4ceb9fe1a85ec53);
x ^= x >> 33;
x
}
fn noise2d(x: i32, z: i32, seed: u64) -> f32 {
let h = hash(seed ^ (x as u64) ^ ((z as u64) << 32));
(h & 0xFFFF) as f32 / 65535.0
}
fn smoothstep(t: f32) -> f32 {
t * t * (3.0 - 2.0 * t)
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
fn value_noise(x: f32, z: f32, seed: u64) -> f32 {
let x0 = floorf(x) as i32;
let z0 = floorf(z) as i32;
let x1 = x0 + 1;
let z1 = z0 + 1;
let tx = smoothstep(x - x0 as f32);
let tz = smoothstep(z - z0 as f32);
let c00 = noise2d(x0, z0, seed);
let c10 = noise2d(x1, z0, seed);
let c01 = noise2d(x0, z1, seed);
let c11 = noise2d(x1, z1, seed);
let a = lerp(c00, c10, tx);
let b = lerp(c01, c11, tx);
lerp(a, b, tz)
}
fn fbm(x: f32, z: f32, seed: u64, octaves: u32) -> f32 {
let mut value = 0.0;
let mut amplitude = 1.0;
let mut frequency = 1.0;
let mut max_value = 0.0;
for i in 0..octaves {
value += amplitude * value_noise(x * frequency, z * frequency, seed.wrapping_add(i as u64 * 1000));
max_value += amplitude;
amplitude *= 0.5;
frequency *= 2.0;
}
value / max_value
}
fn generate_chunk(chunk: &mut VoxelChunk, seed: u64) {
let world_x = chunk.cx * CHUNK_SIZE as i32;
let world_y = chunk.cy * CHUNK_SIZE as i32;
let world_z = chunk.cz * CHUNK_SIZE as i32;
for lz in 0..CHUNK_SIZE {
for lx in 0..CHUNK_SIZE {
let wx = (world_x + lx as i32) as f32;
let wz = (world_z + lz as i32) as f32;
let height = fbm(wx * 0.02, wz * 0.02, seed, 4) * 32.0 + 16.0;
let height = height as i32;
for ly in 0..CHUNK_SIZE {
let wy = world_y + ly as i32;
let block = if wy > height {
AIR
} else if wy == height {
GRASS
} else if wy > height - 4 {
DIRT
} else {
STONE
};
chunk.set(lx, ly, lz, block);
}
}
}
}
impl Default for VoxelWorld {
fn default() -> Self {
Self::new(12345)
}
}

87
pxl8d/src/world.rs Normal file
View file

@ -0,0 +1,87 @@
extern crate alloc;
use alloc::collections::BTreeMap;
use crate::chunk::{Chunk, ChunkId};
use crate::math::Vec3;
use crate::procgen::{ProcgenParams, generate_rooms};
pub struct World {
active: Option<ChunkId>,
chunks: BTreeMap<ChunkId, Chunk>,
seed: u64,
}
impl World {
pub fn new(seed: u64) -> Self {
Self {
chunks: BTreeMap::new(),
active: None,
seed,
}
}
pub fn insert(&mut self, chunk: Chunk) {
let id = chunk.id();
self.chunks.insert(id, chunk);
}
pub fn get(&self, id: &ChunkId) -> Option<&Chunk> {
self.chunks.get(id)
}
pub fn get_mut(&mut self, id: &ChunkId) -> Option<&mut Chunk> {
self.chunks.get_mut(id)
}
pub fn remove(&mut self, id: &ChunkId) -> Option<Chunk> {
self.chunks.remove(id)
}
pub fn set_active(&mut self, id: ChunkId) {
self.active = Some(id);
}
pub fn active(&self) -> Option<&Chunk> {
self.active.as_ref().and_then(|id| self.chunks.get(id))
}
pub fn active_id(&self) -> Option<ChunkId> {
self.active
}
pub fn trace(&self, from: Vec3, to: Vec3, radius: f32) -> Vec3 {
if let Some(chunk) = self.active() {
return chunk.trace(from, to, radius);
}
to
}
pub fn generate_bsp(&mut self, id: u32, params: Option<&ProcgenParams>) -> &Chunk {
let chunk_id = ChunkId::Bsp(id);
if !self.chunks.contains_key(&chunk_id) {
let default_params = ProcgenParams {
seed: (self.seed as u32).wrapping_add(id),
..Default::default()
};
let p = params.unwrap_or(&default_params);
let bsp = generate_rooms(p);
self.chunks.insert(chunk_id, Chunk::Bsp { id, bsp, version: 1 });
}
self.chunks.get(&chunk_id).unwrap()
}
pub fn contains(&self, id: &ChunkId) -> bool {
self.chunks.contains_key(id)
}
pub fn iter(&self) -> impl Iterator<Item = (&ChunkId, &Chunk)> {
self.chunks.iter()
}
}
impl Default for World {
fn default() -> Self {
Self::new(12345)
}
}

1090
server/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,45 +0,0 @@
use bevy_ecs::prelude::*;
#[derive(Component, Clone, Copy, Default)]
pub struct Position {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Component, Clone, Copy, Default)]
pub struct Rotation {
pub pitch: f32,
pub yaw: f32,
}
#[derive(Component, Clone, Copy, Default)]
pub struct Velocity {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Component, Clone, Copy)]
pub struct Health {
pub current: f32,
pub max: f32,
}
impl Health {
pub fn new(max: f32) -> Self {
Self { current: max, max }
}
}
impl Default for Health {
fn default() -> Self {
Self::new(100.0)
}
}
#[derive(Component, Clone, Copy, Default)]
pub struct Player;
#[derive(Component, Clone, Copy)]
pub struct TypeId(pub u16);

View file

@ -1,18 +0,0 @@
#![no_std]
extern crate alloc;
pub mod components;
mod simulation;
pub mod transport;
#[allow(dead_code, non_camel_case_types, non_snake_case, non_upper_case_globals)]
pub mod protocol {
include!(concat!(env!("OUT_DIR"), "/protocol.rs"));
}
pub use bevy_ecs::prelude::*;
pub use components::*;
pub use protocol::*;
pub use simulation::*;
pub use transport::*;

View file

@ -1,142 +0,0 @@
#![no_std]
#![no_main]
extern crate alloc;
mod allocator;
use core::panic::PanicInfo;
use pxl8_server::*;
#[global_allocator]
static ALLOCATOR: allocator::Allocator = allocator::Allocator;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
const TICK_RATE: u64 = 30;
const TICK_NS: u64 = 1_000_000_000 / TICK_RATE;
#[cfg(unix)]
fn get_time_ns() -> u64 {
let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
(ts.tv_sec as u64) * 1_000_000_000 + (ts.tv_nsec as u64)
}
#[cfg(windows)]
fn get_time_ns() -> u64 {
use windows_sys::Win32::System::Performance::*;
static mut FREQ: i64 = 0;
unsafe {
if FREQ == 0 {
QueryPerformanceFrequency(&mut FREQ);
}
let mut count: i64 = 0;
QueryPerformanceCounter(&mut count);
((count as u128 * 1_000_000_000) / FREQ as u128) as u64
}
}
#[cfg(unix)]
fn sleep_ms(ms: u64) {
let ts = libc::timespec {
tv_sec: (ms / 1000) as i64,
tv_nsec: ((ms % 1000) * 1_000_000) as i64,
};
unsafe { libc::nanosleep(&ts, core::ptr::null_mut()) };
}
#[cfg(windows)]
fn sleep_ms(ms: u64) {
use windows_sys::Win32::System::Threading::Sleep;
unsafe { Sleep(ms as u32) };
}
fn extract_spawn_position(payload: &[u8]) -> (f32, f32, f32, f32, f32) {
let x = f32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let y = f32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
let z = f32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
let yaw = f32::from_be_bytes([payload[12], payload[13], payload[14], payload[15]]);
let pitch = f32::from_be_bytes([payload[16], payload[17], payload[18], payload[19]]);
(x, y, z, yaw, pitch)
}
#[unsafe(no_mangle)]
pub extern "C" fn main(_argc: i32, _argv: *const *const u8) -> i32 {
let mut transport = match transport::Transport::bind(transport::DEFAULT_PORT) {
Some(t) => t,
None => return 1,
};
let mut sim = Simulation::new();
let mut player_id: u64 = 0;
let mut last_client_tick: u64 = 0;
let mut sequence: u32 = 0;
let mut last_tick = get_time_ns();
let mut entities_buf = [protocol::pxl8_entity_state {
entity_id: 0,
userdata: [0u8; 56],
}; 64];
let mut inputs_buf: [protocol::pxl8_input_msg; 16] = unsafe { core::mem::zeroed() };
loop {
let now = get_time_ns();
let elapsed = now.saturating_sub(last_tick);
if elapsed >= TICK_NS {
last_tick = now;
let dt = (elapsed as f32) / 1_000_000_000.0;
let mut latest_input: Option<protocol::pxl8_input_msg> = None;
while let Some(msg_type) = transport.recv() {
match msg_type {
x if x == protocol::pxl8_msg_type::PXL8_MSG_INPUT as u8 => {
latest_input = Some(transport.get_input());
}
x if x == protocol::pxl8_msg_type::PXL8_MSG_COMMAND as u8 => {
let cmd = transport.get_command();
if cmd.cmd_type == protocol::pxl8_cmd_type::PXL8_CMD_SPAWN_ENTITY as u16 {
let (x, y, z, _yaw, _pitch) = extract_spawn_position(&cmd.payload);
let player = sim.spawn_player(x, y, z);
player_id = player.to_bits();
}
}
_ => {}
}
}
if let Some(input) = latest_input {
last_client_tick = input.tick;
inputs_buf[0] = input;
sim.step(&inputs_buf[..1], dt);
} else {
sim.step(&[], dt);
}
let mut count = 0;
sim.generate_snapshot(player_id, |state| {
if count < entities_buf.len() {
entities_buf[count] = *state;
count += 1;
}
});
let header = protocol::pxl8_snapshot_header {
entity_count: count as u16,
event_count: 0,
player_id,
tick: last_client_tick,
time: sim.time,
};
transport.send_snapshot(&header, &entities_buf[..count], sequence);
sequence = sequence.wrapping_add(1);
}
sleep_ms(1);
}
}

View file

@ -1,133 +0,0 @@
use bevy_ecs::prelude::*;
use libm::{cosf, sinf};
use crate::components::*;
use crate::protocol::*;
#[derive(Resource, Default)]
pub struct SimTime {
pub dt: f32,
pub time: f32,
}
pub struct Simulation {
pub player: Option<Entity>,
pub tick: u64,
pub time: f32,
pub world: World,
}
impl Simulation {
pub fn new() -> Self {
let mut world = World::new();
world.insert_resource(SimTime::default());
Self {
player: None,
tick: 0,
time: 0.0,
world,
}
}
pub fn step(&mut self, inputs: &[pxl8_input_msg], dt: f32) {
self.tick += 1;
self.time += dt;
if let Some(mut sim_time) = self.world.get_resource_mut::<SimTime>() {
sim_time.dt = dt;
sim_time.time = self.time;
}
for input in inputs {
self.apply_input(input, dt);
}
}
fn apply_input(&mut self, input: &pxl8_input_msg, dt: f32) {
let Some(player) = self.player else { return };
let speed = 200.0;
let yaw = input.yaw;
let sin_yaw = sinf(yaw);
let cos_yaw = cosf(yaw);
let move_x = input.move_x;
let move_y = input.move_y;
let len_sq = move_x * move_x + move_y * move_y;
if len_sq > 0.0 {
let len = libm::sqrtf(len_sq);
let norm_x = move_x / len;
let norm_y = move_y / len;
let forward_x = -sin_yaw * norm_y * speed * dt;
let forward_z = -cos_yaw * norm_y * speed * dt;
let strafe_x = cos_yaw * norm_x * speed * dt;
let strafe_z = -sin_yaw * norm_x * speed * dt;
if let Some(mut pos) = self.world.get_mut::<Position>(player) {
pos.x += forward_x + strafe_x;
pos.z += forward_z + strafe_z;
}
}
if let Some(mut rot) = self.world.get_mut::<Rotation>(player) {
rot.yaw = yaw;
rot.pitch = (rot.pitch - input.look_dy * 0.008).clamp(-1.5, 1.5);
}
}
pub fn spawn_entity(&mut self) -> Entity {
self.world.spawn((
Position::default(),
Rotation::default(),
Velocity::default(),
)).id()
}
pub fn spawn_player(&mut self, x: f32, y: f32, z: f32) -> Entity {
let entity = self.world.spawn((
Player,
Position { x, y, z },
Rotation::default(),
Velocity::default(),
Health::default(),
)).id();
self.player = Some(entity);
entity
}
pub fn generate_snapshot<F>(&mut self, player_id: u64, mut writer: F)
where
F: FnMut(&pxl8_entity_state),
{
let mut query = self.world.query::<(Entity, &Position, &Rotation)>();
for (entity, pos, rot) in query.iter(&self.world) {
let mut state = pxl8_entity_state {
entity_id: entity.to_bits(),
userdata: [0u8; 56],
};
let bytes = &mut state.userdata;
bytes[0..4].copy_from_slice(&pos.x.to_be_bytes());
bytes[4..8].copy_from_slice(&pos.y.to_be_bytes());
bytes[8..12].copy_from_slice(&pos.z.to_be_bytes());
bytes[12..16].copy_from_slice(&rot.yaw.to_be_bytes());
bytes[16..20].copy_from_slice(&rot.pitch.to_be_bytes());
writer(&state);
}
let _ = player_id;
}
pub fn entity_count(&self) -> usize {
self.world.entities().len() as usize
}
}
impl Default for Simulation {
fn default() -> Self {
Self::new()
}
}

View file

@ -55,6 +55,10 @@ static const char embed_pxl8_particles[] = {
#embed "src/lua/pxl8/particles.lua" #embed "src/lua/pxl8/particles.lua"
, 0 }; , 0 };
static const char embed_pxl8_procgen[] = {
#embed "src/lua/pxl8/procgen.lua"
, 0 };
static const char embed_pxl8_sfx[] = { static const char embed_pxl8_sfx[] = {
#embed "src/lua/pxl8/sfx.lua" #embed "src/lua/pxl8/sfx.lua"
, 0 }; , 0 };
@ -89,6 +93,7 @@ static const pxl8_embed pxl8_embeds[] = {
PXL8_EMBED_ENTRY(embed_pxl8_math, "pxl8.math"), PXL8_EMBED_ENTRY(embed_pxl8_math, "pxl8.math"),
PXL8_EMBED_ENTRY(embed_pxl8_net, "pxl8.net"), PXL8_EMBED_ENTRY(embed_pxl8_net, "pxl8.net"),
PXL8_EMBED_ENTRY(embed_pxl8_particles, "pxl8.particles"), PXL8_EMBED_ENTRY(embed_pxl8_particles, "pxl8.particles"),
PXL8_EMBED_ENTRY(embed_pxl8_procgen, "pxl8.procgen"),
PXL8_EMBED_ENTRY(embed_pxl8_sfx, "pxl8.sfx"), PXL8_EMBED_ENTRY(embed_pxl8_sfx, "pxl8.sfx"),
PXL8_EMBED_ENTRY(embed_pxl8_tilemap, "pxl8.tilemap"), PXL8_EMBED_ENTRY(embed_pxl8_tilemap, "pxl8.tilemap"),
PXL8_EMBED_ENTRY(embed_pxl8_transition, "pxl8.transition"), PXL8_EMBED_ENTRY(embed_pxl8_transition, "pxl8.transition"),

View file

@ -237,6 +237,19 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
pxl8_rng_seed(&game->rng, (u32)sys->hal->get_ticks()); pxl8_rng_seed(&game->rng, (u32)sys->hal->get_ticks());
game->world = pxl8_world_create();
if (!game->world) {
pxl8_error("failed to create world");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
pxl8_net_config net_cfg = { .address = "127.0.0.1", .port = 7777 };
game->net = pxl8_net_create(&net_cfg);
if (game->net) {
pxl8_net_set_chunk_cache(game->net, pxl8_world_chunk_cache(game->world));
pxl8_net_connect(game->net);
}
#ifndef NDEBUG #ifndef NDEBUG
game->debug_replay = pxl8_replay_create_buffer(60, 60); game->debug_replay = pxl8_replay_create_buffer(60, 60);
pxl8_sfx_mixer_set_event_callback(game->mixer, pxl8_audio_event_callback, game); pxl8_sfx_mixer_set_event_callback(game->mixer, pxl8_audio_event_callback, game);
@ -338,6 +351,13 @@ pxl8_result pxl8_update(pxl8* sys) {
} }
} }
if (game->net) {
while (pxl8_net_poll(game->net)) {}
pxl8_net_update(game->net, dt);
pxl8_world_sync(game->world, game->net);
}
pxl8_world_update(game->world, dt);
pxl8_gfx_update(game->gfx, dt); pxl8_gfx_update(game->gfx, dt);
pxl8_sfx_mixer_process(game->mixer); pxl8_sfx_mixer_process(game->mixer);
@ -418,6 +438,9 @@ void pxl8_quit(pxl8* sys) {
pxl8_replay_destroy(game->debug_replay); pxl8_replay_destroy(game->debug_replay);
#endif #endif
if (game->net) pxl8_net_destroy(game->net);
if (game->world) pxl8_world_destroy(game->world);
pxl8_sfx_mixer_destroy(game->mixer); pxl8_sfx_mixer_destroy(game->mixer);
pxl8_gfx_destroy(game->gfx); pxl8_gfx_destroy(game->gfx);
pxl8_script_destroy(game->script); pxl8_script_destroy(game->script);
@ -437,6 +460,10 @@ void pxl8_set_running(pxl8* sys, bool running) {
} }
} }
pxl8_world* pxl8_get_world(pxl8* sys) {
return (sys && sys->game) ? sys->game->world : NULL;
}
f32 pxl8_get_fps(const pxl8* sys) { f32 pxl8_get_fps(const pxl8* sys) {
return (sys && sys->game) ? sys->game->fps : 0.0f; return (sys && sys->game) ? sys->game->fps : 0.0f;
} }
@ -449,6 +476,10 @@ pxl8_input_state* pxl8_get_input(const pxl8* sys) {
return (sys && sys->game) ? &sys->game->input : NULL; return (sys && sys->game) ? &sys->game->input : NULL;
} }
pxl8_net* pxl8_get_net(const pxl8* sys) {
return (sys && sys->game) ? sys->game->net : NULL;
}
pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys) { pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys) {
return (sys && sys->game) ? sys->game->mixer : NULL; return (sys && sys->game) ? sys->game->mixer : NULL;
} }

View file

@ -1,37 +1,37 @@
#pragma once #pragma once
#include "pxl8_gfx.h" #include "pxl8_gfx.h"
#include "pxl8_net.h"
#include "pxl8_rng.h" #include "pxl8_rng.h"
#include "pxl8_script.h" #include "pxl8_script.h"
#include "pxl8_sfx.h" #include "pxl8_sfx.h"
#include "pxl8_types.h" #include "pxl8_types.h"
#include "pxl8_world.h"
typedef struct pxl8_replay pxl8_replay; typedef struct pxl8_replay pxl8_replay;
typedef struct pxl8_game { typedef struct pxl8_game {
pxl8_gfx* gfx;
pxl8_script* script;
pxl8_sfx_mixer* mixer;
pxl8_rng rng;
i32 frame_count;
u64 last_time;
f32 time;
f32 fps_accumulator;
i32 fps_frame_count;
f32 fps;
pxl8_input_state input;
pxl8_input_state prev_input;
#ifndef NDEBUG #ifndef NDEBUG
pxl8_replay* debug_replay; pxl8_replay* debug_replay;
#endif #endif
f32 fps;
f32 fps_accumulator;
i32 fps_frame_count;
i32 frame_count;
pxl8_gfx* gfx;
pxl8_input_state input;
u64 last_time;
pxl8_sfx_mixer* mixer;
pxl8_net* net;
pxl8_input_state prev_input;
bool repl_mode; bool repl_mode;
bool repl_started; bool repl_started;
pxl8_rng rng;
bool running; bool running;
pxl8_script* script;
bool script_loaded; bool script_loaded;
char script_path[256]; char script_path[256];
f32 time;
pxl8_world* world;
} pxl8_game; } pxl8_game;

View file

@ -1,5 +1,5 @@
#include "pxl8_log.h" #include "pxl8_log.h"
#include "pxl8_repl.h" #include "pxl8_types.h"
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
@ -18,6 +18,7 @@ static pxl8_log* g_log = NULL;
void pxl8_log_init(pxl8_log* log) { void pxl8_log_init(pxl8_log* log) {
g_log = log; g_log = log;
g_log->handler = NULL;
g_log->level = PXL8_LOG_LEVEL_DEBUG; g_log->level = PXL8_LOG_LEVEL_DEBUG;
const char* env_level = getenv("PXL8_LOG_LEVEL"); const char* env_level = getenv("PXL8_LOG_LEVEL");
@ -30,6 +31,10 @@ void pxl8_log_init(pxl8_log* log) {
} }
} }
void pxl8_log_set_handler(pxl8_log_handler handler) {
if (g_log) g_log->handler = handler;
}
void pxl8_log_set_level(pxl8_log_level level) { void pxl8_log_set_level(pxl8_log_level level) {
if (g_log) g_log->level = level; if (g_log) g_log->level = level;
} }
@ -61,7 +66,7 @@ static void log_output(const char* color, const char* level,
strncat(buffer, "\n", sizeof(buffer) - strlen(buffer) - 1); strncat(buffer, "\n", sizeof(buffer) - strlen(buffer) - 1);
if (!pxl8_repl_push_log(buffer)) { if (!g_log->handler || !g_log->handler(buffer)) {
printf("%s", buffer); printf("%s", buffer);
fflush(stdout); fflush(stdout);
} }

View file

@ -10,11 +10,15 @@ typedef enum {
PXL8_LOG_LEVEL_ERROR = 4, PXL8_LOG_LEVEL_ERROR = 4,
} pxl8_log_level; } pxl8_log_level;
typedef bool (*pxl8_log_handler)(const char* message);
typedef struct pxl8_log { typedef struct pxl8_log {
pxl8_log_handler handler;
pxl8_log_level level; pxl8_log_level level;
} pxl8_log; } pxl8_log;
void pxl8_log_init(pxl8_log* log); void pxl8_log_init(pxl8_log* log);
void pxl8_log_set_handler(pxl8_log_handler handler);
void pxl8_log_set_level(pxl8_log_level level); void pxl8_log_set_level(pxl8_log_level level);
void pxl8_log_write_trace(const char* file, int line, const char* fmt, ...); void pxl8_log_write_trace(const char* file, int line, const char* fmt, ...);

View file

@ -3,6 +3,7 @@
#include "pxl8_gfx.h" #include "pxl8_gfx.h"
#include "pxl8_hal.h" #include "pxl8_hal.h"
#include "pxl8_io.h" #include "pxl8_io.h"
#include "pxl8_net.h"
#include "pxl8_sfx.h" #include "pxl8_sfx.h"
#include "pxl8_types.h" #include "pxl8_types.h"
@ -23,6 +24,7 @@ void pxl8_quit(pxl8* sys);
f32 pxl8_get_fps(const pxl8* sys); f32 pxl8_get_fps(const pxl8* sys);
pxl8_gfx* pxl8_get_gfx(const pxl8* sys); pxl8_gfx* pxl8_get_gfx(const pxl8* sys);
pxl8_input_state* pxl8_get_input(const pxl8* sys); pxl8_input_state* pxl8_get_input(const pxl8* sys);
pxl8_net* pxl8_get_net(const pxl8* sys);
pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution); pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution);
pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys); pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys);
bool pxl8_is_running(const pxl8* sys); bool pxl8_is_running(const pxl8* sys);

View file

@ -87,11 +87,13 @@ pxl8.create_anim_from_ase = anim.Anim.from_ase
pxl8.bounds = math.bounds pxl8.bounds = math.bounds
pxl8.Camera3D = gfx3d.Camera3D pxl8.Camera3D = gfx3d.Camera3D
pxl8.Material = gfx3d.Material
pxl8.Mesh = gfx3d.Mesh pxl8.Mesh = gfx3d.Mesh
pxl8.begin_frame_3d = gfx3d.begin_frame pxl8.begin_frame_3d = gfx3d.begin_frame
pxl8.clear_3d = gfx3d.clear pxl8.clear_3d = gfx3d.clear
pxl8.clear_depth = gfx3d.clear_depth pxl8.clear_depth = gfx3d.clear_depth
pxl8.create_camera_3d = gfx3d.Camera3D.new pxl8.create_camera_3d = gfx3d.Camera3D.new
pxl8.create_material = gfx3d.create_material
pxl8.create_mesh = gfx3d.Mesh.new pxl8.create_mesh = gfx3d.Mesh.new
pxl8.create_vec3_array = gfx3d.create_vec3_array pxl8.create_vec3_array = gfx3d.create_vec3_array
pxl8.draw_line_3d = gfx3d.draw_line pxl8.draw_line_3d = gfx3d.draw_line
@ -132,10 +134,7 @@ pxl8.mat4_rotate_z = math.mat4_rotate_z
pxl8.mat4_scale = math.mat4_scale pxl8.mat4_scale = math.mat4_scale
pxl8.mat4_translate = math.mat4_translate pxl8.mat4_translate = math.mat4_translate
pxl8.Net = net.Net pxl8.get_net = net.get
pxl8.create_net = net.Net.new
pxl8.NET_MODE_LOCAL = net.MODE_LOCAL
pxl8.NET_MODE_REMOTE = net.MODE_REMOTE
pxl8.pack_f32_be = bytes.pack_f32_be pxl8.pack_f32_be = bytes.pack_f32_be
pxl8.pack_f32_le = bytes.pack_f32_le pxl8.pack_f32_le = bytes.pack_f32_le
@ -221,7 +220,11 @@ pxl8.unpack_u32_le = bytes.unpack_u32_le
pxl8.unpack_u64_be = bytes.unpack_u64_be pxl8.unpack_u64_be = bytes.unpack_u64_be
pxl8.unpack_u64_le = bytes.unpack_u64_le pxl8.unpack_u64_le = bytes.unpack_u64_le
pxl8.Bsp = world.Bsp
pxl8.Chunk = world.Chunk
pxl8.CHUNK_BSP = world.CHUNK_BSP
pxl8.CHUNK_VXL = world.CHUNK_VXL
pxl8.World = world.World pxl8.World = world.World
pxl8.create_world = world.World.new pxl8.get_world = world.World.get
return pxl8 return pxl8

View file

@ -205,4 +205,27 @@ function gfx3d.create_vec3_array(count)
return ffi.new("pxl8_vec3[?]", count) return ffi.new("pxl8_vec3[?]", count)
end end
local Material = {}
Material.__index = Material
function Material.new(opts)
opts = opts or {}
local mat = ffi.new("pxl8_gfx_material", {
texture_id = opts.texture or 0,
alpha = opts.alpha or 255,
blend_mode = opts.blend_mode or 0,
dither = opts.dither ~= false,
double_sided = opts.double_sided or false,
dynamic_lighting = opts.lighting or false,
per_pixel = opts.per_pixel or false,
vertex_color_passthrough = opts.passthrough or false,
wireframe = opts.wireframe or false,
emissive_intensity = opts.emissive or 0.0,
})
return setmetatable({ _ptr = mat }, Material)
end
gfx3d.Material = Material
gfx3d.create_material = Material.new
return gfx3d return gfx3d

View file

@ -1,47 +1,24 @@
local ffi = require("ffi") local ffi = require("ffi")
local C = ffi.C local C = ffi.C
local core = require("pxl8.core")
local net = {} local net = {}
local Net = {} local Net = {}
Net.__index = Net Net.__index = Net
net.MODE_LOCAL = C.PXL8_NET_LOCAL function net.get()
net.MODE_REMOTE = C.PXL8_NET_REMOTE local ptr = C.pxl8_get_net(core.sys)
if ptr == nil then
function Net.new(config)
config = config or {}
local cfg = ffi.new("pxl8_net_config")
cfg.address = config.address or "127.0.0.1"
cfg.mode = config.mode or C.PXL8_NET_REMOTE
cfg.port = config.port or 7777
local n = C.pxl8_net_create(cfg)
if n == nil then
return nil return nil
end end
return setmetatable({ _ptr = n }, Net) return setmetatable({ _ptr = ptr }, Net)
end
function Net:connect()
return C.pxl8_net_connect(self._ptr) == 0
end end
function Net:connected() function Net:connected()
return C.pxl8_net_connected(self._ptr) return C.pxl8_net_connected(self._ptr)
end end
function Net:destroy()
if self._ptr then
C.pxl8_net_destroy(self._ptr)
self._ptr = nil
end
end
function Net:disconnect()
C.pxl8_net_disconnect(self._ptr)
end
function Net:entities() function Net:entities()
local snap = C.pxl8_net_snapshot(self._ptr) local snap = C.pxl8_net_snapshot(self._ptr)
if snap == nil then if snap == nil then
@ -113,10 +90,6 @@ function Net:player_id()
return tonumber(C.pxl8_net_player_id(self._ptr)) return tonumber(C.pxl8_net_player_id(self._ptr))
end end
function Net:poll()
return C.pxl8_net_poll(self._ptr)
end
function Net:predicted_state() function Net:predicted_state()
return C.pxl8_net_predicted_state(self._ptr) return C.pxl8_net_predicted_state(self._ptr)
end end
@ -173,10 +146,4 @@ function Net:tick()
return tonumber(C.pxl8_net_tick(self._ptr)) return tonumber(C.pxl8_net_tick(self._ptr))
end end
function Net:update(dt)
C.pxl8_net_update(self._ptr, dt)
end
net.Net = Net
return net return net

View file

@ -4,40 +4,77 @@ local core = require("pxl8.core")
local world = {} local world = {}
world.CHUNK_VXL = 0
world.CHUNK_BSP = 1
world.PROCGEN_ROOMS = C.PXL8_PROCGEN_ROOMS world.PROCGEN_ROOMS = C.PXL8_PROCGEN_ROOMS
world.PROCGEN_TERRAIN = C.PXL8_PROCGEN_TERRAIN world.PROCGEN_TERRAIN = C.PXL8_PROCGEN_TERRAIN
local Bsp = {}
Bsp.__index = Bsp
function Bsp:face_count()
return C.pxl8_bsp_face_count(self._ptr)
end
function Bsp:face_normal(face_id)
return C.pxl8_bsp_face_normal(self._ptr, face_id)
end
function Bsp:face_set_material(face_id, material_id)
C.pxl8_bsp_face_set_material(self._ptr, face_id, material_id)
end
function Bsp:set_material(material_id, material)
C.pxl8_bsp_set_material(self._ptr, material_id, material._ptr)
end
function Bsp:set_wireframe(wireframe)
C.pxl8_bsp_set_wireframe(self._ptr, wireframe)
end
world.Bsp = Bsp
local Chunk = {}
Chunk.__index = Chunk
function Chunk:bsp()
if self._ptr == nil then return nil end
if self._ptr.type ~= world.CHUNK_BSP then return nil end
local ptr = self._ptr.bsp
if ptr == nil then return nil end
return setmetatable({ _ptr = ptr }, Bsp)
end
function Chunk:type()
if self._ptr == nil then return nil end
return self._ptr.type
end
function Chunk:ready()
if self._ptr == nil then return false end
if self._ptr.type == world.CHUNK_BSP then
return self._ptr.bsp ~= nil
elseif self._ptr.type == world.CHUNK_VXL then
return self._ptr.voxel ~= nil
end
return false
end
world.Chunk = Chunk
local World = {} local World = {}
World.__index = World World.__index = World
function World.new() function World.get()
local w = C.pxl8_world_create() local w = C.pxl8_get_world(core.sys)
if w == nil then if w == nil then return nil end
return nil
end
return setmetatable({ _ptr = w }, World) return setmetatable({ _ptr = w }, World)
end end
function World:apply_textures(texture_defs) function World:active_chunk()
local count = #texture_defs local ptr = C.pxl8_world_active_chunk(self._ptr)
local textures = ffi.new("pxl8_world_texture[?]", count) if ptr == nil then return nil end
return setmetatable({ _ptr = ptr }, Chunk)
for i, def in ipairs(texture_defs) do
local idx = i - 1
ffi.copy(textures[idx].name, def.name or "", math.min(#(def.name or ""), 15))
textures[idx].texture_id = def.texture_id or 0
if def.rule then
textures[idx].rule = ffi.cast("bool (*)(const pxl8_vec3*, const pxl8_bsp_face*, const pxl8_bsp*)",
function(normal, face, bsp)
return def.rule(normal[0], face, bsp)
end)
else
textures[idx].rule = nil
end
end
return C.pxl8_world_apply_textures(self._ptr, textures, count)
end end
function World:check_collision(x, y, z, radius) function World:check_collision(x, y, z, radius)
@ -45,34 +82,6 @@ function World:check_collision(x, y, z, radius)
return C.pxl8_world_check_collision(self._ptr, pos, radius) return C.pxl8_world_check_collision(self._ptr, pos, radius)
end end
function World:destroy()
if self._ptr then
C.pxl8_world_destroy(self._ptr)
self._ptr = nil
end
end
function World:generate(params)
local c_params = ffi.new("pxl8_procgen_params")
c_params.type = params.type or C.PXL8_PROCGEN_ROOMS
c_params.width = params.width or 32
c_params.height = params.height or 32
c_params.depth = params.depth or 0
c_params.seed = params.seed or 0
c_params.min_room_size = params.min_room_size or 5
c_params.max_room_size = params.max_room_size or 10
c_params.num_rooms = params.num_rooms or 8
return C.pxl8_world_generate(self._ptr, core.gfx, c_params)
end
function World:is_loaded()
return C.pxl8_world_is_loaded(self._ptr)
end
function World:load(filepath)
return C.pxl8_world_load(self._ptr, filepath)
end
function World:render(camera_pos) function World:render(camera_pos)
local vec = ffi.new("pxl8_vec3", {x = camera_pos[1], y = camera_pos[2], z = camera_pos[3]}) local vec = ffi.new("pxl8_vec3", {x = camera_pos[1], y = camera_pos[2], z = camera_pos[3]})
C.pxl8_world_render(self._ptr, core.gfx, vec) C.pxl8_world_render(self._ptr, core.gfx, vec)
@ -85,14 +94,6 @@ function World:resolve_collision(from_x, from_y, from_z, to_x, to_y, to_z, radiu
return result.x, result.y, result.z return result.x, result.y, result.z
end end
function World:set_wireframe(enabled)
C.pxl8_world_set_wireframe(self._ptr, enabled)
end
function World:unload()
C.pxl8_world_unload(self._ptr)
end
world.World = World world.World = World
return world return world

View file

@ -1,4 +1,6 @@
#include "pxl8_net.h" #include "pxl8_net.h"
#include "pxl8_chunk_cache.h"
#include "pxl8_log.h"
#include "pxl8_mem.h" #include "pxl8_mem.h"
#include <stdlib.h> #include <stdlib.h>
@ -28,6 +30,9 @@
struct pxl8_net { struct pxl8_net {
char address[256]; char address[256];
u32 chunk_id;
u8 chunk_type;
pxl8_chunk_cache* chunk_cache;
bool connected; bool connected;
pxl8_entity_state entities[PXL8_MAX_SNAPSHOT_ENTITIES]; pxl8_entity_state entities[PXL8_MAX_SNAPSHOT_ENTITIES];
pxl8_event_msg events[PXL8_MAX_SNAPSHOT_EVENTS]; pxl8_event_msg events[PXL8_MAX_SNAPSHOT_EVENTS];
@ -36,7 +41,6 @@ struct pxl8_net {
u64 input_head; u64 input_head;
u64 input_oldest_tick; u64 input_oldest_tick;
f32 interp_time; f32 interp_time;
pxl8_net_mode mode;
u16 port; u16 port;
u8 predicted_state[PXL8_NET_USERDATA_SIZE]; u8 predicted_state[PXL8_NET_USERDATA_SIZE];
u64 predicted_tick; u64 predicted_tick;
@ -98,7 +102,6 @@ pxl8_net* pxl8_net_create(const pxl8_net_config* config) {
pxl8_net* net = pxl8_calloc(1, sizeof(pxl8_net)); pxl8_net* net = pxl8_calloc(1, sizeof(pxl8_net));
if (!net) return NULL; if (!net) return NULL;
net->mode = config->mode;
net->port = config->port ? config->port : PXL8_NET_DEFAULT_PORT; net->port = config->port ? config->port : PXL8_NET_DEFAULT_PORT;
net->sock = INVALID_SOCK; net->sock = INVALID_SOCK;
net->connected = false; net->connected = false;
@ -209,6 +212,32 @@ bool pxl8_net_poll(pxl8_net* net) {
pxl8_msg_header hdr; pxl8_msg_header hdr;
usize offset = pxl8_protocol_deserialize_header(net->recv_buf, len, &hdr); usize offset = pxl8_protocol_deserialize_header(net->recv_buf, len, &hdr);
if (hdr.type == PXL8_MSG_CHUNK) {
if (!net->chunk_cache) return false;
pxl8_chunk_msg_header chunk_hdr;
offset += pxl8_protocol_deserialize_chunk_msg_header(net->recv_buf + offset, len - offset, &chunk_hdr);
const u8* payload = net->recv_buf + offset;
usize payload_len = chunk_hdr.payload_size;
if (payload_len > len - offset) {
payload_len = len - offset;
}
pxl8_chunk_cache_receive(net->chunk_cache, &chunk_hdr, payload, payload_len);
return true;
}
if (hdr.type == PXL8_MSG_CHUNK_ENTER) {
pxl8_chunk_enter_msg chunk_msg;
pxl8_protocol_deserialize_chunk_enter(net->recv_buf + offset, len - offset, &chunk_msg);
net->chunk_id = chunk_msg.chunk_id;
net->chunk_type = chunk_msg.chunk_type;
pxl8_debug("[CLIENT] Received CHUNK_ENTER type=%u id=%u", chunk_msg.chunk_type, chunk_msg.chunk_id);
return true;
}
if (hdr.type != PXL8_MSG_SNAPSHOT) return false; if (hdr.type != PXL8_MSG_SNAPSHOT) return false;
pxl8_snapshot_header snap; pxl8_snapshot_header snap;
@ -311,3 +340,23 @@ void pxl8_net_update(pxl8_net* net, f32 dt) {
if (!net) return; if (!net) return;
net->interp_time += dt; net->interp_time += dt;
} }
void pxl8_net_set_chunk_cache(pxl8_net* net, pxl8_chunk_cache* cache) {
if (!net) return;
net->chunk_cache = cache;
}
pxl8_chunk_cache* pxl8_net_chunk_cache(pxl8_net* net) {
if (!net) return NULL;
return net->chunk_cache;
}
u32 pxl8_net_chunk_id(const pxl8_net* net) {
if (!net) return 0;
return net->chunk_id;
}
u8 pxl8_net_chunk_type(const pxl8_net* net) {
if (!net) return PXL8_CHUNK_TYPE_VXL;
return net->chunk_type;
}

View file

@ -11,15 +11,10 @@ extern "C" {
#define PXL8_NET_USERDATA_SIZE 56 #define PXL8_NET_USERDATA_SIZE 56
typedef struct pxl8_net pxl8_net; typedef struct pxl8_net pxl8_net;
typedef struct pxl8_chunk_cache pxl8_chunk_cache;
typedef enum pxl8_net_mode {
PXL8_NET_LOCAL = 0,
PXL8_NET_REMOTE
} pxl8_net_mode;
typedef struct pxl8_net_config { typedef struct pxl8_net_config {
const char* address; const char* address;
pxl8_net_mode mode;
u16 port; u16 port;
} pxl8_net_config; } pxl8_net_config;
@ -48,6 +43,11 @@ pxl8_result pxl8_net_send_input(pxl8_net* net, const pxl8_input_msg* input);
const pxl8_snapshot_header* pxl8_net_snapshot(const pxl8_net* net); const pxl8_snapshot_header* pxl8_net_snapshot(const pxl8_net* net);
u64 pxl8_net_tick(const pxl8_net* net); u64 pxl8_net_tick(const pxl8_net* net);
void pxl8_net_update(pxl8_net* net, f32 dt); void pxl8_net_update(pxl8_net* net, f32 dt);
void pxl8_net_set_chunk_cache(pxl8_net* net, pxl8_chunk_cache* cache);
pxl8_chunk_cache* pxl8_net_chunk_cache(pxl8_net* net);
u32 pxl8_net_chunk_id(const pxl8_net* net);
u8 pxl8_net_chunk_type(const pxl8_net* net);
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -122,3 +122,76 @@ usize pxl8_protocol_deserialize_snapshot_header(const u8* buf, usize len, pxl8_s
hdr->time = pxl8_read_f32_be(&s); hdr->time = pxl8_read_f32_be(&s);
return s.offset; return s.offset;
} }
usize pxl8_protocol_serialize_chunk_msg_header(const pxl8_chunk_msg_header* hdr, u8* buf, usize len) {
if (len < 24) return 0;
pxl8_write_stream s = pxl8_write_stream_create(buf, (u32)len);
pxl8_write_u8(&s, hdr->chunk_type);
pxl8_write_u8(&s, hdr->flags);
pxl8_write_u8(&s, hdr->fragment_idx);
pxl8_write_u8(&s, hdr->fragment_count);
pxl8_write_u32_be(&s, hdr->id);
pxl8_write_u32_be(&s, (u32)hdr->cx);
pxl8_write_u32_be(&s, (u32)hdr->cy);
pxl8_write_u32_be(&s, (u32)hdr->cz);
pxl8_write_u32_be(&s, hdr->version);
pxl8_write_u16_be(&s, hdr->payload_size);
pxl8_write_u16_be(&s, hdr->reserved);
return s.offset;
}
usize pxl8_protocol_deserialize_chunk_msg_header(const u8* buf, usize len, pxl8_chunk_msg_header* hdr) {
if (len < 24) return 0;
pxl8_stream s = pxl8_stream_create(buf, (u32)len);
hdr->chunk_type = pxl8_read_u8(&s);
hdr->flags = pxl8_read_u8(&s);
hdr->fragment_idx = pxl8_read_u8(&s);
hdr->fragment_count = pxl8_read_u8(&s);
hdr->id = pxl8_read_u32_be(&s);
hdr->cx = (i32)pxl8_read_u32_be(&s);
hdr->cy = (i32)pxl8_read_u32_be(&s);
hdr->cz = (i32)pxl8_read_u32_be(&s);
hdr->version = pxl8_read_u32_be(&s);
hdr->payload_size = pxl8_read_u16_be(&s);
hdr->reserved = pxl8_read_u16_be(&s);
return s.offset;
}
usize pxl8_protocol_deserialize_bsp_wire_header(const u8* buf, usize len, pxl8_bsp_wire_header* hdr) {
if (len < 44) return 0;
pxl8_stream s = pxl8_stream_create(buf, (u32)len);
hdr->num_vertices = pxl8_read_u32_be(&s);
hdr->num_edges = pxl8_read_u32_be(&s);
hdr->num_faces = pxl8_read_u32_be(&s);
hdr->num_planes = pxl8_read_u32_be(&s);
hdr->num_nodes = pxl8_read_u32_be(&s);
hdr->num_leafs = pxl8_read_u32_be(&s);
hdr->num_surfedges = pxl8_read_u32_be(&s);
hdr->num_marksurfaces = pxl8_read_u32_be(&s);
hdr->num_cell_portals = pxl8_read_u32_be(&s);
hdr->visdata_size = pxl8_read_u32_be(&s);
hdr->num_vertex_lights = pxl8_read_u32_be(&s);
return s.offset;
}
usize pxl8_protocol_serialize_chunk_enter(const pxl8_chunk_enter_msg* msg, u8* buf, usize len) {
if (len < 8) return 0;
pxl8_write_stream s = pxl8_write_stream_create(buf, (u32)len);
pxl8_write_u32_be(&s, msg->chunk_id);
pxl8_write_u8(&s, msg->chunk_type);
pxl8_write_u8(&s, msg->reserved[0]);
pxl8_write_u8(&s, msg->reserved[1]);
pxl8_write_u8(&s, msg->reserved[2]);
return s.offset;
}
usize pxl8_protocol_deserialize_chunk_enter(const u8* buf, usize len, pxl8_chunk_enter_msg* msg) {
if (len < 8) return 0;
pxl8_stream s = pxl8_stream_create(buf, (u32)len);
msg->chunk_id = pxl8_read_u32_be(&s);
msg->chunk_type = pxl8_read_u8(&s);
msg->reserved[0] = pxl8_read_u8(&s);
msg->reserved[1] = pxl8_read_u8(&s);
msg->reserved[2] = pxl8_read_u8(&s);
return s.offset;
}

View file

@ -14,12 +14,14 @@ extern "C" {
typedef enum pxl8_msg_type { typedef enum pxl8_msg_type {
PXL8_MSG_NONE = 0, PXL8_MSG_NONE = 0,
PXL8_MSG_CHUNK,
PXL8_MSG_CHUNK_ENTER,
PXL8_MSG_COMMAND,
PXL8_MSG_CONNECT, PXL8_MSG_CONNECT,
PXL8_MSG_DISCONNECT, PXL8_MSG_DISCONNECT,
PXL8_MSG_EVENT,
PXL8_MSG_INPUT, PXL8_MSG_INPUT,
PXL8_MSG_COMMAND, PXL8_MSG_SNAPSHOT
PXL8_MSG_SNAPSHOT,
PXL8_MSG_EVENT
} pxl8_msg_type; } pxl8_msg_type;
typedef struct pxl8_msg_header { typedef struct pxl8_msg_header {
@ -70,6 +72,39 @@ typedef struct pxl8_snapshot_header {
f32 time; f32 time;
} pxl8_snapshot_header; } pxl8_snapshot_header;
#define PXL8_CHUNK_TYPE_VXL 0
#define PXL8_CHUNK_TYPE_BSP 1
#define PXL8_CHUNK_FLAG_RLE 0x01
#define PXL8_CHUNK_FLAG_FINAL 0x04
#define PXL8_CHUNK_MAX_PAYLOAD 1400
typedef struct pxl8_chunk_msg_header {
u8 chunk_type;
u8 flags;
u8 fragment_idx;
u8 fragment_count;
u32 id;
i32 cx, cy, cz;
u32 version;
u16 payload_size;
u16 reserved;
} pxl8_chunk_msg_header;
typedef struct pxl8_bsp_wire_header {
u32 num_vertices;
u32 num_edges;
u32 num_faces;
u32 num_planes;
u32 num_nodes;
u32 num_leafs;
u32 num_surfedges;
u32 num_marksurfaces;
u32 num_cell_portals;
u32 visdata_size;
u32 num_vertex_lights;
} pxl8_bsp_wire_header;
usize pxl8_protocol_serialize_header(const pxl8_msg_header* msg, u8* buf, usize len); usize pxl8_protocol_serialize_header(const pxl8_msg_header* msg, u8* buf, usize len);
usize pxl8_protocol_deserialize_header(const u8* buf, usize len, pxl8_msg_header* msg); usize pxl8_protocol_deserialize_header(const u8* buf, usize len, pxl8_msg_header* msg);
@ -88,6 +123,20 @@ usize pxl8_protocol_deserialize_event(const u8* buf, usize len, pxl8_event_msg*
usize pxl8_protocol_serialize_snapshot_header(const pxl8_snapshot_header* hdr, u8* buf, usize len); usize pxl8_protocol_serialize_snapshot_header(const pxl8_snapshot_header* hdr, u8* buf, usize len);
usize pxl8_protocol_deserialize_snapshot_header(const u8* buf, usize len, pxl8_snapshot_header* hdr); usize pxl8_protocol_deserialize_snapshot_header(const u8* buf, usize len, pxl8_snapshot_header* hdr);
usize pxl8_protocol_serialize_chunk_msg_header(const pxl8_chunk_msg_header* hdr, u8* buf, usize len);
usize pxl8_protocol_deserialize_chunk_msg_header(const u8* buf, usize len, pxl8_chunk_msg_header* hdr);
usize pxl8_protocol_deserialize_bsp_wire_header(const u8* buf, usize len, pxl8_bsp_wire_header* hdr);
typedef struct pxl8_chunk_enter_msg {
u32 chunk_id;
u8 chunk_type;
u8 reserved[3];
} pxl8_chunk_enter_msg;
usize pxl8_protocol_serialize_chunk_enter(const pxl8_chunk_enter_msg* msg, u8* buf, usize len);
usize pxl8_protocol_deserialize_chunk_enter(const u8* buf, usize len, pxl8_chunk_enter_msg* msg);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View file

@ -1,15 +1,16 @@
#include "pxl8_repl.h" #include "pxl8_repl.h"
#include "pxl8_mem.h"
#include <linenoise.h>
#include <poll.h> #include <poll.h>
#include <SDL3/SDL.h>
#include <stdatomic.h> #include <stdatomic.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <SDL3/SDL.h> #include "pxl8_log.h"
#include <linenoise.h> #include "pxl8_mem.h"
#define PXL8_MAX_REPL_COMMAND_SIZE 4096 #define PXL8_MAX_REPL_COMMAND_SIZE 4096
#define PXL8_REPL_QUEUE_SIZE 8 #define PXL8_REPL_QUEUE_SIZE 8
@ -235,6 +236,7 @@ pxl8_repl* pxl8_repl_create(void) {
linenoiseHistoryLoad(".pxl8_history"); linenoiseHistoryLoad(".pxl8_history");
g_repl = repl; g_repl = repl;
pxl8_log_set_handler(pxl8_repl_push_log);
repl->thread = SDL_CreateThread(pxl8_repl_thread, "pxl8-repl", repl); repl->thread = SDL_CreateThread(pxl8_repl_thread, "pxl8-repl", repl);
if (!repl->thread) { if (!repl->thread) {
@ -260,6 +262,7 @@ void pxl8_repl_destroy(pxl8_repl* repl) {
pxl8_repl_flush_logs(repl); pxl8_repl_flush_logs(repl);
g_repl = NULL; g_repl = NULL;
pxl8_log_set_handler(NULL);
system("stty sane 2>/dev/null"); system("stty sane 2>/dev/null");
pxl8_free(repl); pxl8_free(repl);

View file

@ -424,27 +424,33 @@ static const char* pxl8_ffi_cdefs =
"void pxl8_graph_eval_texture(const pxl8_graph* graph, u8* buffer, i32 width, i32 height);\n" "void pxl8_graph_eval_texture(const pxl8_graph* graph, u8* buffer, i32 width, i32 height);\n"
"\n" "\n"
"typedef struct pxl8_bsp pxl8_bsp;\n" "typedef struct pxl8_bsp pxl8_bsp;\n"
"typedef struct pxl8_bsp_face pxl8_bsp_face;\n"
"typedef bool (*pxl8_texture_rule)(const pxl8_vec3* normal, const pxl8_bsp_face* face, const pxl8_bsp* bsp);\n"
"\n" "\n"
"typedef struct pxl8_world_texture {\n" "u32 pxl8_bsp_face_count(const pxl8_bsp* bsp);\n"
" char name[16];\n" "pxl8_vec3 pxl8_bsp_face_normal(const pxl8_bsp* bsp, u32 face_id);\n"
" unsigned int texture_id;\n" "void pxl8_bsp_face_set_material(pxl8_bsp* bsp, u32 face_id, u16 material_id);\n"
" pxl8_texture_rule rule;\n" "void pxl8_bsp_set_material(pxl8_bsp* bsp, u16 material_id, const pxl8_gfx_material* material);\n"
"} pxl8_world_texture;\n" "void pxl8_bsp_set_wireframe(pxl8_bsp* bsp, bool wireframe);\n"
"\n"
"typedef enum { PXL8_CHUNK_VXL = 0, PXL8_CHUNK_BSP = 1 } pxl8_chunk_type;\n"
"\n"
"typedef struct pxl8_chunk {\n"
" pxl8_chunk_type type;\n"
" u32 id;\n"
" u32 version;\n"
" i32 cx, cy, cz;\n"
" union {\n"
" void* voxel;\n"
" pxl8_bsp* bsp;\n"
" };\n"
"} pxl8_chunk;\n"
"\n" "\n"
"typedef struct pxl8_world pxl8_world;\n" "typedef struct pxl8_world pxl8_world;\n"
"pxl8_world* pxl8_world_create(void);\n" "\n"
"void pxl8_world_destroy(pxl8_world* world);\n" "pxl8_world* pxl8_get_world(pxl8* sys);\n"
"int pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params);\n" "pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world);\n"
"int pxl8_world_load(pxl8_world* world, const char* path);\n"
"void pxl8_world_unload(pxl8_world* world);\n"
"int pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, unsigned int count);\n"
"bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, float radius);\n" "bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, float radius);\n"
"bool pxl8_world_is_loaded(const pxl8_world* world);\n"
"void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);\n" "void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);\n"
"pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, float radius);\n" "pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, float radius);\n"
"void pxl8_world_set_wireframe(pxl8_world* world, bool enabled);\n"
"\n" "\n"
"typedef struct { i32 cursor_x; i32 cursor_y; bool cursor_down; bool cursor_clicked; u32 hot_id; u32 active_id; } pxl8_gui_state;\n" "typedef struct { i32 cursor_x; i32 cursor_y; bool cursor_down; bool cursor_clicked; u32 hot_id; u32 active_id; } pxl8_gui_state;\n"
"pxl8_gui_state* pxl8_gui_state_create(void);\n" "pxl8_gui_state* pxl8_gui_state_create(void);\n"
@ -517,8 +523,7 @@ static const char* pxl8_ffi_cdefs =
"void pxl8_sfx_stop_voice(pxl8_sfx_context* ctx, u16 voice_id);\n" "void pxl8_sfx_stop_voice(pxl8_sfx_context* ctx, u16 voice_id);\n"
"\n" "\n"
"typedef struct pxl8_net pxl8_net;\n" "typedef struct pxl8_net pxl8_net;\n"
"typedef enum pxl8_net_mode { PXL8_NET_LOCAL = 0, PXL8_NET_REMOTE } pxl8_net_mode;\n" "typedef struct pxl8_net_config { const char* address; u16 port; } pxl8_net_config;\n"
"typedef struct pxl8_net_config { const char* address; pxl8_net_mode mode; u16 port; } pxl8_net_config;\n"
"typedef enum pxl8_cmd_type { PXL8_CMD_NONE = 0, PXL8_CMD_SPAWN_ENTITY } pxl8_cmd_type;\n" "typedef enum pxl8_cmd_type { PXL8_CMD_NONE = 0, PXL8_CMD_SPAWN_ENTITY } pxl8_cmd_type;\n"
"\n" "\n"
"typedef struct pxl8_command_msg {\n" "typedef struct pxl8_command_msg {\n"
@ -574,6 +579,7 @@ static const char* pxl8_ffi_cdefs =
"const pxl8_snapshot_header* pxl8_net_snapshot(const pxl8_net* net);\n" "const pxl8_snapshot_header* pxl8_net_snapshot(const pxl8_net* net);\n"
"u64 pxl8_net_tick(const pxl8_net* net);\n" "u64 pxl8_net_tick(const pxl8_net* net);\n"
"void pxl8_net_update(pxl8_net* net, f32 dt);\n" "void pxl8_net_update(pxl8_net* net, f32 dt);\n"
"pxl8_net* pxl8_get_net(const pxl8* sys);\n"
"\n" "\n"
"void pxl8_bit_clear(u32* val, u8 bit);\n" "void pxl8_bit_clear(u32* val, u8 bit);\n"
"u32 pxl8_bit_count(u32 val);\n" "u32 pxl8_bit_count(u32 val);\n"

View file

@ -394,6 +394,48 @@ i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos) {
return -(node_id + 1); return -(node_id + 1);
} }
bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos) {
i32 leaf = pxl8_bsp_find_leaf(bsp, pos);
if (leaf < 0 || (u32)leaf >= bsp->num_leafs) return true;
return bsp->leafs[leaf].contents == -1;
}
static bool point_clear(const pxl8_bsp* bsp, f32 x, f32 y, f32 z, f32 radius) {
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x + radius, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x - radius, y, z})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z + radius})) return false;
if (pxl8_bsp_point_solid(bsp, (pxl8_vec3){x, y, z - radius})) return false;
return true;
}
pxl8_vec3 pxl8_bsp_trace(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
if (!bsp || bsp->num_nodes == 0) return to;
if (point_clear(bsp, to.x, to.y, to.z, radius)) {
return to;
}
bool x_ok = point_clear(bsp, to.x, from.y, from.z, radius);
bool z_ok = point_clear(bsp, from.x, from.y, to.z, radius);
if (x_ok && z_ok) {
f32 dx = to.x - from.x;
f32 dz = to.z - from.z;
if (dx * dx > dz * dz) {
return (pxl8_vec3){to.x, from.y, from.z};
} else {
return (pxl8_vec3){from.x, from.y, to.z};
}
} else if (x_ok) {
return (pxl8_vec3){to.x, from.y, from.z};
} else if (z_ok) {
return (pxl8_vec3){from.x, from.y, to.z};
}
return from;
}
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to) { bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to) {
if (!bsp || !bsp->visdata || bsp->visdata_size == 0) return true; if (!bsp || !bsp->visdata || bsp->visdata_size == 0) return true;
if (leaf_from < 0 || leaf_to < 0) return true; if (leaf_from < 0 || leaf_to < 0) return true;
@ -576,12 +618,6 @@ static inline bool face_in_frustum(const pxl8_bsp* bsp, u32 face_id, const pxl8_
return pxl8_frustum_test_aabb(frustum, face->aabb_min, face->aabb_max); return pxl8_frustum_test_aabb(frustum, face->aabb_min, face->aabb_max);
} }
static inline bool leaf_in_frustum(const pxl8_bsp_leaf* leaf, const pxl8_frustum* frustum) {
pxl8_vec3 mins = {(f32)leaf->mins[0], (f32)leaf->mins[1], (f32)leaf->mins[2]};
pxl8_vec3 maxs = {(f32)leaf->maxs[0], (f32)leaf->maxs[1], (f32)leaf->maxs[2]};
return pxl8_frustum_test_aabb(frustum, mins, maxs);
}
static inline bool screen_rect_valid(screen_rect r) { static inline bool screen_rect_valid(screen_rect r) {
return r.x0 < r.x1 && r.y0 < r.y1; return r.x0 < r.x1 && r.y0 < r.y1;
} }
@ -744,16 +780,24 @@ void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, const
} }
void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) { void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) {
if (!gfx || !bsp || bsp->num_faces == 0) return; if (!gfx || !bsp || bsp->num_faces == 0) {
if (!bsp->cell_portals || bsp->num_cell_portals == 0) return; return;
if (!bsp->materials || bsp->num_materials == 0) return; }
if (!bsp->cell_portals || bsp->num_cell_portals == 0) {
pxl8_debug("[BSP] render: no cell_portals (ptr=%p count=%u)", (void*)bsp->cell_portals, bsp->num_cell_portals);
return;
}
if (!bsp->materials || bsp->num_materials == 0) {
pxl8_debug("[BSP] render: no materials (ptr=%p count=%u)", (void*)bsp->materials, bsp->num_materials);
return;
}
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx); const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
const pxl8_mat4* vp = pxl8_3d_get_view_proj(gfx); const pxl8_mat4* vp = pxl8_3d_get_view_proj(gfx);
if (!frustum || !vp) return; if (!frustum || !vp) return;
i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos); i32 camera_leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
if (camera_leaf < 0 || (u32)camera_leaf >= bsp->num_cell_portals) return; if (camera_leaf < 0 || (u32)camera_leaf >= bsp->num_leafs) return;
pxl8_bsp* bsp_mut = (pxl8_bsp*)bsp; pxl8_bsp* bsp_mut = (pxl8_bsp*)bsp;
if (!bsp_mut->render_face_flags) { if (!bsp_mut->render_face_flags) {
@ -837,15 +881,12 @@ void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) {
} }
u32 current_material = 0xFFFFFFFF; u32 current_material = 0xFFFFFFFF;
u32 total_faces = 0;
for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) { for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) {
u32 byte = leaf_id >> 3; if (!(visited[leaf_id >> 3] & (1 << (leaf_id & 7)))) continue;
u32 bit = leaf_id & 7; if (bsp->leafs[leaf_id].contents == -1) continue;
if (!(visited[byte] & (1 << bit))) continue;
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id]; const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_id];
if (!leaf_in_frustum(leaf, frustum)) continue;
for (u32 i = 0; i < leaf->num_marksurfaces; i++) { for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 surf_idx = leaf->first_marksurface + i; u32 surf_idx = leaf->first_marksurface + i;
@ -862,7 +903,6 @@ void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) {
const pxl8_bsp_face* face = &bsp->faces[face_id]; const pxl8_bsp_face* face = &bsp->faces[face_id];
u32 material_id = face->material_id; u32 material_id = face->material_id;
if (material_id >= bsp->num_materials) continue; if (material_id >= bsp->num_materials) continue;
total_faces++;
if (material_id != current_material && mesh->index_count > 0 && current_material < bsp->num_materials) { if (material_id != current_material && mesh->index_count > 0 && current_material < bsp->num_materials) {
pxl8_mat4 identity = pxl8_mat4_identity(); pxl8_mat4 identity = pxl8_mat4_identity();
@ -880,19 +920,61 @@ void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos) {
pxl8_3d_draw_mesh(gfx, mesh, &identity, &bsp->materials[current_material]); pxl8_3d_draw_mesh(gfx, mesh, &identity, &bsp->materials[current_material]);
} }
static u32 debug_count = 0;
if (debug_count++ < 5) {
pxl8_debug("BSP render: %u faces, mesh verts=%u indices=%u", total_faces, mesh->vertex_count, mesh->index_count);
if (mesh->vertex_count > 0) {
pxl8_vertex* v = &mesh->vertices[0];
pxl8_debug(" vert[0]: pos=(%.1f,%.1f,%.1f) uv=(%.2f,%.2f)",
v->position.x, v->position.y, v->position.z, v->u, v->v);
}
}
pxl8_bsp_pvs_destroy(&pvs); pxl8_bsp_pvs_destroy(&pvs);
pxl8_free(visited); pxl8_free(visited);
pxl8_free(cell_windows); pxl8_free(cell_windows);
pxl8_free(queue); pxl8_free(queue);
pxl8_mesh_destroy(mesh); pxl8_mesh_destroy(mesh);
} }
u32 pxl8_bsp_face_count(const pxl8_bsp* bsp) {
if (!bsp) return 0;
return bsp->num_faces;
}
pxl8_vec3 pxl8_bsp_face_normal(const pxl8_bsp* bsp, u32 face_id) {
pxl8_vec3 up = {0, 1, 0};
if (!bsp || face_id >= bsp->num_faces) return up;
const pxl8_bsp_face* face = &bsp->faces[face_id];
if (face->plane_id >= bsp->num_planes) return up;
pxl8_vec3 normal = bsp->planes[face->plane_id].normal;
if (face->side) {
normal.x = -normal.x;
normal.y = -normal.y;
normal.z = -normal.z;
}
return normal;
}
void pxl8_bsp_face_set_material(pxl8_bsp* bsp, u32 face_id, u16 material_id) {
if (!bsp || face_id >= bsp->num_faces) return;
bsp->faces[face_id].material_id = material_id;
}
void pxl8_bsp_set_material(pxl8_bsp* bsp, u16 material_id, const pxl8_gfx_material* material) {
if (!bsp || !material) return;
if (material_id >= bsp->num_materials) {
u32 new_count = material_id + 1;
pxl8_gfx_material* new_materials = pxl8_realloc(bsp->materials, new_count * sizeof(pxl8_gfx_material));
if (!new_materials) return;
for (u32 i = bsp->num_materials; i < new_count; i++) {
memset(&new_materials[i], 0, sizeof(pxl8_gfx_material));
}
bsp->materials = new_materials;
bsp->num_materials = new_count;
}
bsp->materials[material_id] = *material;
}
void pxl8_bsp_set_wireframe(pxl8_bsp* bsp, bool wireframe) {
if (!bsp || !bsp->materials) return;
for (u32 i = 0; i < bsp->num_materials; i++) {
bsp->materials[i].wireframe = wireframe;
}
}

View file

@ -136,22 +136,25 @@ typedef struct pxl8_bsp {
extern "C" { extern "C" {
#endif #endif
pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp); pxl8_bsp_pvs pxl8_bsp_decompress_pvs(const pxl8_bsp* bsp, i32 leaf);
void pxl8_bsp_destroy(pxl8_bsp* bsp); void pxl8_bsp_destroy(pxl8_bsp* bsp);
u32 pxl8_bsp_face_count(const pxl8_bsp* bsp);
pxl8_vec3 pxl8_bsp_face_normal(const pxl8_bsp* bsp, u32 face_id);
void pxl8_bsp_face_set_material(pxl8_bsp* bsp, u32 face_id, u16 material_id);
i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos); i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos);
bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to); bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to);
bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos);
pxl8_bsp_pvs pxl8_bsp_decompress_pvs(const pxl8_bsp* bsp, i32 leaf); pxl8_vec3 pxl8_bsp_trace(const pxl8_bsp* bsp, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
pxl8_bsp_lightmap pxl8_bsp_lightmap_mapped(u8 width, u8 height, u32 offset);
pxl8_bsp_lightmap pxl8_bsp_lightmap_uniform(u8 r, u8 g, u8 b);
pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp);
void pxl8_bsp_pvs_destroy(pxl8_bsp_pvs* pvs); void pxl8_bsp_pvs_destroy(pxl8_bsp_pvs* pvs);
bool pxl8_bsp_pvs_is_visible(const pxl8_bsp_pvs* pvs, i32 leaf); bool pxl8_bsp_pvs_is_visible(const pxl8_bsp_pvs* pvs, i32 leaf);
pxl8_bsp_lightmap pxl8_bsp_lightmap_uniform(u8 r, u8 g, u8 b);
pxl8_bsp_lightmap pxl8_bsp_lightmap_mapped(u8 width, u8 height, u32 offset);
pxl8_bsp_lightmap_sample pxl8_bsp_sample_lightmap(const pxl8_bsp* bsp, u32 face_idx, f32 u, f32 v);
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, const pxl8_gfx_material* material);
void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos); void pxl8_bsp_render(pxl8_gfx* gfx, const pxl8_bsp* bsp, pxl8_vec3 camera_pos);
void pxl8_bsp_render_face(pxl8_gfx* gfx, const pxl8_bsp* bsp, u32 face_id, const pxl8_gfx_material* material);
pxl8_bsp_lightmap_sample pxl8_bsp_sample_lightmap(const pxl8_bsp* bsp, u32 face_idx, f32 u, f32 v);
void pxl8_bsp_set_material(pxl8_bsp* bsp, u16 material_id, const pxl8_gfx_material* material);
void pxl8_bsp_set_wireframe(pxl8_bsp* bsp, bool wireframe);
#ifdef __cplusplus #ifdef __cplusplus
} }

44
src/world/pxl8_chunk.c Normal file
View file

@ -0,0 +1,44 @@
#include "pxl8_chunk.h"
#include "pxl8_mem.h"
pxl8_chunk* pxl8_chunk_create_vxl(i32 cx, i32 cy, i32 cz) {
pxl8_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_chunk));
if (!chunk) return NULL;
chunk->type = PXL8_CHUNK_VXL;
chunk->cx = cx;
chunk->cy = cy;
chunk->cz = cz;
chunk->voxel = pxl8_voxel_chunk_create();
if (!chunk->voxel) {
pxl8_free(chunk);
return NULL;
}
return chunk;
}
pxl8_chunk* pxl8_chunk_create_bsp(u32 id) {
pxl8_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_chunk));
if (!chunk) return NULL;
chunk->type = PXL8_CHUNK_BSP;
chunk->id = id;
chunk->bsp = NULL;
return chunk;
}
void pxl8_chunk_destroy(pxl8_chunk* chunk) {
if (!chunk) return;
if (chunk->type == PXL8_CHUNK_VXL && chunk->voxel) {
pxl8_voxel_chunk_destroy(chunk->voxel);
} else if (chunk->type == PXL8_CHUNK_BSP && chunk->bsp) {
pxl8_bsp_destroy(chunk->bsp);
}
pxl8_free(chunk);
}

33
src/world/pxl8_chunk.h Normal file
View file

@ -0,0 +1,33 @@
#pragma once
#include "pxl8_bsp.h"
#include "pxl8_types.h"
#include "pxl8_voxel.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum pxl8_chunk_type {
PXL8_CHUNK_VXL,
PXL8_CHUNK_BSP
} pxl8_chunk_type;
typedef struct pxl8_chunk {
pxl8_chunk_type type;
u32 id;
u32 version;
i32 cx, cy, cz;
union {
pxl8_voxel_chunk* voxel;
pxl8_bsp* bsp;
};
} pxl8_chunk;
pxl8_chunk* pxl8_chunk_create_vxl(i32 cx, i32 cy, i32 cz);
pxl8_chunk* pxl8_chunk_create_bsp(u32 id);
void pxl8_chunk_destroy(pxl8_chunk* chunk);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,538 @@
#include "pxl8_chunk_cache.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_bytes.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
#define PXL8_VOXEL_CHUNK_VOLUME (PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE)
static pxl8_chunk_cache_entry* find_entry_vxl(pxl8_chunk_cache* cache, i32 cx, i32 cy, i32 cz) {
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (e->valid && e->chunk && e->chunk->type == PXL8_CHUNK_VXL &&
e->chunk->cx == cx && e->chunk->cy == cy && e->chunk->cz == cz) {
return e;
}
}
return NULL;
}
static pxl8_chunk_cache_entry* find_entry_bsp(pxl8_chunk_cache* cache, u32 id) {
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (e->valid && e->chunk && e->chunk->type == PXL8_CHUNK_BSP &&
e->chunk->id == id) {
return e;
}
}
return NULL;
}
static pxl8_chunk_cache_entry* alloc_entry(pxl8_chunk_cache* cache) {
if (cache->entry_count < PXL8_CHUNK_CACHE_SIZE) {
pxl8_chunk_cache_entry* e = &cache->entries[cache->entry_count++];
memset(e, 0, sizeof(*e));
return e;
}
for (u32 i = 0; i < PXL8_CHUNK_CACHE_SIZE; i++) {
if (!cache->entries[i].valid) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
memset(e, 0, sizeof(*e));
return e;
}
}
u64 oldest = cache->entries[0].last_used;
u32 slot = 0;
for (u32 i = 1; i < PXL8_CHUNK_CACHE_SIZE; i++) {
if (cache->entries[i].last_used < oldest) {
oldest = cache->entries[i].last_used;
slot = i;
}
}
pxl8_chunk_cache_entry* e = &cache->entries[slot];
if (e->chunk) pxl8_chunk_destroy(e->chunk);
if (e->mesh) pxl8_mesh_destroy(e->mesh);
memset(e, 0, sizeof(*e));
return e;
}
static void assembly_reset(pxl8_chunk_assembly* a) {
a->type = PXL8_CHUNK_VXL;
a->id = 0;
a->cx = 0;
a->cy = 0;
a->cz = 0;
a->version = 0;
a->fragment_count = 0;
a->fragments_received = 0;
a->data_size = 0;
a->active = false;
a->complete = false;
}
static void assembly_init(pxl8_chunk_assembly* a, const pxl8_chunk_msg_header* hdr) {
assembly_reset(a);
a->type = hdr->chunk_type == PXL8_CHUNK_TYPE_BSP ? PXL8_CHUNK_BSP : PXL8_CHUNK_VXL;
a->id = hdr->id;
a->cx = hdr->cx;
a->cy = hdr->cy;
a->cz = hdr->cz;
a->version = hdr->version;
a->fragment_count = hdr->fragment_count;
a->active = true;
u32 needed = PXL8_CHUNK_MAX_PAYLOAD * hdr->fragment_count;
if (a->data_capacity < needed) {
a->data_capacity = needed;
a->data = pxl8_realloc(a->data, a->data_capacity);
}
}
static bool rle_decode_voxel(const u8* src, usize src_len, pxl8_voxel_chunk* chunk) {
usize src_pos = 0;
usize dst_pos = 0;
while (src_pos + 1 < src_len && dst_pos < PXL8_VOXEL_CHUNK_VOLUME) {
u8 block = src[src_pos++];
u8 run_minus_one = src[src_pos++];
usize run = (usize)run_minus_one + 1;
for (usize i = 0; i < run && dst_pos < PXL8_VOXEL_CHUNK_VOLUME; i++) {
i32 x = (i32)(dst_pos % PXL8_VOXEL_CHUNK_SIZE);
i32 y = (i32)((dst_pos / PXL8_VOXEL_CHUNK_SIZE) % PXL8_VOXEL_CHUNK_SIZE);
i32 z = (i32)(dst_pos / (PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE));
pxl8_voxel_set(chunk, x, y, z, block);
dst_pos++;
}
}
return dst_pos == PXL8_VOXEL_CHUNK_VOLUME;
}
static pxl8_result deserialize_vertex(pxl8_stream* s, pxl8_bsp_vertex* v) {
v->position.x = pxl8_read_f32_be(s);
v->position.y = pxl8_read_f32_be(s);
v->position.z = pxl8_read_f32_be(s);
return PXL8_OK;
}
static pxl8_result deserialize_edge(pxl8_stream* s, pxl8_bsp_edge* e) {
e->vertex[0] = pxl8_read_u16_be(s);
e->vertex[1] = pxl8_read_u16_be(s);
return PXL8_OK;
}
static pxl8_result deserialize_plane(pxl8_stream* s, pxl8_bsp_plane* p) {
p->normal.x = pxl8_read_f32_be(s);
p->normal.y = pxl8_read_f32_be(s);
p->normal.z = pxl8_read_f32_be(s);
p->dist = pxl8_read_f32_be(s);
p->type = (i32)pxl8_read_u32_be(s);
return PXL8_OK;
}
static pxl8_result deserialize_face(pxl8_stream* s, pxl8_bsp_face* f) {
f->first_edge = pxl8_read_u32_be(s);
f->lightmap_offset = pxl8_read_u32_be(s);
f->num_edges = pxl8_read_u16_be(s);
f->plane_id = pxl8_read_u16_be(s);
f->side = pxl8_read_u16_be(s);
pxl8_read_bytes(s, f->styles, 4);
f->material_id = pxl8_read_u16_be(s);
f->aabb_min.x = pxl8_read_f32_be(s);
f->aabb_min.y = pxl8_read_f32_be(s);
f->aabb_min.z = pxl8_read_f32_be(s);
f->aabb_max.x = pxl8_read_f32_be(s);
f->aabb_max.y = pxl8_read_f32_be(s);
f->aabb_max.z = pxl8_read_f32_be(s);
return PXL8_OK;
}
static pxl8_result deserialize_node(pxl8_stream* s, pxl8_bsp_node* n) {
n->children[0] = (i32)pxl8_read_u32_be(s);
n->children[1] = (i32)pxl8_read_u32_be(s);
n->first_face = pxl8_read_u16_be(s);
n->maxs[0] = (i16)pxl8_read_u16_be(s);
n->maxs[1] = (i16)pxl8_read_u16_be(s);
n->maxs[2] = (i16)pxl8_read_u16_be(s);
n->mins[0] = (i16)pxl8_read_u16_be(s);
n->mins[1] = (i16)pxl8_read_u16_be(s);
n->mins[2] = (i16)pxl8_read_u16_be(s);
n->num_faces = pxl8_read_u16_be(s);
n->plane_id = pxl8_read_u32_be(s);
return PXL8_OK;
}
static pxl8_result deserialize_leaf(pxl8_stream* s, pxl8_bsp_leaf* l) {
pxl8_read_bytes(s, l->ambient_level, 4);
l->contents = (i32)pxl8_read_u32_be(s);
l->first_marksurface = pxl8_read_u16_be(s);
l->maxs[0] = (i16)pxl8_read_u16_be(s);
l->maxs[1] = (i16)pxl8_read_u16_be(s);
l->maxs[2] = (i16)pxl8_read_u16_be(s);
l->mins[0] = (i16)pxl8_read_u16_be(s);
l->mins[1] = (i16)pxl8_read_u16_be(s);
l->mins[2] = (i16)pxl8_read_u16_be(s);
l->num_marksurfaces = pxl8_read_u16_be(s);
l->visofs = (i32)pxl8_read_u32_be(s);
return PXL8_OK;
}
static pxl8_result deserialize_portal(pxl8_stream* s, pxl8_bsp_portal* p) {
p->x0 = pxl8_read_f32_be(s);
p->z0 = pxl8_read_f32_be(s);
p->x1 = pxl8_read_f32_be(s);
p->z1 = pxl8_read_f32_be(s);
p->target_leaf = pxl8_read_u32_be(s);
return PXL8_OK;
}
static pxl8_result deserialize_cell_portals(pxl8_stream* s, pxl8_bsp_cell_portals* cp) {
cp->num_portals = pxl8_read_u8(s);
pxl8_read_u8(s);
pxl8_read_u8(s);
pxl8_read_u8(s);
for (int i = 0; i < 4; i++) {
deserialize_portal(s, &cp->portals[i]);
}
return PXL8_OK;
}
static pxl8_bsp* assembly_to_bsp(pxl8_chunk_assembly* a) {
if (!a->complete || a->data_size < 44) {
return NULL;
}
pxl8_bsp* bsp = pxl8_calloc(1, sizeof(pxl8_bsp));
if (!bsp) return NULL;
pxl8_stream s = pxl8_stream_create(a->data, (u32)a->data_size);
pxl8_bsp_wire_header wire_hdr;
pxl8_protocol_deserialize_bsp_wire_header(a->data, 44, &wire_hdr);
s.offset = 44;
pxl8_debug("[CLIENT] Wire header: verts=%u edges=%u faces=%u planes=%u nodes=%u leafs=%u surfedges=%u visdata=%u",
wire_hdr.num_vertices, wire_hdr.num_edges, wire_hdr.num_faces,
wire_hdr.num_planes, wire_hdr.num_nodes, wire_hdr.num_leafs,
wire_hdr.num_surfedges, wire_hdr.visdata_size);
if (wire_hdr.num_vertices > 0) {
bsp->vertices = pxl8_calloc(wire_hdr.num_vertices, sizeof(pxl8_bsp_vertex));
bsp->num_vertices = wire_hdr.num_vertices;
for (u32 i = 0; i < wire_hdr.num_vertices; i++) {
deserialize_vertex(&s, &bsp->vertices[i]);
}
}
if (wire_hdr.num_edges > 0) {
bsp->edges = pxl8_calloc(wire_hdr.num_edges, sizeof(pxl8_bsp_edge));
bsp->num_edges = wire_hdr.num_edges;
for (u32 i = 0; i < wire_hdr.num_edges; i++) {
deserialize_edge(&s, &bsp->edges[i]);
}
}
if (wire_hdr.num_surfedges > 0) {
bsp->surfedges = pxl8_calloc(wire_hdr.num_surfedges, sizeof(i32));
bsp->num_surfedges = wire_hdr.num_surfedges;
for (u32 i = 0; i < wire_hdr.num_surfedges; i++) {
bsp->surfedges[i] = (i32)pxl8_read_u32_be(&s);
}
}
if (wire_hdr.num_planes > 0) {
bsp->planes = pxl8_calloc(wire_hdr.num_planes, sizeof(pxl8_bsp_plane));
bsp->num_planes = wire_hdr.num_planes;
for (u32 i = 0; i < wire_hdr.num_planes; i++) {
deserialize_plane(&s, &bsp->planes[i]);
}
}
if (wire_hdr.num_faces > 0) {
bsp->faces = pxl8_calloc(wire_hdr.num_faces, sizeof(pxl8_bsp_face));
bsp->num_faces = wire_hdr.num_faces;
for (u32 i = 0; i < wire_hdr.num_faces; i++) {
deserialize_face(&s, &bsp->faces[i]);
}
}
if (wire_hdr.num_nodes > 0) {
bsp->nodes = pxl8_calloc(wire_hdr.num_nodes, sizeof(pxl8_bsp_node));
bsp->num_nodes = wire_hdr.num_nodes;
for (u32 i = 0; i < wire_hdr.num_nodes; i++) {
deserialize_node(&s, &bsp->nodes[i]);
}
}
if (wire_hdr.num_leafs > 0) {
bsp->leafs = pxl8_calloc(wire_hdr.num_leafs, sizeof(pxl8_bsp_leaf));
bsp->num_leafs = wire_hdr.num_leafs;
for (u32 i = 0; i < wire_hdr.num_leafs; i++) {
deserialize_leaf(&s, &bsp->leafs[i]);
}
}
if (wire_hdr.num_marksurfaces > 0) {
bsp->marksurfaces = pxl8_calloc(wire_hdr.num_marksurfaces, sizeof(u16));
bsp->num_marksurfaces = wire_hdr.num_marksurfaces;
for (u32 i = 0; i < wire_hdr.num_marksurfaces; i++) {
bsp->marksurfaces[i] = pxl8_read_u16_be(&s);
}
}
if (wire_hdr.num_cell_portals > 0) {
bsp->cell_portals = pxl8_calloc(wire_hdr.num_cell_portals, sizeof(pxl8_bsp_cell_portals));
bsp->num_cell_portals = wire_hdr.num_cell_portals;
for (u32 i = 0; i < wire_hdr.num_cell_portals; i++) {
deserialize_cell_portals(&s, &bsp->cell_portals[i]);
}
}
if (wire_hdr.visdata_size > 0) {
bsp->visdata = pxl8_malloc(wire_hdr.visdata_size);
bsp->visdata_size = wire_hdr.visdata_size;
pxl8_read_bytes(&s, bsp->visdata, wire_hdr.visdata_size);
}
if (wire_hdr.num_vertex_lights > 0) {
bsp->vertex_lights = pxl8_calloc(wire_hdr.num_vertex_lights, sizeof(u32));
bsp->num_vertex_lights = wire_hdr.num_vertex_lights;
for (u32 i = 0; i < wire_hdr.num_vertex_lights; i++) {
bsp->vertex_lights[i] = pxl8_read_u32_be(&s);
}
}
pxl8_debug("Deserialized BSP: %u verts, %u faces, %u nodes, %u leafs",
bsp->num_vertices, bsp->num_faces, bsp->num_nodes, bsp->num_leafs);
return bsp;
}
static pxl8_result assemble_vxl(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a) {
pxl8_chunk_cache_entry* entry = find_entry_vxl(cache, a->cx, a->cy, a->cz);
if (!entry) {
entry = alloc_entry(cache);
entry->chunk = pxl8_chunk_create_vxl(a->cx, a->cy, a->cz);
entry->valid = true;
}
entry->chunk->version = a->version;
entry->mesh_dirty = true;
entry->last_used = cache->frame_counter;
if (entry->mesh) {
pxl8_mesh_destroy(entry->mesh);
entry->mesh = NULL;
}
pxl8_voxel_chunk_clear(entry->chunk->voxel);
if (!rle_decode_voxel(a->data, a->data_size, entry->chunk->voxel)) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
assembly_reset(a);
return PXL8_OK;
}
static pxl8_result assemble_bsp(pxl8_chunk_cache* cache, pxl8_chunk_assembly* a) {
pxl8_debug("[CLIENT] assemble_bsp: id=%u data_size=%zu", a->id, a->data_size);
pxl8_bsp* bsp = assembly_to_bsp(a);
if (!bsp) {
pxl8_debug("[CLIENT] assemble_bsp: assembly_to_bsp returned NULL!");
assembly_reset(a);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_debug("[CLIENT] assemble_bsp: BSP created with %u verts %u faces", bsp->num_vertices, bsp->num_faces);
pxl8_chunk_cache_entry* entry = find_entry_bsp(cache, a->id);
if (entry) {
if (entry->chunk && entry->chunk->bsp) {
pxl8_bsp_destroy(entry->chunk->bsp);
}
} else {
entry = alloc_entry(cache);
entry->chunk = pxl8_chunk_create_bsp(a->id);
entry->valid = true;
}
entry->chunk->bsp = bsp;
entry->chunk->version = a->version;
entry->last_used = cache->frame_counter;
assembly_reset(a);
return PXL8_OK;
}
pxl8_chunk_cache* pxl8_chunk_cache_create(void) {
pxl8_chunk_cache* cache = pxl8_calloc(1, sizeof(pxl8_chunk_cache));
if (!cache) return NULL;
assembly_reset(&cache->assembly);
return cache;
}
void pxl8_chunk_cache_destroy(pxl8_chunk_cache* cache) {
if (!cache) return;
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (e->chunk) pxl8_chunk_destroy(e->chunk);
if (e->mesh) pxl8_mesh_destroy(e->mesh);
}
pxl8_free(cache->assembly.data);
pxl8_free(cache);
}
pxl8_result pxl8_chunk_cache_receive(pxl8_chunk_cache* cache,
const pxl8_chunk_msg_header* hdr,
const u8* payload, usize len) {
if (!cache || !hdr || !payload) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_chunk_assembly* a = &cache->assembly;
bool new_assembly = !a->active ||
(hdr->chunk_type == PXL8_CHUNK_TYPE_BSP && a->id != hdr->id) ||
(hdr->chunk_type == PXL8_CHUNK_TYPE_VXL &&
(a->cx != hdr->cx || a->cy != hdr->cy || a->cz != hdr->cz)) ||
a->version != hdr->version ||
hdr->fragment_idx == 0;
if (new_assembly) {
assembly_init(a, hdr);
}
if (hdr->fragment_idx >= PXL8_CHUNK_MAX_FRAGMENTS) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
u32 offset = (u32)hdr->fragment_idx * PXL8_CHUNK_MAX_PAYLOAD;
u32 required = offset + (u32)len;
if (required > a->data_capacity) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
memcpy(a->data + offset, payload, len);
if (required > a->data_size) {
a->data_size = required;
}
a->fragments_received++;
if (hdr->flags & PXL8_CHUNK_FLAG_FINAL) {
a->complete = true;
pxl8_debug("[CLIENT] Final fragment received, assembling type=%d data_size=%zu", a->type, a->data_size);
if (a->type == PXL8_CHUNK_BSP) {
return assemble_bsp(cache, a);
} else {
return assemble_vxl(cache, a);
}
}
return PXL8_OK;
}
pxl8_chunk* pxl8_chunk_cache_get_vxl(pxl8_chunk_cache* cache, i32 cx, i32 cy, i32 cz) {
if (!cache) return NULL;
pxl8_chunk_cache_entry* e = find_entry_vxl(cache, cx, cy, cz);
if (e) {
e->last_used = cache->frame_counter;
return e->chunk;
}
return NULL;
}
pxl8_chunk* pxl8_chunk_cache_get_bsp(pxl8_chunk_cache* cache, u32 id) {
if (!cache) return NULL;
pxl8_chunk_cache_entry* e = find_entry_bsp(cache, id);
if (e) {
e->last_used = cache->frame_counter;
return e->chunk;
}
return NULL;
}
pxl8_mesh* pxl8_chunk_cache_get_mesh(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config) {
if (!cache || !registry) return NULL;
pxl8_chunk_cache_entry* entry = find_entry_vxl(cache, cx, cy, cz);
if (!entry || !entry->chunk || !entry->chunk->voxel) return NULL;
if (entry->mesh && !entry->mesh_dirty) {
return entry->mesh;
}
if (entry->mesh) {
pxl8_mesh_destroy(entry->mesh);
entry->mesh = NULL;
}
pxl8_chunk* nx = pxl8_chunk_cache_get_vxl(cache, cx - 1, cy, cz);
pxl8_chunk* px = pxl8_chunk_cache_get_vxl(cache, cx + 1, cy, cz);
pxl8_chunk* ny = pxl8_chunk_cache_get_vxl(cache, cx, cy - 1, cz);
pxl8_chunk* py = pxl8_chunk_cache_get_vxl(cache, cx, cy + 1, cz);
pxl8_chunk* nz = pxl8_chunk_cache_get_vxl(cache, cx, cy, cz - 1);
pxl8_chunk* pz = pxl8_chunk_cache_get_vxl(cache, cx, cy, cz + 1);
const pxl8_voxel_chunk* neighbors[6] = {
nx ? nx->voxel : NULL,
px ? px->voxel : NULL,
ny ? ny->voxel : NULL,
py ? py->voxel : NULL,
nz ? nz->voxel : NULL,
pz ? pz->voxel : NULL
};
entry->mesh = pxl8_voxel_build_mesh(entry->chunk->voxel, neighbors, registry, config);
entry->mesh_dirty = false;
return entry->mesh;
}
void pxl8_chunk_cache_tick(pxl8_chunk_cache* cache) {
if (!cache) return;
cache->frame_counter++;
}
void pxl8_chunk_cache_evict_distant(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz, i32 radius) {
if (!cache) return;
for (u32 i = 0; i < cache->entry_count; i++) {
pxl8_chunk_cache_entry* e = &cache->entries[i];
if (!e->valid || !e->chunk || e->chunk->type != PXL8_CHUNK_VXL) continue;
i32 dx = e->chunk->cx - cx;
i32 dy = e->chunk->cy - cy;
i32 dz = e->chunk->cz - cz;
if (abs(dx) > radius || abs(dy) > radius || abs(dz) > radius) {
pxl8_chunk_destroy(e->chunk);
if (e->mesh) pxl8_mesh_destroy(e->mesh);
e->chunk = NULL;
e->mesh = NULL;
e->valid = false;
}
}
}
void pxl8_chunk_cache_invalidate_meshes(pxl8_chunk_cache* cache) {
if (!cache) return;
for (u32 i = 0; i < cache->entry_count; i++) {
cache->entries[i].mesh_dirty = true;
}
}

View file

@ -0,0 +1,67 @@
#pragma once
#include "pxl8_chunk.h"
#include "pxl8_mesh.h"
#include "pxl8_protocol.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_CHUNK_CACHE_SIZE 64
#define PXL8_CHUNK_MAX_FRAGMENTS 64
#define PXL8_CHUNK_MAX_DATA_SIZE 131072
typedef struct pxl8_chunk_cache_entry {
pxl8_chunk* chunk;
pxl8_mesh* mesh;
u64 last_used;
bool mesh_dirty;
bool valid;
} pxl8_chunk_cache_entry;
typedef struct pxl8_chunk_assembly {
pxl8_chunk_type type;
u32 id;
i32 cx, cy, cz;
u32 version;
u8 fragment_count;
u8 fragments_received;
u8* data;
u32 data_size;
u32 data_capacity;
bool active;
bool complete;
} pxl8_chunk_assembly;
typedef struct pxl8_chunk_cache {
pxl8_chunk_cache_entry entries[PXL8_CHUNK_CACHE_SIZE];
pxl8_chunk_assembly assembly;
u32 entry_count;
u64 frame_counter;
} pxl8_chunk_cache;
pxl8_chunk_cache* pxl8_chunk_cache_create(void);
void pxl8_chunk_cache_destroy(pxl8_chunk_cache* cache);
pxl8_result pxl8_chunk_cache_receive(pxl8_chunk_cache* cache,
const pxl8_chunk_msg_header* hdr,
const u8* payload, usize len);
pxl8_chunk* pxl8_chunk_cache_get_vxl(pxl8_chunk_cache* cache, i32 cx, i32 cy, i32 cz);
pxl8_chunk* pxl8_chunk_cache_get_bsp(pxl8_chunk_cache* cache, u32 id);
pxl8_mesh* pxl8_chunk_cache_get_mesh(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config);
void pxl8_chunk_cache_tick(pxl8_chunk_cache* cache);
void pxl8_chunk_cache_evict_distant(pxl8_chunk_cache* cache,
i32 cx, i32 cy, i32 cz, i32 radius);
void pxl8_chunk_cache_invalidate_meshes(pxl8_chunk_cache* cache);
#ifdef __cplusplus
}
#endif

496
src/world/pxl8_entity.c Normal file
View file

@ -0,0 +1,496 @@
#include "pxl8_entity.h"
#include <string.h>
#include "pxl8_mem.h"
#define PXL8_ENTITY_COMPONENT_NAME_MAX 32
#define PXL8_ENTITY_RELATIONSHIP_NAME_MAX 32
typedef struct pxl8_component_type {
char name[PXL8_ENTITY_COMPONENT_NAME_MAX];
u32 size;
} pxl8_component_type;
typedef struct pxl8_component_storage {
u32* sparse;
void* dense_data;
pxl8_entity* dense_entities;
u32 count;
} pxl8_component_storage;
typedef struct pxl8_relationship_type {
char name[PXL8_ENTITY_RELATIONSHIP_NAME_MAX];
} pxl8_relationship_type;
typedef struct pxl8_relationship_entry {
pxl8_entity subject;
pxl8_entity object;
pxl8_entity_relationship rel;
u32 next_by_subject;
u32 next_by_object;
} pxl8_relationship_entry;
struct pxl8_entity_pool {
u32* generations;
u32* free_list;
u32 free_count;
u32 capacity;
u32 alive_count;
pxl8_component_type* component_types;
pxl8_component_storage* component_storage;
u32 component_type_count;
u32 component_type_capacity;
pxl8_relationship_type* relationship_types;
u32 relationship_type_count;
u32 relationship_type_capacity;
pxl8_relationship_entry* relationships;
u32* rel_by_subject;
u32* rel_by_object;
u32 relationship_count;
u32 relationship_capacity;
};
pxl8_entity_pool* pxl8_entity_pool_create(u32 capacity) {
pxl8_entity_pool* pool = pxl8_calloc(1, sizeof(pxl8_entity_pool));
if (!pool) return NULL;
pool->capacity = capacity;
pool->generations = pxl8_calloc(capacity, sizeof(u32));
pool->free_list = pxl8_malloc(capacity * sizeof(u32));
if (!pool->generations || !pool->free_list) {
pxl8_entity_pool_destroy(pool);
return NULL;
}
for (u32 i = 0; i < capacity; i++) {
pool->free_list[i] = capacity - 1 - i;
}
pool->free_count = capacity;
pool->component_type_capacity = 16;
pool->component_types = pxl8_calloc(pool->component_type_capacity, sizeof(pxl8_component_type));
pool->component_storage = pxl8_calloc(pool->component_type_capacity, sizeof(pxl8_component_storage));
pool->relationship_type_capacity = 16;
pool->relationship_types = pxl8_calloc(pool->relationship_type_capacity, sizeof(pxl8_relationship_type));
pool->relationship_capacity = 256;
pool->relationships = pxl8_malloc(pool->relationship_capacity * sizeof(pxl8_relationship_entry));
pool->rel_by_subject = pxl8_malloc(capacity * sizeof(u32));
pool->rel_by_object = pxl8_malloc(capacity * sizeof(u32));
for (u32 i = 0; i < capacity; i++) {
pool->rel_by_subject[i] = UINT32_MAX;
pool->rel_by_object[i] = UINT32_MAX;
}
return pool;
}
void pxl8_entity_pool_clear(pxl8_entity_pool* pool) {
if (!pool) return;
for (u32 i = 0; i < pool->capacity; i++) {
pool->generations[i] = 0;
pool->free_list[i] = pool->capacity - 1 - i;
pool->rel_by_subject[i] = UINT32_MAX;
pool->rel_by_object[i] = UINT32_MAX;
}
pool->free_count = pool->capacity;
pool->alive_count = 0;
for (u32 i = 0; i < pool->component_type_count; i++) {
pool->component_storage[i].count = 0;
}
pool->relationship_count = 0;
}
void pxl8_entity_pool_destroy(pxl8_entity_pool* pool) {
if (!pool) return;
for (u32 i = 0; i < pool->component_type_count; i++) {
pxl8_free(pool->component_storage[i].sparse);
pxl8_free(pool->component_storage[i].dense_data);
pxl8_free(pool->component_storage[i].dense_entities);
}
pxl8_free(pool->component_types);
pxl8_free(pool->component_storage);
pxl8_free(pool->relationship_types);
pxl8_free(pool->relationships);
pxl8_free(pool->rel_by_subject);
pxl8_free(pool->rel_by_object);
pxl8_free(pool->generations);
pxl8_free(pool->free_list);
pxl8_free(pool);
}
pxl8_entity pxl8_entity_spawn(pxl8_entity_pool* pool) {
if (!pool || pool->free_count == 0) return PXL8_ENTITY_INVALID;
u32 idx = pool->free_list[--pool->free_count];
pool->generations[idx]++;
pool->alive_count++;
return (pxl8_entity){ .idx = idx, .gen = pool->generations[idx] };
}
void pxl8_entity_despawn(pxl8_entity_pool* pool, pxl8_entity e) {
if (!pool || !pxl8_entity_alive(pool, e)) return;
for (u32 i = 0; i < pool->component_type_count; i++) {
pxl8_entity_component_remove(pool, e, i + 1);
}
pool->free_list[pool->free_count++] = e.idx;
pool->generations[e.idx]++;
pool->alive_count--;
}
bool pxl8_entity_alive(const pxl8_entity_pool* pool, pxl8_entity e) {
if (!pool || e.idx >= pool->capacity) return false;
return pool->generations[e.idx] == e.gen && e.gen != 0;
}
u32 pxl8_entity_count(const pxl8_entity_pool* pool) {
return pool ? pool->alive_count : 0;
}
pxl8_entity_component pxl8_entity_component_register(pxl8_entity_pool* pool, const char* name, u32 size) {
if (!pool || !name || size == 0) return PXL8_ENTITY_COMPONENT_INVALID;
pxl8_entity_component existing = pxl8_entity_component_find(pool, name);
if (existing != PXL8_ENTITY_COMPONENT_INVALID) return existing;
if (pool->component_type_count >= pool->component_type_capacity) {
u32 new_capacity = pool->component_type_capacity * 2;
pxl8_component_type* new_types = pxl8_realloc(pool->component_types, new_capacity * sizeof(pxl8_component_type));
pxl8_component_storage* new_storage = pxl8_realloc(pool->component_storage, new_capacity * sizeof(pxl8_component_storage));
if (!new_types || !new_storage) return PXL8_ENTITY_COMPONENT_INVALID;
pool->component_types = new_types;
pool->component_storage = new_storage;
memset(&pool->component_types[pool->component_type_capacity], 0, (new_capacity - pool->component_type_capacity) * sizeof(pxl8_component_type));
memset(&pool->component_storage[pool->component_type_capacity], 0, (new_capacity - pool->component_type_capacity) * sizeof(pxl8_component_storage));
pool->component_type_capacity = new_capacity;
}
u32 type_idx = pool->component_type_count++;
strncpy(pool->component_types[type_idx].name, name, PXL8_ENTITY_COMPONENT_NAME_MAX - 1);
pool->component_types[type_idx].size = size;
pxl8_component_storage* storage = &pool->component_storage[type_idx];
storage->sparse = pxl8_malloc(pool->capacity * sizeof(u32));
storage->dense_data = pxl8_malloc(pool->capacity * size);
storage->dense_entities = pxl8_malloc(pool->capacity * sizeof(pxl8_entity));
storage->count = 0;
for (u32 i = 0; i < pool->capacity; i++) {
storage->sparse[i] = UINT32_MAX;
}
return type_idx + 1;
}
pxl8_entity_component pxl8_entity_component_find(const pxl8_entity_pool* pool, const char* name) {
if (!pool || !name) return PXL8_ENTITY_COMPONENT_INVALID;
for (u32 i = 0; i < pool->component_type_count; i++) {
if (strcmp(pool->component_types[i].name, name) == 0) {
return i + 1;
}
}
return PXL8_ENTITY_COMPONENT_INVALID;
}
const char* pxl8_entity_component_name(const pxl8_entity_pool* pool, pxl8_entity_component comp) {
if (!pool || comp == 0 || comp > pool->component_type_count) return NULL;
return pool->component_types[comp - 1].name;
}
void* pxl8_entity_component_add(pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp) {
if (!pool || !pxl8_entity_alive(pool, e)) return NULL;
if (comp == 0 || comp > pool->component_type_count) return NULL;
u32 type_idx = comp - 1;
pxl8_component_storage* storage = &pool->component_storage[type_idx];
u32 size = pool->component_types[type_idx].size;
if (storage->sparse[e.idx] != UINT32_MAX) {
return (u8*)storage->dense_data + storage->sparse[e.idx] * size;
}
u32 dense_idx = storage->count++;
storage->sparse[e.idx] = dense_idx;
storage->dense_entities[dense_idx] = e;
void* data = (u8*)storage->dense_data + dense_idx * size;
memset(data, 0, size);
return data;
}
void* pxl8_entity_component_get(const pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp) {
if (!pool || !pxl8_entity_alive(pool, e)) return NULL;
if (comp == 0 || comp > pool->component_type_count) return NULL;
u32 type_idx = comp - 1;
const pxl8_component_storage* storage = &pool->component_storage[type_idx];
if (storage->sparse[e.idx] == UINT32_MAX) return NULL;
u32 size = pool->component_types[type_idx].size;
return (u8*)storage->dense_data + storage->sparse[e.idx] * size;
}
void pxl8_entity_component_remove(pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp) {
if (!pool || !pxl8_entity_alive(pool, e)) return;
if (comp == 0 || comp > pool->component_type_count) return;
u32 type_idx = comp - 1;
pxl8_component_storage* storage = &pool->component_storage[type_idx];
u32 size = pool->component_types[type_idx].size;
u32 dense_idx = storage->sparse[e.idx];
if (dense_idx == UINT32_MAX) return;
u32 last_idx = storage->count - 1;
if (dense_idx != last_idx) {
pxl8_entity last_entity = storage->dense_entities[last_idx];
memcpy((u8*)storage->dense_data + dense_idx * size,
(u8*)storage->dense_data + last_idx * size, size);
storage->dense_entities[dense_idx] = last_entity;
storage->sparse[last_entity.idx] = dense_idx;
}
storage->sparse[e.idx] = UINT32_MAX;
storage->count--;
}
bool pxl8_entity_component_has(const pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp) {
if (!pool || !pxl8_entity_alive(pool, e)) return false;
if (comp == 0 || comp > pool->component_type_count) return false;
return pool->component_storage[comp - 1].sparse[e.idx] != UINT32_MAX;
}
pxl8_entity_relationship pxl8_entity_relationship_register(pxl8_entity_pool* pool, const char* name) {
if (!pool || !name) return PXL8_ENTITY_RELATIONSHIP_INVALID;
pxl8_entity_relationship existing = pxl8_entity_relationship_find(pool, name);
if (existing != PXL8_ENTITY_RELATIONSHIP_INVALID) return existing;
if (pool->relationship_type_count >= pool->relationship_type_capacity) {
u32 new_capacity = pool->relationship_type_capacity * 2;
pxl8_relationship_type* new_types = pxl8_realloc(pool->relationship_types, new_capacity * sizeof(pxl8_relationship_type));
if (!new_types) return PXL8_ENTITY_RELATIONSHIP_INVALID;
pool->relationship_types = new_types;
memset(&pool->relationship_types[pool->relationship_type_capacity], 0, (new_capacity - pool->relationship_type_capacity) * sizeof(pxl8_relationship_type));
pool->relationship_type_capacity = new_capacity;
}
u32 type_idx = pool->relationship_type_count++;
strncpy(pool->relationship_types[type_idx].name, name, PXL8_ENTITY_RELATIONSHIP_NAME_MAX - 1);
return type_idx + 1;
}
pxl8_entity_relationship pxl8_entity_relationship_find(const pxl8_entity_pool* pool, const char* name) {
if (!pool || !name) return PXL8_ENTITY_RELATIONSHIP_INVALID;
for (u32 i = 0; i < pool->relationship_type_count; i++) {
if (strcmp(pool->relationship_types[i].name, name) == 0) {
return i + 1;
}
}
return PXL8_ENTITY_RELATIONSHIP_INVALID;
}
const char* pxl8_entity_relationship_name(const pxl8_entity_pool* pool, pxl8_entity_relationship rel) {
if (!pool || rel == 0 || rel > pool->relationship_type_count) return NULL;
return pool->relationship_types[rel - 1].name;
}
void pxl8_entity_relationship_add(pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity object) {
if (!pool) return;
if (!pxl8_entity_alive(pool, subject) || !pxl8_entity_alive(pool, object)) return;
if (rel == 0 || rel > pool->relationship_type_count) return;
if (pxl8_entity_relationship_has(pool, subject, rel, object)) return;
if (pool->relationship_count >= pool->relationship_capacity) {
u32 new_capacity = pool->relationship_capacity * 2;
pxl8_relationship_entry* new_rels = pxl8_realloc(pool->relationships, new_capacity * sizeof(pxl8_relationship_entry));
if (!new_rels) return;
pool->relationships = new_rels;
pool->relationship_capacity = new_capacity;
}
u32 entry_idx = pool->relationship_count++;
pxl8_relationship_entry* entry = &pool->relationships[entry_idx];
entry->subject = subject;
entry->object = object;
entry->rel = rel;
entry->next_by_subject = pool->rel_by_subject[subject.idx];
pool->rel_by_subject[subject.idx] = entry_idx;
entry->next_by_object = pool->rel_by_object[object.idx];
pool->rel_by_object[object.idx] = entry_idx;
}
void pxl8_entity_relationship_remove(pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity object) {
if (!pool) return;
if (rel == 0 || rel > pool->relationship_type_count) return;
u32* prev_ptr = &pool->rel_by_subject[subject.idx];
u32 idx = *prev_ptr;
while (idx != UINT32_MAX) {
pxl8_relationship_entry* entry = &pool->relationships[idx];
if (pxl8_entity_eq(entry->subject, subject) &&
pxl8_entity_eq(entry->object, object) &&
entry->rel == rel) {
*prev_ptr = entry->next_by_subject;
u32* obj_prev = &pool->rel_by_object[object.idx];
while (*obj_prev != UINT32_MAX) {
if (*obj_prev == idx) {
*obj_prev = entry->next_by_object;
break;
}
obj_prev = &pool->relationships[*obj_prev].next_by_object;
}
if (idx != pool->relationship_count - 1) {
u32 last_idx = pool->relationship_count - 1;
pxl8_relationship_entry* last = &pool->relationships[last_idx];
u32* last_subj_prev = &pool->rel_by_subject[last->subject.idx];
while (*last_subj_prev != UINT32_MAX) {
if (*last_subj_prev == last_idx) {
*last_subj_prev = idx;
break;
}
last_subj_prev = &pool->relationships[*last_subj_prev].next_by_subject;
}
u32* last_obj_prev = &pool->rel_by_object[last->object.idx];
while (*last_obj_prev != UINT32_MAX) {
if (*last_obj_prev == last_idx) {
*last_obj_prev = idx;
break;
}
last_obj_prev = &pool->relationships[*last_obj_prev].next_by_object;
}
*entry = *last;
}
pool->relationship_count--;
return;
}
prev_ptr = &entry->next_by_subject;
idx = *prev_ptr;
}
}
bool pxl8_entity_relationship_has(const pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity object) {
if (!pool) return false;
if (rel == 0 || rel > pool->relationship_type_count) return false;
u32 idx = pool->rel_by_subject[subject.idx];
while (idx != UINT32_MAX) {
const pxl8_relationship_entry* entry = &pool->relationships[idx];
if (pxl8_entity_eq(entry->subject, subject) &&
pxl8_entity_eq(entry->object, object) &&
entry->rel == rel) {
return true;
}
idx = entry->next_by_subject;
}
return false;
}
u32 pxl8_entity_relationship_subjects(const pxl8_entity_pool* pool, pxl8_entity object, pxl8_entity_relationship rel, pxl8_entity* out, u32 max) {
if (!pool || !out || max == 0) return 0;
if (rel == 0 || rel > pool->relationship_type_count) return 0;
u32 count = 0;
u32 idx = pool->rel_by_object[object.idx];
while (idx != UINT32_MAX && count < max) {
const pxl8_relationship_entry* entry = &pool->relationships[idx];
if (pxl8_entity_eq(entry->object, object) && entry->rel == rel) {
out[count++] = entry->subject;
}
idx = entry->next_by_object;
}
return count;
}
u32 pxl8_entity_relationship_objects(const pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity* out, u32 max) {
if (!pool || !out || max == 0) return 0;
if (rel == 0 || rel > pool->relationship_type_count) return 0;
u32 count = 0;
u32 idx = pool->rel_by_subject[subject.idx];
while (idx != UINT32_MAX && count < max) {
const pxl8_relationship_entry* entry = &pool->relationships[idx];
if (pxl8_entity_eq(entry->subject, subject) && entry->rel == rel) {
out[count++] = entry->object;
}
idx = entry->next_by_subject;
}
return count;
}
void pxl8_entity_each(pxl8_entity_pool* pool, pxl8_entity_component comp, pxl8_entity_each_fn fn, void* ctx) {
if (!pool || !fn) return;
if (comp == 0 || comp > pool->component_type_count) return;
pxl8_component_storage* storage = &pool->component_storage[comp - 1];
for (u32 i = 0; i < storage->count; i++) {
pxl8_entity e = storage->dense_entities[i];
if (pxl8_entity_alive(pool, e)) {
fn(pool, e, ctx);
}
}
}
void pxl8_entity_each_with(pxl8_entity_pool* pool, const pxl8_entity_component* comps, u32 count, pxl8_entity_each_fn fn, void* ctx) {
if (!pool || !comps || !fn || count == 0) return;
u32 smallest_idx = 0;
u32 smallest_count = UINT32_MAX;
for (u32 i = 0; i < count; i++) {
if (comps[i] == 0 || comps[i] > pool->component_type_count) return;
u32 storage_count = pool->component_storage[comps[i] - 1].count;
if (storage_count < smallest_count) {
smallest_count = storage_count;
smallest_idx = i;
}
}
pxl8_component_storage* storage = &pool->component_storage[comps[smallest_idx] - 1];
for (u32 i = 0; i < storage->count; i++) {
pxl8_entity e = storage->dense_entities[i];
if (!pxl8_entity_alive(pool, e)) continue;
bool has_all = true;
for (u32 j = 0; j < count && has_all; j++) {
if (j != smallest_idx) {
has_all = pxl8_entity_component_has(pool, e, comps[j]);
}
}
if (has_all) {
fn(pool, e, ctx);
}
}
}

67
src/world/pxl8_entity.h Normal file
View file

@ -0,0 +1,67 @@
#pragma once
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct pxl8_entity_pool pxl8_entity_pool;
typedef struct pxl8_entity {
u32 idx;
u32 gen;
} pxl8_entity;
#define PXL8_ENTITY_INVALID ((pxl8_entity){0, 0})
typedef u32 pxl8_entity_component;
typedef u32 pxl8_entity_relationship;
#define PXL8_ENTITY_COMPONENT_INVALID 0
#define PXL8_ENTITY_RELATIONSHIP_INVALID 0
pxl8_entity_pool* pxl8_entity_pool_create(u32 capacity);
void pxl8_entity_pool_clear(pxl8_entity_pool* pool);
void pxl8_entity_pool_destroy(pxl8_entity_pool* pool);
pxl8_entity pxl8_entity_spawn(pxl8_entity_pool* pool);
void pxl8_entity_despawn(pxl8_entity_pool* pool, pxl8_entity e);
bool pxl8_entity_alive(const pxl8_entity_pool* pool, pxl8_entity e);
u32 pxl8_entity_count(const pxl8_entity_pool* pool);
pxl8_entity_component pxl8_entity_component_register(pxl8_entity_pool* pool, const char* name, u32 size);
pxl8_entity_component pxl8_entity_component_find(const pxl8_entity_pool* pool, const char* name);
const char* pxl8_entity_component_name(const pxl8_entity_pool* pool, pxl8_entity_component comp);
void* pxl8_entity_component_add(pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp);
void* pxl8_entity_component_get(const pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp);
void pxl8_entity_component_remove(pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp);
bool pxl8_entity_component_has(const pxl8_entity_pool* pool, pxl8_entity e, pxl8_entity_component comp);
pxl8_entity_relationship pxl8_entity_relationship_register(pxl8_entity_pool* pool, const char* name);
pxl8_entity_relationship pxl8_entity_relationship_find(const pxl8_entity_pool* pool, const char* name);
const char* pxl8_entity_relationship_name(const pxl8_entity_pool* pool, pxl8_entity_relationship rel);
void pxl8_entity_relationship_add(pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity object);
void pxl8_entity_relationship_remove(pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity object);
bool pxl8_entity_relationship_has(const pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity object);
u32 pxl8_entity_relationship_subjects(const pxl8_entity_pool* pool, pxl8_entity object, pxl8_entity_relationship rel, pxl8_entity* out, u32 max);
u32 pxl8_entity_relationship_objects(const pxl8_entity_pool* pool, pxl8_entity subject, pxl8_entity_relationship rel, pxl8_entity* out, u32 max);
typedef void (*pxl8_entity_each_fn)(pxl8_entity_pool* pool, pxl8_entity e, void* ctx);
void pxl8_entity_each(pxl8_entity_pool* pool, pxl8_entity_component comp, pxl8_entity_each_fn fn, void* ctx);
void pxl8_entity_each_with(pxl8_entity_pool* pool, const pxl8_entity_component* comps, u32 count, pxl8_entity_each_fn fn, void* ctx);
static inline bool pxl8_entity_valid(pxl8_entity e) {
return e.idx != 0 || e.gen != 0;
}
static inline bool pxl8_entity_eq(pxl8_entity a, pxl8_entity b) {
return a.idx == b.idx && a.gen == b.gen;
}
#ifdef __cplusplus
}
#endif

406
src/world/pxl8_voxel.c Normal file
View file

@ -0,0 +1,406 @@
#include "pxl8_voxel.h"
#include <string.h>
#include "pxl8_mem.h"
#define PXL8_VOXEL_CHUNK_VOLUME (PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE)
typedef struct pxl8_block_def {
char name[32];
u8 texture_id;
pxl8_voxel_geometry geometry;
bool registered;
} pxl8_block_def;
struct pxl8_block_registry {
pxl8_block_def blocks[PXL8_BLOCK_COUNT];
};
struct pxl8_voxel_chunk {
pxl8_block blocks[PXL8_VOXEL_CHUNK_VOLUME];
};
static inline u32 voxel_index(i32 x, i32 y, i32 z) {
return (u32)(x + y * PXL8_VOXEL_CHUNK_SIZE + z * PXL8_VOXEL_CHUNK_SIZE * PXL8_VOXEL_CHUNK_SIZE);
}
static inline bool voxel_in_bounds(i32 x, i32 y, i32 z) {
return x >= 0 && x < PXL8_VOXEL_CHUNK_SIZE &&
y >= 0 && y < PXL8_VOXEL_CHUNK_SIZE &&
z >= 0 && z < PXL8_VOXEL_CHUNK_SIZE;
}
pxl8_voxel_chunk* pxl8_voxel_chunk_create(void) {
pxl8_voxel_chunk* chunk = pxl8_calloc(1, sizeof(pxl8_voxel_chunk));
return chunk;
}
void pxl8_voxel_chunk_clear(pxl8_voxel_chunk* chunk) {
if (!chunk) return;
memset(chunk->blocks, 0, sizeof(chunk->blocks));
}
void pxl8_voxel_chunk_destroy(pxl8_voxel_chunk* chunk) {
pxl8_free(chunk);
}
pxl8_block pxl8_voxel_get(const pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z) {
if (!chunk || !voxel_in_bounds(x, y, z)) return PXL8_BLOCK_AIR;
return chunk->blocks[voxel_index(x, y, z)];
}
void pxl8_voxel_set(pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z, pxl8_block block) {
if (!chunk || !voxel_in_bounds(x, y, z)) return;
chunk->blocks[voxel_index(x, y, z)] = block;
}
void pxl8_voxel_fill(pxl8_voxel_chunk* chunk, pxl8_block block) {
if (!chunk) return;
memset(chunk->blocks, block, sizeof(chunk->blocks));
}
pxl8_block_registry* pxl8_block_registry_create(void) {
pxl8_block_registry* registry = pxl8_calloc(1, sizeof(pxl8_block_registry));
return registry;
}
void pxl8_block_registry_destroy(pxl8_block_registry* registry) {
pxl8_free(registry);
}
void pxl8_block_registry_register(pxl8_block_registry* registry, pxl8_block id, const char* name, u8 texture_id, pxl8_voxel_geometry geo) {
if (!registry || id == PXL8_BLOCK_AIR) return;
pxl8_block_def* def = &registry->blocks[id];
strncpy(def->name, name ? name : "", sizeof(def->name) - 1);
def->texture_id = texture_id;
def->geometry = geo;
def->registered = true;
}
const char* pxl8_block_registry_name(const pxl8_block_registry* registry, pxl8_block id) {
if (!registry || !registry->blocks[id].registered) return NULL;
return registry->blocks[id].name;
}
pxl8_voxel_geometry pxl8_block_registry_geometry(const pxl8_block_registry* registry, pxl8_block id) {
if (!registry || !registry->blocks[id].registered) return PXL8_VOXEL_GEOMETRY_CUBE;
return registry->blocks[id].geometry;
}
u8 pxl8_block_registry_texture(const pxl8_block_registry* registry, pxl8_block id) {
if (!registry || !registry->blocks[id].registered) return 0;
return registry->blocks[id].texture_id;
}
static bool block_is_opaque(pxl8_block block) {
return block != PXL8_BLOCK_AIR;
}
static pxl8_block get_block_or_neighbor(const pxl8_voxel_chunk* chunk, const pxl8_voxel_chunk** neighbors, i32 x, i32 y, i32 z) {
if (voxel_in_bounds(x, y, z)) {
return chunk->blocks[voxel_index(x, y, z)];
}
i32 nx = x, ny = y, nz = z;
i32 neighbor_idx = -1;
if (x < 0) { neighbor_idx = 0; nx = x + PXL8_VOXEL_CHUNK_SIZE; }
else if (x >= PXL8_VOXEL_CHUNK_SIZE) { neighbor_idx = 1; nx = x - PXL8_VOXEL_CHUNK_SIZE; }
else if (y < 0) { neighbor_idx = 2; ny = y + PXL8_VOXEL_CHUNK_SIZE; }
else if (y >= PXL8_VOXEL_CHUNK_SIZE) { neighbor_idx = 3; ny = y - PXL8_VOXEL_CHUNK_SIZE; }
else if (z < 0) { neighbor_idx = 4; nz = z + PXL8_VOXEL_CHUNK_SIZE; }
else if (z >= PXL8_VOXEL_CHUNK_SIZE) { neighbor_idx = 5; nz = z - PXL8_VOXEL_CHUNK_SIZE; }
if (neighbor_idx >= 0 && neighbors && neighbors[neighbor_idx]) {
return pxl8_voxel_get(neighbors[neighbor_idx], nx, ny, nz);
}
return PXL8_BLOCK_AIR;
}
static f32 compute_ao(const pxl8_voxel_chunk* chunk, const pxl8_voxel_chunk** neighbors,
i32 x, i32 y, i32 z, i32 dx1, i32 dy1, i32 dz1, i32 dx2, i32 dy2, i32 dz2) {
bool side1 = block_is_opaque(get_block_or_neighbor(chunk, neighbors, x + dx1, y + dy1, z + dz1));
bool side2 = block_is_opaque(get_block_or_neighbor(chunk, neighbors, x + dx2, y + dy2, z + dz2));
bool corner = block_is_opaque(get_block_or_neighbor(chunk, neighbors, x + dx1 + dx2, y + dy1 + dy2, z + dz1 + dz2));
if (side1 && side2) return 0.0f;
return (3.0f - (f32)side1 - (f32)side2 - (f32)corner) / 3.0f;
}
static void add_face_vertices(pxl8_mesh* mesh, const pxl8_voxel_mesh_config* config,
pxl8_vec3 pos, i32 face, u8 texture_id,
f32 ao0, f32 ao1, f32 ao2, f32 ao3) {
static const pxl8_vec3 face_normals[6] = {
{-1, 0, 0}, { 1, 0, 0},
{ 0, -1, 0}, { 0, 1, 0},
{ 0, 0, -1}, { 0, 0, 1}
};
static const i32 face_vertices[6][4][3] = {
{{0,0,1}, {0,0,0}, {0,1,0}, {0,1,1}},
{{1,0,0}, {1,0,1}, {1,1,1}, {1,1,0}},
{{0,0,0}, {1,0,0}, {1,0,1}, {0,0,1}},
{{0,1,1}, {1,1,1}, {1,1,0}, {0,1,0}},
{{0,0,0}, {0,1,0}, {1,1,0}, {1,0,0}},
{{1,0,1}, {1,1,1}, {0,1,1}, {0,0,1}}
};
static const f32 face_uvs[4][2] = {
{0, 1}, {1, 1}, {1, 0}, {0, 0}
};
f32 ao_values[4] = {ao0, ao1, ao2, ao3};
f32 ao_strength = config->ambient_occlusion ? config->ao_strength : 0.0f;
f32 tex_scale = config->texture_scale;
pxl8_vec3 normal = face_normals[face];
u16 indices[4];
for (i32 i = 0; i < 4; i++) {
pxl8_vertex v = {0};
v.position.x = pos.x + (f32)face_vertices[face][i][0];
v.position.y = pos.y + (f32)face_vertices[face][i][1];
v.position.z = pos.z + (f32)face_vertices[face][i][2];
v.normal = normal;
v.u = face_uvs[i][0] * tex_scale;
v.v = face_uvs[i][1] * tex_scale;
v.color = texture_id;
v.light = (u8)(255.0f * (1.0f - ao_strength * (1.0f - ao_values[i])));
indices[i] = pxl8_mesh_push_vertex(mesh, v);
}
if (ao0 + ao2 > ao1 + ao3) {
pxl8_mesh_push_quad(mesh, indices[0], indices[1], indices[2], indices[3]);
} else {
pxl8_mesh_push_triangle(mesh, indices[0], indices[1], indices[2]);
pxl8_mesh_push_triangle(mesh, indices[0], indices[2], indices[3]);
}
}
static void add_cube_faces(pxl8_mesh* mesh, const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors, const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config,
i32 x, i32 y, i32 z, pxl8_block block) {
u8 texture_id = pxl8_block_registry_texture(registry, block);
pxl8_vec3 pos = {(f32)x, (f32)y, (f32)z};
static const i32 face_dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
for (i32 face = 0; face < 6; face++) {
i32 nx = x + face_dirs[face][0];
i32 ny = y + face_dirs[face][1];
i32 nz = z + face_dirs[face][2];
pxl8_block neighbor = get_block_or_neighbor(chunk, neighbors, nx, ny, nz);
if (block_is_opaque(neighbor)) continue;
f32 ao[4] = {1.0f, 1.0f, 1.0f, 1.0f};
if (config->ambient_occlusion) {
switch (face) {
case 0:
ao[0] = compute_ao(chunk, neighbors, x-1, y, z, 0, -1, 0, 0, 0, 1);
ao[1] = compute_ao(chunk, neighbors, x-1, y, z, 0, -1, 0, 0, 0, -1);
ao[2] = compute_ao(chunk, neighbors, x-1, y, z, 0, 1, 0, 0, 0, -1);
ao[3] = compute_ao(chunk, neighbors, x-1, y, z, 0, 1, 0, 0, 0, 1);
break;
case 1:
ao[0] = compute_ao(chunk, neighbors, x+1, y, z, 0, -1, 0, 0, 0, -1);
ao[1] = compute_ao(chunk, neighbors, x+1, y, z, 0, -1, 0, 0, 0, 1);
ao[2] = compute_ao(chunk, neighbors, x+1, y, z, 0, 1, 0, 0, 0, 1);
ao[3] = compute_ao(chunk, neighbors, x+1, y, z, 0, 1, 0, 0, 0, -1);
break;
case 2:
ao[0] = compute_ao(chunk, neighbors, x, y-1, z, -1, 0, 0, 0, 0, -1);
ao[1] = compute_ao(chunk, neighbors, x, y-1, z, 1, 0, 0, 0, 0, -1);
ao[2] = compute_ao(chunk, neighbors, x, y-1, z, 1, 0, 0, 0, 0, 1);
ao[3] = compute_ao(chunk, neighbors, x, y-1, z, -1, 0, 0, 0, 0, 1);
break;
case 3:
ao[0] = compute_ao(chunk, neighbors, x, y+1, z, -1, 0, 0, 0, 0, 1);
ao[1] = compute_ao(chunk, neighbors, x, y+1, z, 1, 0, 0, 0, 0, 1);
ao[2] = compute_ao(chunk, neighbors, x, y+1, z, 1, 0, 0, 0, 0, -1);
ao[3] = compute_ao(chunk, neighbors, x, y+1, z, -1, 0, 0, 0, 0, -1);
break;
case 4:
ao[0] = compute_ao(chunk, neighbors, x, y, z-1, -1, 0, 0, 0, 1, 0);
ao[1] = compute_ao(chunk, neighbors, x, y, z-1, -1, 0, 0, 0, -1, 0);
ao[2] = compute_ao(chunk, neighbors, x, y, z-1, 1, 0, 0, 0, -1, 0);
ao[3] = compute_ao(chunk, neighbors, x, y, z-1, 1, 0, 0, 0, 1, 0);
break;
case 5:
ao[0] = compute_ao(chunk, neighbors, x, y, z+1, 1, 0, 0, 0, -1, 0);
ao[1] = compute_ao(chunk, neighbors, x, y, z+1, 1, 0, 0, 0, 1, 0);
ao[2] = compute_ao(chunk, neighbors, x, y, z+1, -1, 0, 0, 0, 1, 0);
ao[3] = compute_ao(chunk, neighbors, x, y, z+1, -1, 0, 0, 0, -1, 0);
break;
}
}
add_face_vertices(mesh, config, pos, face, texture_id, ao[0], ao[1], ao[2], ao[3]);
}
}
static void add_slab_faces(pxl8_mesh* mesh, const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors, const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config,
i32 x, i32 y, i32 z, pxl8_block block, bool top) {
u8 texture_id = pxl8_block_registry_texture(registry, block);
f32 y_offset = top ? 0.5f : 0.0f;
pxl8_vec3 pos = {(f32)x, (f32)y + y_offset, (f32)z};
static const i32 horiz_faces[4] = {0, 1, 4, 5};
static const i32 face_dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
for (i32 i = 0; i < 4; i++) {
i32 face = horiz_faces[i];
i32 nx = x + face_dirs[face][0];
i32 nz = z + face_dirs[face][2];
pxl8_block neighbor = get_block_or_neighbor(chunk, neighbors, nx, y, nz);
if (block_is_opaque(neighbor)) continue;
add_face_vertices(mesh, config, pos, face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
i32 top_face = 3;
i32 bot_face = 2;
if (top) {
pxl8_block above = get_block_or_neighbor(chunk, neighbors, x, y + 1, z);
if (!block_is_opaque(above)) {
add_face_vertices(mesh, config, pos, top_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
add_face_vertices(mesh, config, (pxl8_vec3){(f32)x, (f32)y + 0.5f, (f32)z}, bot_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
} else {
add_face_vertices(mesh, config, pos, top_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
pxl8_block below = get_block_or_neighbor(chunk, neighbors, x, y - 1, z);
if (!block_is_opaque(below)) {
add_face_vertices(mesh, config, pos, bot_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
}
}
static void add_slope_faces(pxl8_mesh* mesh, const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors, const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config,
i32 x, i32 y, i32 z, pxl8_block block, i32 direction) {
u8 texture_id = pxl8_block_registry_texture(registry, block);
pxl8_vec3 pos = {(f32)x, (f32)y, (f32)z};
pxl8_block below = get_block_or_neighbor(chunk, neighbors, x, y - 1, z);
if (!block_is_opaque(below)) {
add_face_vertices(mesh, config, pos, 2, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
static const i32 dir_to_back_face[4] = {5, 4, 1, 0};
i32 back_face = dir_to_back_face[direction];
static const i32 face_dirs[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}
};
i32 bx = x + face_dirs[back_face][0];
i32 bz = z + face_dirs[back_face][2];
pxl8_block back_neighbor = get_block_or_neighbor(chunk, neighbors, bx, y, bz);
if (!block_is_opaque(back_neighbor)) {
add_face_vertices(mesh, config, pos, back_face, texture_id, 1.0f, 1.0f, 1.0f, 1.0f);
}
pxl8_vertex verts[4];
memset(verts, 0, sizeof(verts));
static const f32 slope_verts[4][4][3] = {
{{0,0,0}, {1,0,0}, {1,1,1}, {0,1,1}},
{{0,0,1}, {1,0,1}, {1,1,0}, {0,1,0}},
{{1,0,0}, {1,0,1}, {0,1,1}, {0,1,0}},
{{0,0,0}, {0,0,1}, {1,1,1}, {1,1,0}}
};
for (i32 i = 0; i < 4; i++) {
verts[i].position.x = pos.x + slope_verts[direction][i][0];
verts[i].position.y = pos.y + slope_verts[direction][i][1];
verts[i].position.z = pos.z + slope_verts[direction][i][2];
verts[i].color = texture_id;
verts[i].light = 255;
}
static const pxl8_vec3 slope_normals[4] = {
{0, 0.707f, -0.707f}, {0, 0.707f, 0.707f},
{-0.707f, 0.707f, 0}, {0.707f, 0.707f, 0}
};
for (i32 i = 0; i < 4; i++) {
verts[i].normal = slope_normals[direction];
}
u16 i0 = pxl8_mesh_push_vertex(mesh, verts[0]);
u16 i1 = pxl8_mesh_push_vertex(mesh, verts[1]);
u16 i2 = pxl8_mesh_push_vertex(mesh, verts[2]);
u16 i3 = pxl8_mesh_push_vertex(mesh, verts[3]);
pxl8_mesh_push_quad(mesh, i0, i1, i2, i3);
}
pxl8_mesh* pxl8_voxel_build_mesh(const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config) {
if (!chunk || !registry) return NULL;
pxl8_voxel_mesh_config cfg = config ? *config : PXL8_VOXEL_MESH_CONFIG_DEFAULT;
u32 max_faces = PXL8_VOXEL_CHUNK_VOLUME * 6;
pxl8_mesh* mesh = pxl8_mesh_create(max_faces * 4, max_faces * 6);
if (!mesh) return NULL;
for (i32 z = 0; z < PXL8_VOXEL_CHUNK_SIZE; z++) {
for (i32 y = 0; y < PXL8_VOXEL_CHUNK_SIZE; y++) {
for (i32 x = 0; x < PXL8_VOXEL_CHUNK_SIZE; x++) {
pxl8_block block = chunk->blocks[voxel_index(x, y, z)];
if (block == PXL8_BLOCK_AIR) continue;
pxl8_voxel_geometry geo = pxl8_block_registry_geometry(registry, block);
switch (geo) {
case PXL8_VOXEL_GEOMETRY_CUBE:
add_cube_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block);
break;
case PXL8_VOXEL_GEOMETRY_SLAB_BOTTOM:
add_slab_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, false);
break;
case PXL8_VOXEL_GEOMETRY_SLAB_TOP:
add_slab_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, true);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_NORTH:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 0);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_SOUTH:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 1);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_EAST:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 2);
break;
case PXL8_VOXEL_GEOMETRY_SLOPE_WEST:
add_slope_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block, 3);
break;
case PXL8_VOXEL_GEOMETRY_STAIRS_NORTH:
case PXL8_VOXEL_GEOMETRY_STAIRS_SOUTH:
case PXL8_VOXEL_GEOMETRY_STAIRS_EAST:
case PXL8_VOXEL_GEOMETRY_STAIRS_WEST:
add_cube_faces(mesh, chunk, neighbors, registry, &cfg, x, y, z, block);
break;
}
}
}
}
return mesh;
}

67
src/world/pxl8_voxel.h Normal file
View file

@ -0,0 +1,67 @@
#pragma once
#include "pxl8_mesh.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_VOXEL_CHUNK_SIZE 32
#define PXL8_BLOCK_COUNT 256
typedef struct pxl8_voxel_chunk pxl8_voxel_chunk;
typedef struct pxl8_block_registry pxl8_block_registry;
typedef u8 pxl8_block;
#define PXL8_BLOCK_AIR 0
typedef enum pxl8_voxel_geometry {
PXL8_VOXEL_GEOMETRY_CUBE,
PXL8_VOXEL_GEOMETRY_SLAB_BOTTOM,
PXL8_VOXEL_GEOMETRY_SLAB_TOP,
PXL8_VOXEL_GEOMETRY_SLOPE_NORTH,
PXL8_VOXEL_GEOMETRY_SLOPE_SOUTH,
PXL8_VOXEL_GEOMETRY_SLOPE_EAST,
PXL8_VOXEL_GEOMETRY_SLOPE_WEST,
PXL8_VOXEL_GEOMETRY_STAIRS_NORTH,
PXL8_VOXEL_GEOMETRY_STAIRS_SOUTH,
PXL8_VOXEL_GEOMETRY_STAIRS_EAST,
PXL8_VOXEL_GEOMETRY_STAIRS_WEST
} pxl8_voxel_geometry;
typedef struct pxl8_voxel_mesh_config {
bool ambient_occlusion;
f32 ao_strength;
f32 texture_scale;
} pxl8_voxel_mesh_config;
#define PXL8_VOXEL_MESH_CONFIG_DEFAULT ((pxl8_voxel_mesh_config){ \
.ambient_occlusion = true, \
.ao_strength = 0.2f, \
.texture_scale = 1.0f \
})
pxl8_voxel_chunk* pxl8_voxel_chunk_create(void);
void pxl8_voxel_chunk_clear(pxl8_voxel_chunk* chunk);
void pxl8_voxel_chunk_destroy(pxl8_voxel_chunk* chunk);
pxl8_block pxl8_voxel_get(const pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z);
void pxl8_voxel_set(pxl8_voxel_chunk* chunk, i32 x, i32 y, i32 z, pxl8_block block);
void pxl8_voxel_fill(pxl8_voxel_chunk* chunk, pxl8_block block);
pxl8_block_registry* pxl8_block_registry_create(void);
void pxl8_block_registry_destroy(pxl8_block_registry* registry);
void pxl8_block_registry_register(pxl8_block_registry* registry, pxl8_block id, const char* name, u8 texture_id, pxl8_voxel_geometry geo);
const char* pxl8_block_registry_name(const pxl8_block_registry* registry, pxl8_block id);
pxl8_voxel_geometry pxl8_block_registry_geometry(const pxl8_block_registry* registry, pxl8_block id);
u8 pxl8_block_registry_texture(const pxl8_block_registry* registry, pxl8_block id);
pxl8_mesh* pxl8_voxel_build_mesh(const pxl8_voxel_chunk* chunk,
const pxl8_voxel_chunk** neighbors,
const pxl8_block_registry* registry,
const pxl8_voxel_mesh_config* config);
#ifdef __cplusplus
}
#endif

View file

@ -1,413 +1,125 @@
#include "pxl8_world.h" #include "pxl8_world.h"
#include <stdlib.h> #include <stdio.h>
#include <string.h>
#include "pxl8_bsp.h"
#include "pxl8_gen.h"
#include "pxl8_log.h" #include "pxl8_log.h"
#include "pxl8_math.h"
#include "pxl8_mem.h" #include "pxl8_mem.h"
#define PXL8_WORLD_ENTITY_CAPACITY 256
struct pxl8_world { struct pxl8_world {
pxl8_bsp bsp; pxl8_chunk* active_chunk;
bool loaded; pxl8_block_registry* block_registry;
pxl8_chunk_cache* chunk_cache;
pxl8_entity_pool* entities;
}; };
pxl8_world* pxl8_world_create(void) { pxl8_world* pxl8_world_create(void) {
pxl8_world* world = (pxl8_world*)pxl8_calloc(1, sizeof(pxl8_world)); pxl8_world* world = pxl8_calloc(1, sizeof(pxl8_world));
if (!world) { if (!world) return NULL;
pxl8_error("Failed to allocate world");
world->block_registry = pxl8_block_registry_create();
world->chunk_cache = pxl8_chunk_cache_create();
world->entities = pxl8_entity_pool_create(PXL8_WORLD_ENTITY_CAPACITY);
if (!world->block_registry || !world->chunk_cache || !world->entities) {
pxl8_world_destroy(world);
return NULL; return NULL;
} }
world->loaded = false;
return world; return world;
} }
void pxl8_world_destroy(pxl8_world* world) { void pxl8_world_destroy(pxl8_world* world) {
if (!world) return; if (!world) return;
if (world->loaded) { pxl8_block_registry_destroy(world->block_registry);
pxl8_bsp_destroy(&world->bsp); pxl8_chunk_cache_destroy(world->chunk_cache);
} pxl8_entity_pool_destroy(world->entities);
pxl8_free(world); pxl8_free(world);
} }
pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params) { pxl8_chunk_cache* pxl8_world_chunk_cache(pxl8_world* world) {
if (!world || !gfx || !params) { if (!world) return NULL;
pxl8_error("Invalid arguments to pxl8_world_generate"); return world->chunk_cache;
return PXL8_ERROR_INVALID_ARGUMENT;
}
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
memset(&world->bsp, 0, sizeof(pxl8_bsp));
pxl8_result result = pxl8_procgen(&world->bsp, params);
if (result != PXL8_OK) {
pxl8_error("Failed to generate world: %d", result);
pxl8_bsp_destroy(&world->bsp);
return result;
}
world->loaded = true;
return PXL8_OK;
} }
pxl8_result pxl8_world_load(pxl8_world* world, const char* path) { pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world) {
if (!world || !path) return PXL8_ERROR_INVALID_ARGUMENT; if (!world) return NULL;
return world->active_chunk;
if (world->loaded) {
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
}
memset(&world->bsp, 0, sizeof(pxl8_bsp));
pxl8_result result = pxl8_bsp_load(path, &world->bsp);
if (result != PXL8_OK) {
pxl8_error("Failed to load world: %s", path);
return result;
}
world->loaded = true;
pxl8_info("Loaded world: %s", path);
return PXL8_OK;
} }
void pxl8_world_unload(pxl8_world* world) { void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_chunk* chunk) {
if (!world || !world->loaded) return; if (!world) return;
world->active_chunk = chunk;
pxl8_bsp_destroy(&world->bsp);
world->loaded = false;
} }
pxl8_result pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, u32 count) { pxl8_block_registry* pxl8_world_block_registry(pxl8_world* world) {
if (!world || !world->loaded || !textures || count == 0) { if (!world) return NULL;
return PXL8_ERROR_INVALID_ARGUMENT; return world->block_registry;
} }
pxl8_bsp* bsp = &world->bsp; pxl8_entity_pool* pxl8_world_entities(pxl8_world* world) {
if (!world) return NULL;
return world->entities;
}
u32 max_materials = count * 6; pxl8_entity pxl8_world_spawn(pxl8_world* world) {
bsp->materials = pxl8_calloc(max_materials, sizeof(pxl8_gfx_material)); if (!world || !world->entities) return PXL8_ENTITY_INVALID;
if (!bsp->materials) { return pxl8_entity_spawn(world->entities);
return PXL8_ERROR_OUT_OF_MEMORY;
}
bsp->num_materials = 0;
for (u32 face_idx = 0; face_idx < bsp->num_faces; face_idx++) {
pxl8_bsp_face* face = &bsp->faces[face_idx];
pxl8_vec3 normal = bsp->planes[face->plane_id].normal;
u32 matched_texture_idx = count;
for (u32 tex_idx = 0; tex_idx < count; tex_idx++) {
if (textures[tex_idx].rule && textures[tex_idx].rule(&normal, face, bsp)) {
matched_texture_idx = tex_idx;
break;
}
}
if (matched_texture_idx >= count) {
pxl8_warn("No texture rule matched for face %u", face_idx);
continue;
}
const pxl8_world_texture* matched = &textures[matched_texture_idx];
pxl8_vec3 u_axis, v_axis;
if (fabsf(normal.y) > 0.9f) {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else if (fabsf(normal.x) > 0.7f) {
u_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 0.0f, 1.0f};
} else {
u_axis = (pxl8_vec3){1.0f, 0.0f, 0.0f};
v_axis = (pxl8_vec3){0.0f, 1.0f, 0.0f};
}
u32 material_idx = bsp->num_materials;
bool found_existing = false;
for (u32 i = 0; i < bsp->num_materials; i++) {
if (strcmp(bsp->materials[i].name, matched->name) == 0 &&
bsp->materials[i].texture_id == matched->texture_id &&
bsp->materials[i].u_axis.x == u_axis.x &&
bsp->materials[i].u_axis.y == u_axis.y &&
bsp->materials[i].u_axis.z == u_axis.z &&
bsp->materials[i].v_axis.x == v_axis.x &&
bsp->materials[i].v_axis.y == v_axis.y &&
bsp->materials[i].v_axis.z == v_axis.z) {
material_idx = i;
found_existing = true;
break;
}
}
if (!found_existing) {
if (bsp->num_materials >= max_materials) {
pxl8_error("Too many unique material entries");
return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_gfx_material* mat = &bsp->materials[material_idx];
memcpy(mat->name, matched->name, sizeof(mat->name));
mat->name[sizeof(mat->name) - 1] = '\0';
mat->texture_id = matched->texture_id;
mat->u_offset = 0.0f;
mat->v_offset = 0.0f;
mat->u_axis = u_axis;
mat->v_axis = v_axis;
mat->alpha = 255;
mat->dither = true;
mat->double_sided = true;
mat->dynamic_lighting = true;
bsp->num_materials++;
}
face->material_id = material_idx;
}
pxl8_info("Applied %u textures to %u faces, created %u materials",
count, bsp->num_faces, bsp->num_materials);
return PXL8_OK;
} }
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius) { bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius) {
if (!world || !world->loaded) return false; (void)radius;
const pxl8_bsp* bsp = &world->bsp; if (!world || !world->active_chunk) return false;
for (u32 i = 0; i < bsp->num_faces; i++) { if (world->active_chunk->type == PXL8_CHUNK_BSP && world->active_chunk->bsp) {
const pxl8_bsp_face* face = &bsp->faces[i]; return pxl8_bsp_point_solid(world->active_chunk->bsp, pos);
const pxl8_bsp_plane* plane = &bsp->planes[face->plane_id];
if (fabsf(plane->normal.y) > 0.7f) {
continue;
}
f32 dist = plane->normal.x * pos.x +
plane->normal.y * pos.y +
plane->normal.z * pos.z - plane->dist;
if (fabsf(dist) > radius) {
continue;
}
pxl8_vec3 closest_point = {
pos.x - plane->normal.x * dist,
pos.y - plane->normal.y * dist,
pos.z - plane->normal.z * dist
};
if (closest_point.x < face->aabb_min.x - radius || closest_point.x > face->aabb_max.x + radius ||
closest_point.y < face->aabb_min.y - radius || closest_point.y > face->aabb_max.y + radius ||
closest_point.z < face->aabb_min.z - radius || closest_point.z > face->aabb_max.z + radius) {
continue;
}
return true;
} }
return false; return false;
} }
bool pxl8_world_is_loaded(const pxl8_world* world) { pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
return world && world->loaded; if (!world || !world->active_chunk) return to;
if (world->active_chunk->type == PXL8_CHUNK_BSP && world->active_chunk->bsp) {
return pxl8_bsp_trace(world->active_chunk->bsp, from, to, radius);
}
return to;
} }
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) { void pxl8_world_update(pxl8_world* world, f32 dt) {
if (!world || !world->loaded) return to; (void)dt;
if (!world) return;
const pxl8_bsp* bsp = &world->bsp; pxl8_chunk_cache_tick(world->chunk_cache);
pxl8_vec3 pos = to;
pxl8_vec3 original_velocity = {to.x - from.x, to.y - from.y, to.z - from.z};
pxl8_vec3 clip_planes[5];
u32 num_planes = 0;
const f32 edge_epsilon = 1.2f;
const f32 radius_min = -radius + edge_epsilon;
const f32 radius_max = radius - edge_epsilon;
for (i32 iteration = 0; iteration < 4; iteration++) {
bool collided = false;
for (u32 i = 0; i < bsp->num_faces; i++) {
const pxl8_bsp_face* face = &bsp->faces[i];
const pxl8_bsp_plane* plane = &bsp->planes[face->plane_id];
if (fabsf(plane->normal.y) > 0.7f) continue;
f32 dist = plane->normal.x * pos.x +
plane->normal.y * pos.y +
plane->normal.z * pos.z - plane->dist;
f32 abs_dist = fabsf(dist);
if (abs_dist > radius) continue;
pxl8_vec3 closest_point = {
pos.x - plane->normal.x * dist,
pos.y - plane->normal.y * dist,
pos.z - plane->normal.z * dist
};
if (closest_point.x < face->aabb_min.x + radius_min || closest_point.x > face->aabb_max.x + radius_max ||
closest_point.y < face->aabb_min.y + radius_min || closest_point.y > face->aabb_max.y + radius_max ||
closest_point.z < face->aabb_min.z + radius_min || closest_point.z > face->aabb_max.z + radius_max) {
continue;
}
f32 penetration = radius - abs_dist;
if (penetration > 0.01f) {
pxl8_vec3 push_dir;
if (dist < 0) {
push_dir.x = -plane->normal.x;
push_dir.y = -plane->normal.y;
push_dir.z = -plane->normal.z;
} else {
push_dir.x = plane->normal.x;
push_dir.y = plane->normal.y;
push_dir.z = plane->normal.z;
}
bool is_new_plane = true;
for (u32 p = 0; p < num_planes; p++) {
if (pxl8_vec3_dot(push_dir, clip_planes[p]) > 0.99f) {
is_new_plane = false;
break;
}
}
if (is_new_plane && num_planes < 5) {
clip_planes[num_planes++] = push_dir;
}
pos.x += push_dir.x * penetration;
pos.y += push_dir.y * penetration;
pos.z += push_dir.z * penetration;
collided = true;
}
}
if (!collided) {
break;
}
if (num_planes >= 3) {
break;
}
}
if (num_planes == 2) {
f32 orig_vel_len_sq = original_velocity.x * original_velocity.x +
original_velocity.y * original_velocity.y +
original_velocity.z * original_velocity.z;
if (orig_vel_len_sq > 0.000001f) {
f32 vdot0 = pxl8_vec3_dot(original_velocity, clip_planes[0]);
f32 vdot1 = pxl8_vec3_dot(original_velocity, clip_planes[1]);
f32 dot0 = fabsf(vdot0);
f32 dot1 = fabsf(vdot1);
pxl8_vec3 slide_vel;
if (dot0 < dot1) {
slide_vel.x = original_velocity.x - clip_planes[0].x * vdot0;
slide_vel.y = original_velocity.y - clip_planes[0].y * vdot0;
slide_vel.z = original_velocity.z - clip_planes[0].z * vdot0;
} else {
slide_vel.x = original_velocity.x - clip_planes[1].x * vdot1;
slide_vel.y = original_velocity.y - clip_planes[1].y * vdot1;
slide_vel.z = original_velocity.z - clip_planes[1].z * vdot1;
}
f32 slide_len = sqrtf(slide_vel.x * slide_vel.x + slide_vel.y * slide_vel.y + slide_vel.z * slide_vel.z);
if (slide_len > 0.01f) {
pos.x += slide_vel.x;
pos.y += slide_vel.y;
pos.z += slide_vel.z;
pxl8_vec3 crease_dir = pxl8_vec3_cross(clip_planes[0], clip_planes[1]);
f32 crease_len = pxl8_vec3_length(crease_dir);
if (crease_len > 0.01f && fabsf(crease_dir.y / crease_len) > 0.9f) {
f32 bias0 = pxl8_vec3_dot(original_velocity, clip_planes[0]);
f32 bias1 = pxl8_vec3_dot(original_velocity, clip_planes[1]);
if (bias0 < 0 && bias1 < 0) {
const f32 corner_push = 0.1f;
pxl8_vec3 push_away = {
clip_planes[0].x + clip_planes[1].x,
clip_planes[0].y + clip_planes[1].y,
clip_planes[0].z + clip_planes[1].z
};
f32 push_len_sq = push_away.x * push_away.x + push_away.y * push_away.y + push_away.z * push_away.z;
if (push_len_sq > 0.000001f) {
f32 inv_push_len = corner_push / sqrtf(push_len_sq);
pos.x += push_away.x * inv_push_len;
pos.y += push_away.y * inv_push_len;
pos.z += push_away.z * inv_push_len;
}
}
}
}
}
}
f32 horizontal_movement_sq = (pos.x - from.x) * (pos.x - from.x) +
(pos.z - from.z) * (pos.z - from.z);
f32 desired_horizontal_sq = (to.x - from.x) * (to.x - from.x) +
(to.z - from.z) * (to.z - from.z);
if (desired_horizontal_sq > 0.01f && horizontal_movement_sq < desired_horizontal_sq * 0.25f) {
const f32 max_step_height = 0.4f;
pxl8_vec3 step_up = pos;
step_up.y += max_step_height;
if (!pxl8_world_check_collision(world, step_up, radius)) {
pxl8_vec3 step_forward = {
step_up.x + (to.x - pos.x),
step_up.y,
step_up.z + (to.z - pos.z)
};
if (!pxl8_world_check_collision(world, step_forward, radius)) {
pos = step_forward;
}
}
}
return pos;
} }
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) { void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
if (!world || !gfx || !world->loaded) { if (!world || !gfx || !world->active_chunk) return;
static int count = 0;
if (count++ < 10) {
pxl8_debug("world_render: early return - world=%p, gfx=%p, loaded=%d",
(void*)world, (void*)gfx, world ? world->loaded : -1);
}
return;
}
pxl8_bsp_render(gfx, &world->bsp, camera_pos); if (world->active_chunk->type == PXL8_CHUNK_BSP && world->active_chunk->bsp) {
} pxl8_bsp_render(gfx, world->active_chunk->bsp, camera_pos);
}
void pxl8_world_set_wireframe(pxl8_world* world, bool enabled) { }
if (!world || !world->loaded) return;
void pxl8_world_sync(pxl8_world* world, pxl8_net* net) {
for (u32 i = 0; i < world->bsp.num_materials; i++) { if (!world || !net) return;
world->bsp.materials[i].wireframe = enabled;
u8 chunk_type = pxl8_net_chunk_type(net);
u32 chunk_id = pxl8_net_chunk_id(net);
if (chunk_type == PXL8_CHUNK_TYPE_BSP && chunk_id != 0) {
pxl8_chunk* chunk = pxl8_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);
}
}
} }
} }

View file

@ -1,38 +1,36 @@
#pragma once #pragma once
#include "pxl8_bsp.h" #include "pxl8_chunk.h"
#include "pxl8_gen.h" #include "pxl8_chunk_cache.h"
#include "pxl8_entity.h"
#include "pxl8_gfx.h" #include "pxl8_gfx.h"
#include "pxl8_math.h" #include "pxl8_math.h"
#include "pxl8_net.h"
#include "pxl8_types.h" #include "pxl8_types.h"
typedef struct pxl8_world pxl8_world;
typedef bool (*pxl8_texture_rule)(const pxl8_vec3* normal, const pxl8_bsp_face* face, const pxl8_bsp* bsp);
typedef struct pxl8_world_texture {
char name[16];
u32 texture_id;
pxl8_texture_rule rule;
} pxl8_world_texture;
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
typedef struct pxl8_world pxl8_world;
pxl8_world* pxl8_world_create(void); pxl8_world* pxl8_world_create(void);
void pxl8_world_destroy(pxl8_world* world); void pxl8_world_destroy(pxl8_world* world);
pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params); pxl8_chunk_cache* pxl8_world_chunk_cache(pxl8_world* world);
pxl8_result pxl8_world_load(pxl8_world* world, const char* path); pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world);
void pxl8_world_unload(pxl8_world* world); void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_chunk* chunk);
pxl8_block_registry* pxl8_world_block_registry(pxl8_world* world);
pxl8_entity_pool* pxl8_world_entities(pxl8_world* world);
pxl8_entity pxl8_world_spawn(pxl8_world* world);
pxl8_result pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, u32 count);
bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius); bool pxl8_world_check_collision(const pxl8_world* world, pxl8_vec3 pos, f32 radius);
bool pxl8_world_is_loaded(const pxl8_world* world);
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);
pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius); pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius);
void pxl8_world_set_wireframe(pxl8_world* world, bool enabled);
void pxl8_world_update(pxl8_world* world, f32 dt);
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);
void pxl8_world_sync(pxl8_world* world, pxl8_net* net);
#ifdef __cplusplus #ifdef __cplusplus
} }