diff --git a/demo/mod/first_person3d.fnl b/demo/mod/first_person3d.fnl index 7df90ea..c66aa46 100644 --- a/demo/mod/first_person3d.fnl +++ b/demo/mod/first_person3d.fnl @@ -11,7 +11,7 @@ (local bob-amount 4.0) (local bob-speed 8.0) (local cam-smoothing 0.25) -(local cell-size 64) +(local chunk-size 64) (local cursor-sensitivity 0.010) (local gravity -800) (local grid-size 64) @@ -32,25 +32,29 @@ (var auto-run-cancel-key nil) (var bob-time 0) (var cam-pitch 0) -(var cam-x 1000) +(var cam-x 416) (var cam-y 64) (var cam-yaw 0) -(var cam-z 1000) +(var cam-z 416) (var camera nil) -(var grounded? true) -(var land-squash 0) -(var light-time 0) -(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 ceiling-tex nil) +(var fireball-mesh nil) +(var floor-tex nil) (var fps-avg 0) (var fps-sample-count 0) -(var fireball-mesh nil) +(var grounded? true) +(var land-squash 0) (var last-dt 0.016) +(var light-time 0) (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 FIREBALL_COLOR 218) @@ -247,43 +251,54 @@ (pxl8.set_palette_rgb (+ FIREBALL_COLOR i) r g b))) (sky.update-gradient 1 2 6 6 10 18) (pxl8.update_palette_deps) - (set camera (pxl8.create_camera_3d)) - (set world (pxl8.create_world)) - (set lights (pxl8.create_lights)) + + (when (not camera) + (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) - (create-fireball-mesh) - (set network (net.Net.new {:port 7777})) - (when network - (network:connect) - (network:spawn cam-x cam-y cam-z cam-yaw cam-pitch)) + (when (not network) + (set network (net.get)) + (when network + (network:spawn cam-x cam-y cam-z cam-yaw cam-pitch))) - (let [result (world:generate { - :type pxl8.PROCGEN_ROOMS - :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)] + (when (not world) + (set world (pxl8.get_world))) - (let [result (world:apply_textures [ - {:name "floor" - :texture_id floor-tex - :rule (fn [normal] (> normal.y 0.7))} - {:name "ceiling" - :texture_id sky-tex - :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)))}])] - (when (< result 0) - (pxl8.error (.. "Failed to apply textures - result: " result)))))))) + (when (not floor-tex) + (set floor-tex (textures.mossy-cobblestone 44444 STONE_FLOOR_START MOSS_COLOR))) + (when (not wall-tex) + (set wall-tex (textures.ashlar-wall 55555 STONE_WALL_START MOSS_COLOR))) + (when (not ceiling-tex) + (set ceiling-tex (pxl8.create_texture [0] 1 1))) + + ) + +(fn setup-materials [] + (when (and world (not materials-setup)) + (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 [] (var move-forward 0) @@ -328,9 +343,10 @@ (let [input (get-pending-input t) hist (get-position t)] (when (and input hist) - (let [(new-x new-z) (apply-movement cam-x cam-z hist.yaw input)] - (set cam-x new-x) - (set cam-z new-z) + (let [(new-x new-z) (apply-movement cam-x cam-z hist.yaw input) + (resolved-x _ resolved-z) (world:resolve_collision cam-x cam-y cam-z new-x cam-y new-z 5)] + (set cam-x resolved-x) + (set cam-z resolved-z) (store-position t cam-x cam-z hist.yaw)))))))))) (fn update [dt] @@ -343,9 +359,12 @@ (set fps-sample-count 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) - grid-max (* grid-size cell-size) + grid-max (* grid-size chunk-size) movement-yaw cam-yaw] (set time-accumulator (+ time-accumulator dt)) @@ -388,18 +407,16 @@ :look_dy input.look_dy :yaw movement-yaw :tick client-tick}) - (network:update dt) - (when (network:poll) - (let [snapshot (network:snapshot)] - (when (and snapshot (> snapshot.tick last-processed-tick)) - (set last-processed-tick snapshot.tick) - (let [player-id (network:player_id)] - (when (> player-id 0) - (let [curr (network:entity_userdata player-id)] - (when curr - (let [srv-x (pxl8.unpack_f32_be curr 0) - srv-z (pxl8.unpack_f32_be curr 8)] - (reconcile snapshot.tick srv-x srv-z)))))))))))] + (let [snapshot (network:snapshot)] + (when (and snapshot (> snapshot.tick last-processed-tick)) + (set last-processed-tick snapshot.tick) + (let [player-id (network:player_id)] + (when (> player-id 0) + (let [curr (network:entity_userdata player-id)] + (when curr + (let [srv-x (pxl8.unpack_f32_be curr 0) + srv-z (pxl8.unpack_f32_be curr 8)] + (reconcile snapshot.tick srv-x srv-z))))))))))] (when (not ok) (pxl8.error (.. "Network error: " err))))) @@ -430,7 +447,7 @@ (set bob-time (+ (* bob-time 0.8) (* target-phase 0.2)))))) (set light-time (+ light-time (* dt 0.5))) - (set real-time (+ real-time dt))))) + (set real-time (+ real-time dt)))))) (fn frame [] (pxl8.clear 1) @@ -441,10 +458,11 @@ (when (not world) (pxl8.error "world is nil!")) - (when (and world (not (world:is_loaded))) - (pxl8.text "World not loaded yet..." 5 30 12)) + (let [chunk (when world (world:active_chunk))] + (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) eye-y (+ cam-y bob-offset land-squash) forward-x (- (math.sin cam-yaw)) @@ -459,8 +477,8 @@ [0 1 0]) (camera:set_perspective 1.047 aspect 1.0 4096.0) - (let [light-x (+ 1000 (* 50 (math.cos light-time))) - light-z (+ 940 (* 50 (math.sin light-time))) + (let [light-x (+ 384 (* 50 (math.cos light-time))) + light-z (+ 324 (* 50 (math.sin light-time))) light-y 80 phase (+ (* light-x 0.01) 1.7) 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)) (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]) (when fireball-mesh @@ -509,7 +530,7 @@ (pxl8.text (.. "fps: " (string.format "%.0f" fps-avg)) 5 5 text-color) (pxl8.text (.. "pos: " (string.format "%.0f" cam-x) "," (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 :update update diff --git a/demo/mod/sky.fnl b/demo/mod/sky.fnl index 426512b..6661455 100644 --- a/demo/mod/sky.fnl +++ b/demo/mod/sky.fnl @@ -8,8 +8,8 @@ (local NUM_RANDOM_STARS 300) (local NUM_TINY_STARS 7000) +(local STAR_CYCLE_PERIOD 86400) (local STAR_SEED 0xDEADBEEF) -(local STAR_CYCLE_PERIOD 7200) ;; Use existing bright palette colors ;; 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) (set last-gradient-key key)))) -(fn galactic-band-factor [dx dy dz] - (let [band-len (math.sqrt (+ (* 0.6 0.6) (* 0.3 0.3) (* 0.742 0.742))) - bx (/ 0.6 band-len) - 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))] +(fn band-factor [dx dy dz bx by bz width] + (let [dist (math.abs (+ (* dx bx) (* dy by) (* dz bz))) + in-band (- 1 (math.min (* dist width) 1))] (* 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 [] (set random-stars []) (set tiny-stars []) @@ -205,8 +211,9 @@ (let [star (. tiny-stars (+ i 1)) int (math.floor (* star.brightness fade-in))] (when (> int 8) - (star-glows:add (math.floor screen.x) (math.floor screen.y) - 1 int star.color pxl8.GLOW_CIRCLE)))))) + (let [px (math.floor (+ screen.x 0.5)) + 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)] (let [screen (. star-projected (+ tiny-count i))] @@ -215,8 +222,8 @@ phase (+ (* (+ i 1) 2.137) (* time-factor 3.0)) twinkle (+ 0.75 (* 0.25 (math.sin (* phase 6.28)))) int (math.floor (* star.brightness fade-in twinkle)) - sx (math.floor screen.x) - sy (math.floor screen.y)] + sx (math.floor (+ screen.x 0.5)) + sy (math.floor (+ screen.y 0.5))] (if (> star.brightness 220) (do (star-glows:add sx sy 3 (math.floor (* int 1.5)) star.color pxl8.GLOW_DIAMOND) diff --git a/pxl8.sh b/pxl8.sh index 4dec268..bbe5e4f 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -58,20 +58,20 @@ build_luajit() { build_server() { local mode="$1" - if [[ -d "server" ]]; then - print_info "Building server ($mode mode)" - cd server + if [[ -d "pxl8d" ]]; then + print_info "Building pxl8d ($mode mode)" + cd pxl8d if [[ "$mode" == "release" ]]; then - cargo build --release + cargo build --release --quiet else - cargo build + cargo build --quiet fi local status=$? cd - > /dev/null if [[ $status -eq 0 ]]; then - print_info "Built server" + print_info "Built pxl8d" else - print_error "Server build failed" + print_error "pxl8d build failed" fi fi } @@ -80,9 +80,9 @@ start_server() { local mode="$1" local server_bin if [[ "$mode" == "release" ]]; then - server_bin="server/target/release/pxl8-server" + server_bin="pxl8d/target/release/pxl8d" else - server_bin="server/target/debug/pxl8-server" + server_bin="pxl8d/target/debug/pxl8d" fi print_info "Server mode: $mode, binary: $server_bin" if [[ -f "$server_bin" ]]; then @@ -92,8 +92,8 @@ start_server() { print_info "Server started with PID $SERVER_PID" sleep 0.5 else - print_error "Server binary not found: $server_bin" - print_error "Build the server first with: cd server && cargo build" + print_error "pxl8d binary not found: $server_bin" + print_error "Build pxl8d first with: cd pxl8d && cargo build" fi } @@ -105,6 +105,15 @@ stop_server() { 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() { if [[ ! -f "lib/SDL/build/libSDL3.so" ]] && [[ ! -f "lib/SDL/build/libSDL3.a" ]] && [[ ! -f "lib/SDL/build/libSDL3.dylib" ]]; then print_info "Building SDL3" @@ -160,9 +169,9 @@ print_usage() { echo echo -e "${BOLD}OPTIONS:${NC}" echo " --all Clean both build artifacts and dependencies" + echo " --cache Clear ccache (use with clean)" echo " --deps Clean only dependencies" echo " --release Build/run/clean in release mode (default: debug)" - echo " --server Start game server before running (for networked games)" } setup_sdl3() { @@ -364,6 +373,8 @@ case "$COMMAND" in build_sdl fi + build_server "$MODE" + setup_sdl3 print_info "Building pxl8 ($MODE mode)" @@ -380,16 +391,19 @@ case "$COMMAND" in LIB_SOURCE_FILES="lib/linenoise/linenoise.c lib/miniz/miniz.c" 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_bytes.c src/core/pxl8_io.c src/core/pxl8_log.c + src/core/pxl8_replay.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_atlas.c src/gfx/pxl8_blit.c - src/gfx/pxl8_3d_camera.c src/gfx/pxl8_colormap.c src/gfx/pxl8_cpu.c src/gfx/pxl8_dither.c @@ -404,22 +418,23 @@ case "$COMMAND" in src/gfx/pxl8_tilemap.c src/gfx/pxl8_tilesheet.c src/gfx/pxl8_transition.c - src/sfx/pxl8_sfx.c - src/script/pxl8_repl.c - src/script/pxl8_script.c + src/gui/pxl8_gui.c src/hal/pxl8_hal_sdl3.c src/hal/pxl8_mem_sdl3.c - src/world/pxl8_bsp.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/math/pxl8_math.c src/net/pxl8_net.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" @@ -450,7 +465,8 @@ case "$COMMAND" in NEEDS_REBUILD=false if [[ "$src_file" -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 fi @@ -489,24 +505,18 @@ case "$COMMAND" in CART="" EXTRA_ARGS="" - RUN_SERVER=false for arg in "$@"; do if [[ "$arg" == "--release" ]]; then MODE="release" elif [[ "$arg" == "--repl" ]]; then EXTRA_ARGS="$EXTRA_ARGS --repl" - elif [[ "$arg" == "--server" ]]; then - RUN_SERVER=true elif [[ -z "$CART" ]]; then CART="$arg" fi done - if [[ "$RUN_SERVER" == true ]]; then - build_server "$MODE" - start_server "$MODE" - trap stop_server EXIT - fi + start_server "$MODE" + trap stop_server EXIT if [[ -z "$CART" ]]; then "$BINDIR/pxl8" $EXTRA_ARGS @@ -517,17 +527,24 @@ case "$COMMAND" in clean) CLEAN_ALL=false + CLEAN_CACHE=false CLEAN_DEPS=false CLEAN_RELEASE=false for arg in "$@"; do case "$arg" in --all) CLEAN_ALL=true ;; + --cache) CLEAN_CACHE=true ;; --deps) CLEAN_DEPS=true ;; --release) CLEAN_RELEASE=true ;; esac 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 BUILD_PATH=".build/release" BIN_PATH="bin/release" @@ -541,6 +558,7 @@ case "$COMMAND" in if [[ "$CLEAN_ALL" == true ]]; then print_info "Removing build artifacts and dependencies" rm -rf "$BUILD_PATH" "$BIN_PATH" lib + clean_server print_info "Cleaned all" elif [[ "$CLEAN_DEPS" == true ]]; then print_info "Removing dependencies" @@ -549,6 +567,7 @@ case "$COMMAND" in else print_info "Removing build artifacts" rm -rf "$BUILD_PATH" "$BIN_PATH" + clean_server print_info "Cleaned" fi ;; diff --git a/server/.cargo/config.toml b/pxl8d/.cargo/config.toml similarity index 100% rename from server/.cargo/config.toml rename to pxl8d/.cargo/config.toml diff --git a/server/.gitignore b/pxl8d/.gitignore similarity index 100% rename from server/.gitignore rename to pxl8d/.gitignore diff --git a/pxl8d/Cargo.lock b/pxl8d/Cargo.lock new file mode 100644 index 0000000..cf54719 --- /dev/null +++ b/pxl8d/Cargo.lock @@ -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", +] diff --git a/server/Cargo.toml b/pxl8d/Cargo.toml similarity index 77% rename from server/Cargo.toml rename to pxl8d/Cargo.toml index 2ef61a7..23bcb8c 100644 --- a/server/Cargo.toml +++ b/pxl8d/Cargo.toml @@ -1,13 +1,16 @@ [package] -name = "pxl8-server" +name = "pxl8d" version = "0.1.0" edition = "2024" +[lib] +name = "pxl8d" + [build-dependencies] bindgen = "0.72" +cc = "1.2" [dependencies] -bevy_ecs = { version = "0.18", default-features = false } libc = { 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]] -name = "pxl8-server" +name = "pxl8d" path = "src/main.rs" [profile.dev] +opt-level = 2 panic = "abort" +[profile.dev.package."*"] +opt-level = 3 + [profile.release] panic = "abort" - -[features] -default = [] -std = ["bevy_ecs/std"] +lto = true +codegen-units = 1 +strip = true diff --git a/server/build.rs b/pxl8d/build.rs similarity index 68% rename from server/build.rs rename to pxl8d/build.rs index 5c30ae3..4b2a940 100644 --- a/server/build.rs +++ b/pxl8d/build.rs @@ -5,10 +5,19 @@ fn main() { let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); 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/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() + .header(pxl8_src.join("core/pxl8_log.h").to_str().unwrap()) .header(pxl8_src.join("net/pxl8_protocol.h").to_str().unwrap()) .clang_arg(format!("-I{}", pxl8_src.join("core").display())) .use_core() diff --git a/server/rust-toolchain.toml b/pxl8d/rust-toolchain.toml similarity index 100% rename from server/rust-toolchain.toml rename to pxl8d/rust-toolchain.toml diff --git a/server/src/allocator.rs b/pxl8d/src/allocator.rs similarity index 100% rename from server/src/allocator.rs rename to pxl8d/src/allocator.rs diff --git a/pxl8d/src/bsp.rs b/pxl8d/src/bsp.rs new file mode 100644 index 0000000..841d63f --- /dev/null +++ b/pxl8d/src/bsp.rs @@ -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, + pub edges: Vec, + pub surfedges: Vec, + pub planes: Vec, + pub faces: Vec, + pub nodes: Vec, + pub leafs: Vec, + pub marksurfaces: Vec, + pub cell_portals: Vec, + pub visdata: Vec, + pub vertex_lights: Vec, +} + +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() + } +} diff --git a/pxl8d/src/chunk.rs b/pxl8d/src/chunk.rs new file mode 100644 index 0000000..c58e48c --- /dev/null +++ b/pxl8d/src/chunk.rs @@ -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, + } + } +} diff --git a/pxl8d/src/chunk/stream.rs b/pxl8d/src/chunk/stream.rs new file mode 100644 index 0000000..0afe9cf --- /dev/null +++ b/pxl8d/src/chunk/stream.rs @@ -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, + pending: Vec, + pending_messages: Vec, +} + +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 { + self.pending.pop() + } + + pub fn queue_message(&mut self, msg: ChunkMessage) { + self.pending_messages.push(msg); + } + + pub fn queue_messages(&mut self, msgs: Vec) { + self.pending_messages.extend(msgs); + } + + pub fn next_message(&mut self) -> Option { + 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() + } +} diff --git a/pxl8d/src/lib.rs b/pxl8d/src/lib.rs new file mode 100644 index 0000000..18d106f --- /dev/null +++ b/pxl8d/src/lib.rs @@ -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::*; diff --git a/pxl8d/src/log.rs b/pxl8d/src/log.rs new file mode 100644 index 0000000..e8390a8 --- /dev/null +++ b/pxl8d/src/log.rs @@ -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(()) + } +} diff --git a/pxl8d/src/main.rs b/pxl8d/src/main.rs new file mode 100644 index 0000000..e51e269 --- /dev/null +++ b/pxl8d/src/main.rs @@ -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 = 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 = 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 { + 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) +} diff --git a/pxl8d/src/math.rs b/pxl8d/src/math.rs new file mode 100644 index 0000000..3b867f1 --- /dev/null +++ b/pxl8d/src/math.rs @@ -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 for Vec3 { + type Output = Self; + fn mul(self, rhs: f32) -> Self { + Self::new(self.x * rhs, self.y * rhs, self.z * rhs) + } +} diff --git a/pxl8d/src/procgen.rs b/pxl8d/src/procgen.rs new file mode 100644 index 0000000..99b3c3a --- /dev/null +++ b/pxl8d/src/procgen.rs @@ -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, + 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 { + 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 { + 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 { + 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 = 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 = 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) +} diff --git a/pxl8d/src/sim.rs b/pxl8d/src/sim.rs new file mode 100644 index 0000000..8ddef47 --- /dev/null +++ b/pxl8d/src/sim.rs @@ -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, + pub free_list: Vec, + pub player: Option, + 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 { + 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(&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() + } +} diff --git a/server/src/transport.rs b/pxl8d/src/transport.rs similarity index 59% rename from server/src/transport.rs rename to pxl8d/src/transport.rs index 2315433..3aa1381 100644 --- a/server/src/transport.rs +++ b/pxl8d/src/transport.rs @@ -1,6 +1,120 @@ +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; use crate::protocol::*; +use crate::protocol::pxl8_msg_type::*; +use crate::voxel::VoxelChunk; 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, +} + +impl ChunkMessage { + pub fn from_voxel(chunk: &VoxelChunk, version: u32) -> Vec { + 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, chunk_id: u32, version: u32) -> Vec { + 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)] mod sys { @@ -129,8 +243,8 @@ type SockAddr = windows_sys::Win32::Networking::WinSock::SOCKADDR_IN; pub struct Transport { client_addr: SockAddr, has_client: bool, - recv_buf: [u8; 4096], - send_buf: [u8; 4096], + recv_buf: [u8; 512], + send_buf: [u8; 2048], socket: sys::RawSocket, } @@ -154,8 +268,8 @@ impl Transport { Some(Self { client_addr: unsafe { core::mem::zeroed() }, has_client: false, - recv_buf: [0u8; 4096], - send_buf: [0u8; 4096], + recv_buf: [0u8; 512], + send_buf: [0u8; 2048], socket: sock, }) } @@ -198,7 +312,7 @@ impl Transport { let msg_header = pxl8_msg_header { sequence, size: 0, - type_: pxl8_msg_type::PXL8_MSG_SNAPSHOT as u8, + type_: PXL8_MSG_SNAPSHOT as u8, version: PXL8_PROTOCOL_VERSION as u8, }; offset += self.serialize_header(&msg_header, offset); @@ -211,6 +325,72 @@ impl Transport { 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 { let buf = &mut self.send_buf[offset..]; buf[0..4].copy_from_slice(&h.sequence.to_be_bytes()); diff --git a/pxl8d/src/voxel.rs b/pxl8d/src/voxel.rs new file mode 100644 index 0000000..879363e --- /dev/null +++ b/pxl8d/src/voxel.rs @@ -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 { + 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, + 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) + } +} diff --git a/pxl8d/src/world.rs b/pxl8d/src/world.rs new file mode 100644 index 0000000..cf4cc99 --- /dev/null +++ b/pxl8d/src/world.rs @@ -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, + chunks: BTreeMap, + 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 { + 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 { + 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 { + self.chunks.iter() + } +} + +impl Default for World { + fn default() -> Self { + Self::new(12345) + } +} diff --git a/server/Cargo.lock b/server/Cargo.lock deleted file mode 100644 index fbbf93d..0000000 --- a/server/Cargo.lock +++ /dev/null @@ -1,1090 +0,0 @@ -# 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 = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "assert_type_match" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "bevy_ecs" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24637a7c8643cab493f4085cda6bde4895f0e0816699c59006f18819da2ca0b8" -dependencies = [ - "arrayvec", - "bevy_ecs_macros", - "bevy_platform", - "bevy_ptr", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "bitflags", - "bumpalo", - "concurrent-queue", - "derive_more", - "fixedbitset", - "indexmap", - "log", - "nonmax", - "serde", - "slotmap", - "smallvec", - "thiserror", - "variadics_please", -] - -[[package]] -name = "bevy_ecs_macros" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eb14c18ca71e11c69fbae873c2db129064efac6d52e48d0127d37bfba1acfa8" -dependencies = [ - "bevy_macro_utils", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bevy_macro_utils" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7272fca0bf30d8ca2571a803598856104b63e5c596d52850f811ed37c5e1e3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "toml_edit", -] - -[[package]] -name = "bevy_platform" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b29ea749a8e85f98186ab662f607b885b97c804bb14cdb0cdf838164496d474" -dependencies = [ - "critical-section", - "foldhash", - "futures-channel", - "hashbrown", - "js-sys", - "portable-atomic", - "portable-atomic-util", - "serde", - "spin", - "wasm-bindgen", - "wasm-bindgen-futures", -] - -[[package]] -name = "bevy_ptr" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f98cbc6d34bbdb58240b72ed1731931b4991a893b3a3238bb7c42ae054aa676" - -[[package]] -name = "bevy_reflect" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2a977e2b8dba65b6e9c11039c5f9ef108be428f036b3d1cac13ad86ec59f9c" -dependencies = [ - "assert_type_match", - "bevy_platform", - "bevy_ptr", - "bevy_reflect_derive", - "bevy_utils", - "derive_more", - "disqualified", - "downcast-rs", - "erased-serde", - "foldhash", - "glam", - "indexmap", - "serde", - "smallvec", - "smol_str", - "thiserror", - "uuid", - "variadics_please", - "wgpu-types", -] - -[[package]] -name = "bevy_reflect_derive" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "067af30072b1611fda1a577f1cb678b8ea2c9226133068be808dd49aac30cef0" -dependencies = [ - "bevy_macro_utils", - "indexmap", - "proc-macro2", - "quote", - "syn", - "uuid", -] - -[[package]] -name = "bevy_tasks" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990ffedd374dd2c4fe8f0fd4bcefd5617d1ee59164b6c3fcc356a69b48e26e8e" -dependencies = [ - "async-channel", - "async-task", - "atomic-waker", - "bevy_platform", - "crossbeam-queue", - "derive_more", - "futures-lite", - "heapless", - "pin-project", -] - -[[package]] -name = "bevy_utils" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e258c44d869f9c41ac0f88a16815c67f2569eb9fff4716828a40273d127b6f84" -dependencies = [ - "bevy_platform", - "disqualified", - "thread_local", -] - -[[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" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[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 = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", - "portable-atomic", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", -] - -[[package]] -name = "disqualified" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" - -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glam" -version = "0.30.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" -dependencies = [ - "serde_core", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "equivalent", - "serde", - "serde_core", -] - -[[package]] -name = "heapless" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" -dependencies = [ - "hash32", - "portable-atomic", - "stable_deref_trait", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[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 = "nonmax" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "portable-atomic" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[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 = "pxl8-server" -version = "0.1.0" -dependencies = [ - "bevy_ecs", - "bindgen", - "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 = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[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 = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[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 = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "variadics_please" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wgpu-types" -version = "27.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" -dependencies = [ - "bitflags", - "bytemuck", - "js-sys", - "log", - "serde", - "thiserror", - "web-sys", -] - -[[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", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" diff --git a/server/src/components.rs b/server/src/components.rs deleted file mode 100644 index 02f3516..0000000 --- a/server/src/components.rs +++ /dev/null @@ -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); diff --git a/server/src/lib.rs b/server/src/lib.rs deleted file mode 100644 index a628640..0000000 --- a/server/src/lib.rs +++ /dev/null @@ -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::*; diff --git a/server/src/main.rs b/server/src/main.rs deleted file mode 100644 index 165fbbc..0000000 --- a/server/src/main.rs +++ /dev/null @@ -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 = 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); - } -} diff --git a/server/src/simulation.rs b/server/src/simulation.rs deleted file mode 100644 index 5e9f231..0000000 --- a/server/src/simulation.rs +++ /dev/null @@ -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, - 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::() { - 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::(player) { - pos.x += forward_x + strafe_x; - pos.z += forward_z + strafe_z; - } - } - - if let Some(mut rot) = self.world.get_mut::(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(&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() - } -} diff --git a/src/asset/pxl8_embed.h b/src/asset/pxl8_embed.h index a3efa98..7139fa4 100644 --- a/src/asset/pxl8_embed.h +++ b/src/asset/pxl8_embed.h @@ -55,6 +55,10 @@ static const char embed_pxl8_particles[] = { #embed "src/lua/pxl8/particles.lua" , 0 }; +static const char embed_pxl8_procgen[] = { +#embed "src/lua/pxl8/procgen.lua" +, 0 }; + static const char embed_pxl8_sfx[] = { #embed "src/lua/pxl8/sfx.lua" , 0 }; @@ -89,6 +93,7 @@ static const pxl8_embed pxl8_embeds[] = { PXL8_EMBED_ENTRY(embed_pxl8_math, "pxl8.math"), PXL8_EMBED_ENTRY(embed_pxl8_net, "pxl8.net"), 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_tilemap, "pxl8.tilemap"), PXL8_EMBED_ENTRY(embed_pxl8_transition, "pxl8.transition"), diff --git a/src/core/pxl8.c b/src/core/pxl8.c index f6943f8..d89cff1 100644 --- a/src/core/pxl8.c +++ b/src/core/pxl8.c @@ -237,6 +237,19 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { 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 game->debug_replay = pxl8_replay_create_buffer(60, 60); 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_sfx_mixer_process(game->mixer); @@ -418,6 +438,9 @@ void pxl8_quit(pxl8* sys) { pxl8_replay_destroy(game->debug_replay); #endif + if (game->net) pxl8_net_destroy(game->net); + if (game->world) pxl8_world_destroy(game->world); + pxl8_sfx_mixer_destroy(game->mixer); pxl8_gfx_destroy(game->gfx); 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) { 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; } +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) { return (sys && sys->game) ? sys->game->mixer : NULL; } diff --git a/src/core/pxl8_game.h b/src/core/pxl8_game.h index 1dba5f4..e93eb18 100644 --- a/src/core/pxl8_game.h +++ b/src/core/pxl8_game.h @@ -1,37 +1,37 @@ #pragma once #include "pxl8_gfx.h" +#include "pxl8_net.h" #include "pxl8_rng.h" #include "pxl8_script.h" #include "pxl8_sfx.h" #include "pxl8_types.h" +#include "pxl8_world.h" typedef struct pxl8_replay pxl8_replay; 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 pxl8_replay* debug_replay; #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_started; + pxl8_rng rng; bool running; + pxl8_script* script; bool script_loaded; char script_path[256]; + f32 time; + pxl8_world* world; } pxl8_game; diff --git a/src/core/pxl8_log.c b/src/core/pxl8_log.c index 688760b..e378005 100644 --- a/src/core/pxl8_log.c +++ b/src/core/pxl8_log.c @@ -1,5 +1,5 @@ #include "pxl8_log.h" -#include "pxl8_repl.h" +#include "pxl8_types.h" #include #include @@ -18,6 +18,7 @@ static pxl8_log* g_log = NULL; void pxl8_log_init(pxl8_log* log) { g_log = log; + g_log->handler = NULL; g_log->level = PXL8_LOG_LEVEL_DEBUG; 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) { 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); - if (!pxl8_repl_push_log(buffer)) { + if (!g_log->handler || !g_log->handler(buffer)) { printf("%s", buffer); fflush(stdout); } diff --git a/src/core/pxl8_log.h b/src/core/pxl8_log.h index 78aa57b..d7a07da 100644 --- a/src/core/pxl8_log.h +++ b/src/core/pxl8_log.h @@ -10,11 +10,15 @@ typedef enum { PXL8_LOG_LEVEL_ERROR = 4, } pxl8_log_level; +typedef bool (*pxl8_log_handler)(const char* message); + typedef struct pxl8_log { + pxl8_log_handler handler; pxl8_log_level level; } pxl8_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_write_trace(const char* file, int line, const char* fmt, ...); diff --git a/src/core/pxl8_sys.h b/src/core/pxl8_sys.h index f5e59eb..ac791a5 100644 --- a/src/core/pxl8_sys.h +++ b/src/core/pxl8_sys.h @@ -3,6 +3,7 @@ #include "pxl8_gfx.h" #include "pxl8_hal.h" #include "pxl8_io.h" +#include "pxl8_net.h" #include "pxl8_sfx.h" #include "pxl8_types.h" @@ -23,6 +24,7 @@ void pxl8_quit(pxl8* sys); f32 pxl8_get_fps(const pxl8* sys); pxl8_gfx* pxl8_get_gfx(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_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys); bool pxl8_is_running(const pxl8* sys); diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua index 3f02c39..a4617d4 100644 --- a/src/lua/pxl8.lua +++ b/src/lua/pxl8.lua @@ -87,11 +87,13 @@ pxl8.create_anim_from_ase = anim.Anim.from_ase pxl8.bounds = math.bounds pxl8.Camera3D = gfx3d.Camera3D +pxl8.Material = gfx3d.Material pxl8.Mesh = gfx3d.Mesh pxl8.begin_frame_3d = gfx3d.begin_frame pxl8.clear_3d = gfx3d.clear pxl8.clear_depth = gfx3d.clear_depth pxl8.create_camera_3d = gfx3d.Camera3D.new +pxl8.create_material = gfx3d.create_material pxl8.create_mesh = gfx3d.Mesh.new pxl8.create_vec3_array = gfx3d.create_vec3_array 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_translate = math.mat4_translate -pxl8.Net = net.Net -pxl8.create_net = net.Net.new -pxl8.NET_MODE_LOCAL = net.MODE_LOCAL -pxl8.NET_MODE_REMOTE = net.MODE_REMOTE +pxl8.get_net = net.get pxl8.pack_f32_be = bytes.pack_f32_be 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_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.create_world = world.World.new +pxl8.get_world = world.World.get return pxl8 diff --git a/src/lua/pxl8/gfx3d.lua b/src/lua/pxl8/gfx3d.lua index 61ec31c..fc30fc2 100644 --- a/src/lua/pxl8/gfx3d.lua +++ b/src/lua/pxl8/gfx3d.lua @@ -205,4 +205,27 @@ function gfx3d.create_vec3_array(count) return ffi.new("pxl8_vec3[?]", count) 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 diff --git a/src/lua/pxl8/net.lua b/src/lua/pxl8/net.lua index 318889f..33f30cb 100644 --- a/src/lua/pxl8/net.lua +++ b/src/lua/pxl8/net.lua @@ -1,47 +1,24 @@ local ffi = require("ffi") local C = ffi.C +local core = require("pxl8.core") local net = {} local Net = {} Net.__index = Net -net.MODE_LOCAL = C.PXL8_NET_LOCAL -net.MODE_REMOTE = C.PXL8_NET_REMOTE - -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 +function net.get() + local ptr = C.pxl8_get_net(core.sys) + if ptr == nil then return nil end - return setmetatable({ _ptr = n }, Net) -end - -function Net:connect() - return C.pxl8_net_connect(self._ptr) == 0 + return setmetatable({ _ptr = ptr }, Net) end function Net:connected() return C.pxl8_net_connected(self._ptr) 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() local snap = C.pxl8_net_snapshot(self._ptr) if snap == nil then @@ -113,10 +90,6 @@ function Net:player_id() return tonumber(C.pxl8_net_player_id(self._ptr)) end -function Net:poll() - return C.pxl8_net_poll(self._ptr) -end - function Net:predicted_state() return C.pxl8_net_predicted_state(self._ptr) end @@ -173,10 +146,4 @@ function Net:tick() return tonumber(C.pxl8_net_tick(self._ptr)) end -function Net:update(dt) - C.pxl8_net_update(self._ptr, dt) -end - -net.Net = Net - return net diff --git a/src/lua/pxl8/world.lua b/src/lua/pxl8/world.lua index d1c016f..eebcf38 100644 --- a/src/lua/pxl8/world.lua +++ b/src/lua/pxl8/world.lua @@ -4,40 +4,77 @@ local core = require("pxl8.core") local world = {} +world.CHUNK_VXL = 0 +world.CHUNK_BSP = 1 world.PROCGEN_ROOMS = C.PXL8_PROCGEN_ROOMS 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 = {} World.__index = World -function World.new() - local w = C.pxl8_world_create() - if w == nil then - return nil - end +function World.get() + local w = C.pxl8_get_world(core.sys) + if w == nil then return nil end return setmetatable({ _ptr = w }, World) end -function World:apply_textures(texture_defs) - local count = #texture_defs - local textures = ffi.new("pxl8_world_texture[?]", count) - - 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) +function World:active_chunk() + local ptr = C.pxl8_world_active_chunk(self._ptr) + if ptr == nil then return nil end + return setmetatable({ _ptr = ptr }, Chunk) end 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) 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) 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) @@ -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 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 return world diff --git a/src/net/pxl8_net.c b/src/net/pxl8_net.c index 8ea7440..18eb45f 100644 --- a/src/net/pxl8_net.c +++ b/src/net/pxl8_net.c @@ -1,4 +1,6 @@ #include "pxl8_net.h" +#include "pxl8_chunk_cache.h" +#include "pxl8_log.h" #include "pxl8_mem.h" #include @@ -28,6 +30,9 @@ struct pxl8_net { char address[256]; + u32 chunk_id; + u8 chunk_type; + pxl8_chunk_cache* chunk_cache; bool connected; pxl8_entity_state entities[PXL8_MAX_SNAPSHOT_ENTITIES]; pxl8_event_msg events[PXL8_MAX_SNAPSHOT_EVENTS]; @@ -36,7 +41,6 @@ struct pxl8_net { u64 input_head; u64 input_oldest_tick; f32 interp_time; - pxl8_net_mode mode; u16 port; u8 predicted_state[PXL8_NET_USERDATA_SIZE]; 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)); if (!net) return NULL; - net->mode = config->mode; net->port = config->port ? config->port : PXL8_NET_DEFAULT_PORT; net->sock = INVALID_SOCK; net->connected = false; @@ -209,6 +212,32 @@ bool pxl8_net_poll(pxl8_net* net) { pxl8_msg_header 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; pxl8_snapshot_header snap; @@ -311,3 +340,23 @@ void pxl8_net_update(pxl8_net* net, f32 dt) { if (!net) return; 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; +} diff --git a/src/net/pxl8_net.h b/src/net/pxl8_net.h index cf81270..5516355 100644 --- a/src/net/pxl8_net.h +++ b/src/net/pxl8_net.h @@ -11,15 +11,10 @@ extern "C" { #define PXL8_NET_USERDATA_SIZE 56 typedef struct pxl8_net pxl8_net; - -typedef enum pxl8_net_mode { - PXL8_NET_LOCAL = 0, - PXL8_NET_REMOTE -} pxl8_net_mode; +typedef struct pxl8_chunk_cache pxl8_chunk_cache; typedef struct pxl8_net_config { const char* address; - pxl8_net_mode mode; u16 port; } 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); u64 pxl8_net_tick(const pxl8_net* net); 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 } diff --git a/src/net/pxl8_protocol.c b/src/net/pxl8_protocol.c index cb74099..3a3e4ab 100644 --- a/src/net/pxl8_protocol.c +++ b/src/net/pxl8_protocol.c @@ -122,3 +122,76 @@ usize pxl8_protocol_deserialize_snapshot_header(const u8* buf, usize len, pxl8_s hdr->time = pxl8_read_f32_be(&s); 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; +} diff --git a/src/net/pxl8_protocol.h b/src/net/pxl8_protocol.h index 696a7ad..3660987 100644 --- a/src/net/pxl8_protocol.h +++ b/src/net/pxl8_protocol.h @@ -14,12 +14,14 @@ extern "C" { typedef enum pxl8_msg_type { PXL8_MSG_NONE = 0, + PXL8_MSG_CHUNK, + PXL8_MSG_CHUNK_ENTER, + PXL8_MSG_COMMAND, PXL8_MSG_CONNECT, PXL8_MSG_DISCONNECT, + PXL8_MSG_EVENT, PXL8_MSG_INPUT, - PXL8_MSG_COMMAND, - PXL8_MSG_SNAPSHOT, - PXL8_MSG_EVENT + PXL8_MSG_SNAPSHOT } pxl8_msg_type; typedef struct pxl8_msg_header { @@ -70,6 +72,39 @@ typedef struct pxl8_snapshot_header { f32 time; } 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_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_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 } #endif diff --git a/src/script/pxl8_repl.c b/src/script/pxl8_repl.c index 5c7853f..048a1ac 100644 --- a/src/script/pxl8_repl.c +++ b/src/script/pxl8_repl.c @@ -1,15 +1,16 @@ #include "pxl8_repl.h" -#include "pxl8_mem.h" +#include #include +#include #include #include #include #include #include -#include -#include +#include "pxl8_log.h" +#include "pxl8_mem.h" #define PXL8_MAX_REPL_COMMAND_SIZE 4096 #define PXL8_REPL_QUEUE_SIZE 8 @@ -235,6 +236,7 @@ pxl8_repl* pxl8_repl_create(void) { linenoiseHistoryLoad(".pxl8_history"); g_repl = repl; + pxl8_log_set_handler(pxl8_repl_push_log); repl->thread = SDL_CreateThread(pxl8_repl_thread, "pxl8-repl", repl); if (!repl->thread) { @@ -260,6 +262,7 @@ void pxl8_repl_destroy(pxl8_repl* repl) { pxl8_repl_flush_logs(repl); g_repl = NULL; + pxl8_log_set_handler(NULL); system("stty sane 2>/dev/null"); pxl8_free(repl); diff --git a/src/script/pxl8_script_ffi.h b/src/script/pxl8_script_ffi.h index 4ca83fe..40a9618 100644 --- a/src/script/pxl8_script_ffi.h +++ b/src/script/pxl8_script_ffi.h @@ -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" "\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" -"typedef struct pxl8_world_texture {\n" -" char name[16];\n" -" unsigned int texture_id;\n" -" pxl8_texture_rule rule;\n" -"} pxl8_world_texture;\n" +"u32 pxl8_bsp_face_count(const pxl8_bsp* bsp);\n" +"pxl8_vec3 pxl8_bsp_face_normal(const pxl8_bsp* bsp, u32 face_id);\n" +"void pxl8_bsp_face_set_material(pxl8_bsp* bsp, u32 face_id, u16 material_id);\n" +"void pxl8_bsp_set_material(pxl8_bsp* bsp, u16 material_id, const pxl8_gfx_material* material);\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" "typedef struct pxl8_world pxl8_world;\n" -"pxl8_world* pxl8_world_create(void);\n" -"void pxl8_world_destroy(pxl8_world* world);\n" -"int pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params);\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" +"\n" +"pxl8_world* pxl8_get_world(pxl8* sys);\n" +"pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world);\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" "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" "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" @@ -517,8 +523,7 @@ static const char* pxl8_ffi_cdefs = "void pxl8_sfx_stop_voice(pxl8_sfx_context* ctx, u16 voice_id);\n" "\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; pxl8_net_mode mode; u16 port; } pxl8_net_config;\n" +"typedef struct pxl8_net_config { const char* address; u16 port; } pxl8_net_config;\n" "typedef enum pxl8_cmd_type { PXL8_CMD_NONE = 0, PXL8_CMD_SPAWN_ENTITY } pxl8_cmd_type;\n" "\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" "u64 pxl8_net_tick(const pxl8_net* net);\n" "void pxl8_net_update(pxl8_net* net, f32 dt);\n" +"pxl8_net* pxl8_get_net(const pxl8* sys);\n" "\n" "void pxl8_bit_clear(u32* val, u8 bit);\n" "u32 pxl8_bit_count(u32 val);\n" diff --git a/src/world/pxl8_bsp.c b/src/world/pxl8_bsp.c index 3275b31..0d73d7e 100644 --- a/src/world/pxl8_bsp.c +++ b/src/world/pxl8_bsp.c @@ -394,6 +394,48 @@ i32 pxl8_bsp_find_leaf(const pxl8_bsp* bsp, pxl8_vec3 pos) { 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) { if (!bsp || !bsp->visdata || bsp->visdata_size == 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); } -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) { 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) { - if (!gfx || !bsp || bsp->num_faces == 0) return; - if (!bsp->cell_portals || bsp->num_cell_portals == 0) return; - if (!bsp->materials || bsp->num_materials == 0) return; + if (!gfx || !bsp || bsp->num_faces == 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_mat4* vp = pxl8_3d_get_view_proj(gfx); if (!frustum || !vp) return; 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; 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 total_faces = 0; for (u32 leaf_id = 0; leaf_id < bsp->num_leafs; leaf_id++) { - u32 byte = leaf_id >> 3; - u32 bit = leaf_id & 7; - if (!(visited[byte] & (1 << bit))) continue; + if (!(visited[leaf_id >> 3] & (1 << (leaf_id & 7)))) continue; + if (bsp->leafs[leaf_id].contents == -1) continue; 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++) { 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]; u32 material_id = face->material_id; if (material_id >= bsp->num_materials) continue; - total_faces++; if (material_id != current_material && mesh->index_count > 0 && current_material < bsp->num_materials) { 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]); } - 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_free(visited); pxl8_free(cell_windows); pxl8_free(queue); 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; + } +} diff --git a/src/world/pxl8_bsp.h b/src/world/pxl8_bsp.h index 84b4ce7..74aafa8 100644 --- a/src/world/pxl8_bsp.h +++ b/src/world/pxl8_bsp.h @@ -136,22 +136,25 @@ typedef struct pxl8_bsp { extern "C" { #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); - +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); bool pxl8_bsp_is_leaf_visible(const pxl8_bsp* bsp, i32 leaf_from, i32 leaf_to); - -pxl8_bsp_pvs pxl8_bsp_decompress_pvs(const pxl8_bsp* bsp, i32 leaf); +bool pxl8_bsp_point_solid(const pxl8_bsp* bsp, pxl8_vec3 pos); +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); 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_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 } diff --git a/src/world/pxl8_chunk.c b/src/world/pxl8_chunk.c new file mode 100644 index 0000000..d7bcbd5 --- /dev/null +++ b/src/world/pxl8_chunk.c @@ -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); +} diff --git a/src/world/pxl8_chunk.h b/src/world/pxl8_chunk.h new file mode 100644 index 0000000..b531d32 --- /dev/null +++ b/src/world/pxl8_chunk.h @@ -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 diff --git a/src/world/pxl8_chunk_cache.c b/src/world/pxl8_chunk_cache.c new file mode 100644 index 0000000..2d9b34b --- /dev/null +++ b/src/world/pxl8_chunk_cache.c @@ -0,0 +1,538 @@ +#include "pxl8_chunk_cache.h" + +#include +#include + +#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; + } +} diff --git a/src/world/pxl8_chunk_cache.h b/src/world/pxl8_chunk_cache.h new file mode 100644 index 0000000..0a2a49b --- /dev/null +++ b/src/world/pxl8_chunk_cache.h @@ -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 diff --git a/src/world/pxl8_entity.c b/src/world/pxl8_entity.c new file mode 100644 index 0000000..e260b8a --- /dev/null +++ b/src/world/pxl8_entity.c @@ -0,0 +1,496 @@ +#include "pxl8_entity.h" + +#include + +#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); + } + } +} diff --git a/src/world/pxl8_entity.h b/src/world/pxl8_entity.h new file mode 100644 index 0000000..e66a05d --- /dev/null +++ b/src/world/pxl8_entity.h @@ -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 diff --git a/src/world/pxl8_voxel.c b/src/world/pxl8_voxel.c new file mode 100644 index 0000000..20fb38b --- /dev/null +++ b/src/world/pxl8_voxel.c @@ -0,0 +1,406 @@ +#include "pxl8_voxel.h" + +#include + +#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 = ®istry->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; +} diff --git a/src/world/pxl8_voxel.h b/src/world/pxl8_voxel.h new file mode 100644 index 0000000..e4feef7 --- /dev/null +++ b/src/world/pxl8_voxel.h @@ -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 diff --git a/src/world/pxl8_world.c b/src/world/pxl8_world.c index b4d1840..b14d9c1 100644 --- a/src/world/pxl8_world.c +++ b/src/world/pxl8_world.c @@ -1,413 +1,125 @@ #include "pxl8_world.h" -#include -#include +#include -#include "pxl8_bsp.h" -#include "pxl8_gen.h" #include "pxl8_log.h" -#include "pxl8_math.h" #include "pxl8_mem.h" +#define PXL8_WORLD_ENTITY_CAPACITY 256 + struct pxl8_world { - pxl8_bsp bsp; - bool loaded; + pxl8_chunk* active_chunk; + pxl8_block_registry* block_registry; + pxl8_chunk_cache* chunk_cache; + pxl8_entity_pool* entities; }; pxl8_world* pxl8_world_create(void) { - pxl8_world* world = (pxl8_world*)pxl8_calloc(1, sizeof(pxl8_world)); - if (!world) { - pxl8_error("Failed to allocate world"); + pxl8_world* world = pxl8_calloc(1, sizeof(pxl8_world)); + if (!world) return NULL; + + 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; } - world->loaded = false; - return world; } void pxl8_world_destroy(pxl8_world* world) { if (!world) return; - if (world->loaded) { - pxl8_bsp_destroy(&world->bsp); - } - + pxl8_block_registry_destroy(world->block_registry); + pxl8_chunk_cache_destroy(world->chunk_cache); + pxl8_entity_pool_destroy(world->entities); pxl8_free(world); } -pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params) { - if (!world || !gfx || !params) { - pxl8_error("Invalid arguments to pxl8_world_generate"); - 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_chunk_cache* pxl8_world_chunk_cache(pxl8_world* world) { + if (!world) return NULL; + return world->chunk_cache; } -pxl8_result pxl8_world_load(pxl8_world* world, const char* path) { - if (!world || !path) 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_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; +pxl8_chunk* pxl8_world_active_chunk(pxl8_world* world) { + if (!world) return NULL; + return world->active_chunk; } -void pxl8_world_unload(pxl8_world* world) { - if (!world || !world->loaded) return; - - pxl8_bsp_destroy(&world->bsp); - world->loaded = false; +void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_chunk* chunk) { + if (!world) return; + world->active_chunk = chunk; } -pxl8_result pxl8_world_apply_textures(pxl8_world* world, const pxl8_world_texture* textures, u32 count) { - if (!world || !world->loaded || !textures || count == 0) { - return PXL8_ERROR_INVALID_ARGUMENT; - } +pxl8_block_registry* pxl8_world_block_registry(pxl8_world* world) { + if (!world) return NULL; + 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; - bsp->materials = pxl8_calloc(max_materials, sizeof(pxl8_gfx_material)); - if (!bsp->materials) { - 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; +pxl8_entity pxl8_world_spawn(pxl8_world* world) { + if (!world || !world->entities) return PXL8_ENTITY_INVALID; + return pxl8_entity_spawn(world->entities); } 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++) { - 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; - - 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; + if (world->active_chunk->type == PXL8_CHUNK_BSP && world->active_chunk->bsp) { + return pxl8_bsp_point_solid(world->active_chunk->bsp, pos); } return false; } -bool pxl8_world_is_loaded(const pxl8_world* world) { - return world && world->loaded; +pxl8_vec3 pxl8_world_resolve_collision(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) { + 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) { - if (!world || !world->loaded) return to; +void pxl8_world_update(pxl8_world* world, f32 dt) { + (void)dt; + if (!world) return; - const pxl8_bsp* bsp = &world->bsp; - 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; + pxl8_chunk_cache_tick(world->chunk_cache); } void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) { - if (!world || !gfx || !world->loaded) { - 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); + if (!world || !gfx || !world->active_chunk) return; + + 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_sync(pxl8_world* world, pxl8_net* net) { + if (!world || !net) return; + + 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); + } } - return; - } - - pxl8_bsp_render(gfx, &world->bsp, camera_pos); -} - -void pxl8_world_set_wireframe(pxl8_world* world, bool enabled) { - if (!world || !world->loaded) return; - - for (u32 i = 0; i < world->bsp.num_materials; i++) { - world->bsp.materials[i].wireframe = enabled; } } diff --git a/src/world/pxl8_world.h b/src/world/pxl8_world.h index 8e78fd1..1346cd2 100644 --- a/src/world/pxl8_world.h +++ b/src/world/pxl8_world.h @@ -1,38 +1,36 @@ #pragma once -#include "pxl8_bsp.h" -#include "pxl8_gen.h" +#include "pxl8_chunk.h" +#include "pxl8_chunk_cache.h" +#include "pxl8_entity.h" #include "pxl8_gfx.h" #include "pxl8_math.h" +#include "pxl8_net.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 extern "C" { #endif +typedef struct pxl8_world pxl8_world; + pxl8_world* pxl8_world_create(void); void pxl8_world_destroy(pxl8_world* world); -pxl8_result pxl8_world_generate(pxl8_world* world, pxl8_gfx* gfx, const pxl8_procgen_params* params); -pxl8_result pxl8_world_load(pxl8_world* world, const char* path); -void pxl8_world_unload(pxl8_world* world); +pxl8_chunk_cache* pxl8_world_chunk_cache(pxl8_world* world); +pxl8_chunk* pxl8_world_active_chunk(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_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); -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 }