add networking, 3d improvements, reorganize src structure

This commit is contained in:
asrael 2026-01-17 22:52:36 -06:00
parent 39b604b333
commit 415d424057
122 changed files with 5358 additions and 721 deletions

View file

@ -0,0 +1,2 @@
[unstable]
build-std = ["core", "alloc"]

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

1090
server/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

34
server/Cargo.toml Normal file
View file

@ -0,0 +1,34 @@
[package]
name = "pxl8-server"
version = "0.1.0"
edition = "2024"
[build-dependencies]
bindgen = "0.72"
[dependencies]
bevy_ecs = { version = "0.18", default-features = false }
libc = { version = "0.2", default-features = false }
libm = { version = "0.2", default-features = false }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", default-features = false, features = [
"Win32_System_Memory",
"Win32_System_Performance",
"Win32_System_Threading",
"Win32_Networking_WinSock",
] }
[[bin]]
name = "pxl8-server"
path = "src/main.rs"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[features]
default = []
std = ["bevy_ecs/std"]

23
server/build.rs Normal file
View file

@ -0,0 +1,23 @@
use std::env;
use std::path::PathBuf;
fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let client_src = PathBuf::from(&manifest_dir).join("../client/src");
println!("cargo:rerun-if-changed=../client/src/net/pxl8_protocol.h");
println!("cargo:rerun-if-changed=../client/src/core/pxl8_types.h");
let bindings = bindgen::Builder::default()
.header(client_src.join("net/pxl8_protocol.h").to_str().unwrap())
.clang_arg(format!("-I{}", client_src.join("core").display()))
.use_core()
.rustified_enum(".*")
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("protocol.rs"))
.expect("Couldn't write bindings");
}

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

36
server/src/allocator.rs Normal file
View file

@ -0,0 +1,36 @@
use core::alloc::{GlobalAlloc, Layout};
pub struct Allocator;
#[cfg(unix)]
unsafe impl GlobalAlloc for Allocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { libc::memalign(layout.align(), layout.size()) as *mut u8 }
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
unsafe { libc::free(ptr as *mut libc::c_void) }
}
unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 {
unsafe { libc::realloc(ptr as *mut libc::c_void, new_size) as *mut u8 }
}
}
#[cfg(windows)]
unsafe impl GlobalAlloc for Allocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
use windows_sys::Win32::System::Memory::*;
unsafe { HeapAlloc(GetProcessHeap(), 0, layout.size()) as *mut u8 }
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
use windows_sys::Win32::System::Memory::*;
unsafe { HeapFree(GetProcessHeap(), 0, ptr as *mut core::ffi::c_void) };
}
unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 {
use windows_sys::Win32::System::Memory::*;
unsafe { HeapReAlloc(GetProcessHeap(), 0, ptr as *mut core::ffi::c_void, new_size) as *mut u8 }
}
}

45
server/src/components.rs Normal file
View file

@ -0,0 +1,45 @@
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);

18
server/src/lib.rs Normal file
View file

@ -0,0 +1,18 @@
#![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::*;

142
server/src/main.rs Normal file
View file

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

133
server/src/simulation.rs Normal file
View file

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

281
server/src/transport.rs Normal file
View file

@ -0,0 +1,281 @@
use crate::protocol::*;
pub const DEFAULT_PORT: u16 = 7777;
#[cfg(unix)]
mod sys {
use libc::{c_int, c_void, sockaddr, sockaddr_in, socklen_t};
pub type RawSocket = c_int;
pub const INVALID_SOCKET: RawSocket = -1;
pub fn socket() -> RawSocket {
unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) }
}
pub fn bind(sock: RawSocket, port: u16) -> c_int {
let addr = sockaddr_in {
sin_family: libc::AF_INET as u16,
sin_port: port.to_be(),
sin_addr: libc::in_addr { s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be() },
sin_zero: [0; 8],
};
unsafe { libc::bind(sock, &addr as *const _ as *const sockaddr, core::mem::size_of::<sockaddr_in>() as socklen_t) }
}
pub fn set_nonblocking(sock: RawSocket) -> c_int {
unsafe {
let flags = libc::fcntl(sock, libc::F_GETFL, 0);
libc::fcntl(sock, libc::F_SETFL, flags | libc::O_NONBLOCK)
}
}
pub fn recvfrom(sock: RawSocket, buf: &mut [u8], addr: &mut sockaddr_in) -> isize {
let mut addr_len = core::mem::size_of::<sockaddr_in>() as socklen_t;
unsafe {
libc::recvfrom(
sock,
buf.as_mut_ptr() as *mut c_void,
buf.len(),
0,
addr as *mut _ as *mut sockaddr,
&mut addr_len,
)
}
}
pub fn sendto(sock: RawSocket, buf: &[u8], addr: &sockaddr_in) -> isize {
unsafe {
libc::sendto(
sock,
buf.as_ptr() as *const c_void,
buf.len(),
0,
addr as *const _ as *const sockaddr,
core::mem::size_of::<sockaddr_in>() as socklen_t,
)
}
}
pub fn close(sock: RawSocket) {
unsafe { libc::close(sock) };
}
}
#[cfg(windows)]
mod sys {
use windows_sys::Win32::Networking::WinSock::*;
pub type RawSocket = SOCKET;
pub const INVALID_SOCKET_VAL: RawSocket = INVALID_SOCKET;
pub fn socket() -> RawSocket {
unsafe { socket(AF_INET as i32, SOCK_DGRAM as i32, 0) }
}
pub fn bind(sock: RawSocket, port: u16) -> i32 {
let addr = SOCKADDR_IN {
sin_family: AF_INET,
sin_port: port.to_be(),
sin_addr: IN_ADDR { S_un: IN_ADDR_0 { S_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be() } },
sin_zero: [0; 8],
};
unsafe { bind(sock, &addr as *const _ as *const SOCKADDR, core::mem::size_of::<SOCKADDR_IN>() as i32) }
}
pub fn set_nonblocking(sock: RawSocket) -> i32 {
let mut nonblocking: u32 = 1;
unsafe { ioctlsocket(sock, FIONBIO as i32, &mut nonblocking) }
}
pub fn recvfrom(sock: RawSocket, buf: &mut [u8], addr: &mut SOCKADDR_IN) -> i32 {
let mut addr_len = core::mem::size_of::<SOCKADDR_IN>() as i32;
unsafe {
recvfrom(
sock,
buf.as_mut_ptr(),
buf.len() as i32,
0,
addr as *mut _ as *mut SOCKADDR,
&mut addr_len,
)
}
}
pub fn sendto(sock: RawSocket, buf: &[u8], addr: &SOCKADDR_IN) -> i32 {
unsafe {
sendto(
sock,
buf.as_ptr(),
buf.len() as i32,
0,
addr as *const _ as *const SOCKADDR,
core::mem::size_of::<SOCKADDR_IN>() as i32,
)
}
}
pub fn close(sock: RawSocket) {
unsafe { closesocket(sock) };
}
}
#[cfg(unix)]
type SockAddr = libc::sockaddr_in;
#[cfg(windows)]
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],
socket: sys::RawSocket,
}
impl Transport {
pub fn bind(port: u16) -> Option<Self> {
let sock = sys::socket();
if sock == sys::INVALID_SOCKET {
return None;
}
if sys::bind(sock, port) < 0 {
sys::close(sock);
return None;
}
if sys::set_nonblocking(sock) < 0 {
sys::close(sock);
return None;
}
Some(Self {
client_addr: unsafe { core::mem::zeroed() },
has_client: false,
recv_buf: [0u8; 4096],
send_buf: [0u8; 4096],
socket: sock,
})
}
pub fn recv(&mut self) -> Option<u8> {
let mut addr: SockAddr = unsafe { core::mem::zeroed() };
let len = sys::recvfrom(self.socket, &mut self.recv_buf, &mut addr);
if len <= 0 || (len as usize) < size_of::<pxl8_msg_header>() {
return None;
}
self.client_addr = addr;
self.has_client = true;
let header = self.deserialize_header();
Some(header.type_)
}
pub fn get_input(&self) -> pxl8_input_msg {
self.deserialize_input()
}
pub fn get_command(&self) -> pxl8_command_msg {
self.deserialize_command()
}
pub fn send_snapshot(
&mut self,
header: &pxl8_snapshot_header,
entities: &[pxl8_entity_state],
sequence: u32,
) {
if !self.has_client {
return;
}
let mut offset = 0;
let msg_header = pxl8_msg_header {
sequence,
size: 0,
type_: pxl8_msg_type::PXL8_MSG_SNAPSHOT as u8,
version: PXL8_PROTOCOL_VERSION as u8,
};
offset += self.serialize_header(&msg_header, offset);
offset += self.serialize_snapshot_header(header, offset);
for entity in entities {
offset += self.serialize_entity_state(entity, offset);
}
sys::sendto(self.socket, &self.send_buf[..offset], &self.client_addr);
}
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());
buf[4..6].copy_from_slice(&h.size.to_be_bytes());
buf[6] = h.type_;
buf[7] = h.version;
8
}
fn serialize_snapshot_header(&mut self, h: &pxl8_snapshot_header, offset: usize) -> usize {
let buf = &mut self.send_buf[offset..];
buf[0..2].copy_from_slice(&h.entity_count.to_be_bytes());
buf[2..4].copy_from_slice(&h.event_count.to_be_bytes());
buf[4..12].copy_from_slice(&h.player_id.to_be_bytes());
buf[12..20].copy_from_slice(&h.tick.to_be_bytes());
buf[20..24].copy_from_slice(&h.time.to_be_bytes());
24
}
fn serialize_entity_state(&mut self, e: &pxl8_entity_state, offset: usize) -> usize {
let buf = &mut self.send_buf[offset..];
buf[0..8].copy_from_slice(&e.entity_id.to_be_bytes());
buf[8..64].copy_from_slice(&e.userdata);
64
}
fn deserialize_header(&self) -> pxl8_msg_header {
let buf = &self.recv_buf;
pxl8_msg_header {
sequence: u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]),
size: u16::from_be_bytes([buf[4], buf[5]]),
type_: buf[6],
version: buf[7],
}
}
fn deserialize_input(&self) -> pxl8_input_msg {
let buf = &self.recv_buf[8..];
pxl8_input_msg {
buttons: u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]),
look_dx: f32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]),
look_dy: f32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]),
move_x: f32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]),
move_y: f32::from_be_bytes([buf[16], buf[17], buf[18], buf[19]]),
yaw: f32::from_be_bytes([buf[20], buf[21], buf[22], buf[23]]),
tick: u64::from_be_bytes([buf[24], buf[25], buf[26], buf[27], buf[28], buf[29], buf[30], buf[31]]),
timestamp: u64::from_be_bytes([buf[32], buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39]]),
}
}
fn deserialize_command(&self) -> pxl8_command_msg {
let buf = &self.recv_buf[8..];
let mut cmd = pxl8_command_msg {
cmd_type: u16::from_be_bytes([buf[0], buf[1]]),
payload: [0u8; 64],
payload_size: u16::from_be_bytes([buf[66], buf[67]]),
tick: u64::from_be_bytes([buf[68], buf[69], buf[70], buf[71], buf[72], buf[73], buf[74], buf[75]]),
};
cmd.payload.copy_from_slice(&buf[2..66]);
cmd
}
}
impl Drop for Transport {
fn drop(&mut self) {
sys::close(self.socket);
}
}