proximity server+

This commit is contained in:
Aubrey 2024-12-17 18:44:37 -06:00
parent 1963549b45
commit 6708cc3e22
No known key found for this signature in database
36 changed files with 709 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
}

13
examples/extract.rs Normal file
View file

@ -0,0 +1,13 @@
fn main() {
let stages = std::fs::read_dir("game/stages").unwrap();
for stage in stages {
let stage = stage.unwrap();
let bytes: Vec<u8> = std::fs::read(stage.path()).unwrap();
let data = roead::byml::Byml::from_binary(bytes).unwrap();
let map = data.into_map().unwrap();
let mtx: Vec<f32> =
map.get("ProjMatrix").unwrap().as_array().unwrap().into_iter().map(|a| a.as_float().unwrap()).collect();
let mat4 = glam::Mat4::from_cols_slice(&mtx);
println!("{:?}", mat4);
}
}

BIN
game/StagePosList.byml Normal file

Binary file not shown.

BIN
game/WorldListFromDb.byml Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
p12.nu Normal file
View file

@ -0,0 +1 @@
openssl pkcs12 -export -in cert.pem -inkey key.pem -out server.p12

63
src/faker.rs Normal file
View file

@ -0,0 +1,63 @@
use glam::Vec3;
use xtra::{Actor, Address, Handler};
use crate::{
broadcast_packet,
packet::{
Packet, PacketData,
PacketData_variants::{Connect, Player},
},
protocol::String,
};
pub struct Faker {
pub address: Address<Faker>,
}
impl Actor for Faker {
type Stop = ();
async fn stopped(self) -> Self::Stop {}
}
impl Handler<Packet> for Faker {
type Return = ();
async fn handle(&mut self, message: Packet, _: &mut xtra::Context<Self>) -> Self::Return {
// trace!("got packet {message:?}");
match message.data {
PacketData::Connect(connect) => {
broadcast_packet(Packet {
user_id: 1,
udp: message.udp,
data: PacketData::Connect(Connect {
client_name: String::try_from("Bot").unwrap(),
..connect
}),
})
.await;
}
PacketData::Player(player) => {
let pos = Vec3::from_array(player.position);
broadcast_packet(Packet {
user_id: 1,
udp: message.udp,
data: PacketData::Player(Player {
position: (pos + Vec3::new(0., -150., 0.)).into(),
..player
}),
})
.await;
}
data => {
broadcast_packet(Packet {
user_id: 1,
udp: message.udp,
data,
})
.await;
}
}
}
}

0
src/game/mod.rs Normal file
View file

143
src/packet/mod.rs Normal file
View file

@ -0,0 +1,143 @@
pub mod rw;
use newtype_enum::newtype_enum;
use zerocopy::{FromZeros, Immutable, IntoBytes, KnownLayout};
use crate::protocol::{Bool, String};
#[derive(Debug, Clone, Copy, FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
pub struct PacketHeader {
pub user_id: u128,
pub kind: PacketKind,
pub size: u16,
}
#[derive(Debug, Clone, Copy, FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(u16)]
pub enum PacketKind {
Unknown = 0,
Init = 1,
Player = 2,
Cap = 3,
Game = 4,
Tag = 5,
Connect = 6,
Disconnect = 7,
Costume = 8,
Shine = 9,
Capture = 10,
ChangeStage = 11,
Command = 12,
UdpInit = 13,
HolePunch = 14,
}
const COSTUME_NAME_SIZE: usize = 0x20;
const CAP_ANIM_SIZE: usize = 0x30;
pub const STAGE_GAME_NAME_SIZE: usize = 0x40;
const STAGE_CHANGE_NAME_SIZE: usize = 0x30;
const STAGE_ID_SIZE: usize = 0x10;
pub const CLIENT_NAME_SIZE: usize = COSTUME_NAME_SIZE;
#[derive(Clone, Debug)]
pub struct Packet {
pub user_id: u128,
pub udp: bool,
pub data: PacketData,
}
#[newtype_enum]
#[derive(Clone, Debug)]
pub enum PacketData {
Unknown(Vec<u8>),
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Init {
pub max_players: u16,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Player {
position: [f32; 3],
rotation: [f32; 4],
weights: [f32; 6],
action: u16,
subaction: u16,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Cap {
position: [f32; 3],
rotation: [f32; 4],
out: Bool,
anim: String<CAP_ANIM_SIZE>,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Game {
is_2d: u8,
scenario_num: u8,
stage: String<STAGE_GAME_NAME_SIZE>,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Tag {
update_type: u8,
is_it: Bool,
seconds: u8,
minutes: u16,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Connect {
kind: ConnectionKind,
max_player: u16,
client_name: String<CLIENT_NAME_SIZE>,
},
Disconnect,
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Costume {
body_name: String<COSTUME_NAME_SIZE>,
cap_name: String<COSTUME_NAME_SIZE>,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Shine {
shine_id: i32,
is_grand: Bool,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
Capture {
model: String<COSTUME_NAME_SIZE>,
},
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
ChangeStage {
stage: String<STAGE_CHANGE_NAME_SIZE>,
id: String<STAGE_ID_SIZE>,
scenario: i8,
sub_scenario: u8,
},
Command,
#[derive(FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
UdpInit {
port: u16,
},
HolePunch,
}
pub enum TagUpdateBit {
Time = 0,
State = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, FromZeros, IntoBytes, KnownLayout, Immutable)]
#[repr(u32)]
pub enum ConnectionKind {
New = 0,
Old = 1,
}

114
src/packet/rw.rs Normal file
View file

@ -0,0 +1,114 @@
use anyhow::{bail, Context};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use zerocopy::{IntoBytes, TryFromBytes};
use crate::packet::{
Packet, PacketData,
PacketData_variants::{Command, Disconnect, HolePunch},
PacketHeader, PacketKind,
};
pub async fn read_packet<R: AsyncReadExt + Unpin>(reader: &mut R, udp: bool) -> anyhow::Result<Packet> {
let mut header = [0; size_of::<PacketHeader>()];
reader.read_exact(&mut header).await.context("reading data")?;
let Ok(header) = PacketHeader::try_read_from_bytes(&header) else {
bail!("parsing packet buffer")
};
macro_rules! read_data {
($read: expr, $size: expr, $ty: ident $(, $field:ident => $assert: expr)*) => {{
async fn read_data<R: AsyncReadExt + Unpin>(reader: &mut R, size: u16) -> anyhow::Result<PacketData> {
type T = crate::packet::PacketData_variants::$ty;
let size = size as usize;
if size < size_of::<T>() {
bail!("buffer too small for packet: expected {}, got {size}", size_of::<T>())
}
let mut data = [0; u16::MAX as usize];
reader.read_exact(&mut data[..size]).await.context("reading data")?;
let packet_data = match T::try_read_from_bytes(&data[..size_of::<T>()]) {
Ok(data) => data,
Err(error) => {
bail!(concat!("interpreting ", stringify!($ty), ": {:?}"), error)
}
};
$(
{
let $field = packet_data.$field;
$assert
}?;
)*
Ok(PacketData::$ty(packet_data))
}
type _FixIntellisense = crate::packet::PacketData_variants::$ty;
read_data($read, $size).await?
}};
}
let data: PacketData = match header.kind {
PacketKind::Unknown => {
let mut data = vec![0; header.size.into()];
reader.read_exact(&mut data).await.context("reading unknown data")?;
PacketData::Unknown(data)
}
PacketKind::Init => read_data!(reader, header.size, Init),
PacketKind::Player => read_data!(reader, header.size, Player),
PacketKind::Cap => read_data!(reader, header.size, Cap, anim => anim.assert_valid()),
PacketKind::Game => read_data!(reader, header.size, Game, stage => stage.assert_valid()),
PacketKind::Tag => read_data!(reader, header.size, Tag),
PacketKind::Connect => read_data!(reader, header.size, Connect),
PacketKind::Disconnect => PacketData::Disconnect(Disconnect),
PacketKind::Costume => read_data!(reader, header.size, Costume),
PacketKind::Shine => read_data!(reader, header.size, Shine),
PacketKind::Capture => read_data!(reader, header.size, Capture),
PacketKind::ChangeStage => read_data!(reader, header.size, ChangeStage),
PacketKind::Command => PacketData::Command(Command),
PacketKind::UdpInit => read_data!(reader, header.size, UdpInit),
PacketKind::HolePunch => PacketData::HolePunch(HolePunch),
};
Ok(Packet {
user_id: header.user_id,
udp,
data,
})
}
pub async fn write_packet<W: AsyncWriteExt + Unpin>(writer: &mut W, id: u128, data: PacketData) -> anyhow::Result<()> {
let (kind, slice) = match &data {
PacketData::Unknown(vec) => {
if vec.len() >= (256 as usize) {
bail!("unknown packet vec too large")
}
(PacketKind::Unknown, vec.as_slice())
}
PacketData::Init(init) => (PacketKind::Init, init.as_bytes()),
PacketData::Player(player) => (PacketKind::Player, player.as_bytes()),
PacketData::Cap(cap) => (PacketKind::Cap, cap.as_bytes()),
PacketData::Game(game) => (PacketKind::Game, game.as_bytes()),
PacketData::Tag(tag) => (PacketKind::Tag, tag.as_bytes()),
PacketData::Connect(connect) => (PacketKind::Connect, connect.as_bytes()),
PacketData::Disconnect(..) => (PacketKind::Disconnect, [].as_slice()),
PacketData::Costume(costume) => (PacketKind::Costume, costume.as_bytes()),
PacketData::Shine(shine) => (PacketKind::Shine, shine.as_bytes()),
PacketData::Capture(capture) => (PacketKind::Capture, capture.as_bytes()),
PacketData::ChangeStage(change_stage) => (PacketKind::ChangeStage, change_stage.as_bytes()),
PacketData::Command(..) => (PacketKind::Command, [].as_slice()),
PacketData::UdpInit(udp_init) => (PacketKind::UdpInit, udp_init.as_bytes()),
PacketData::HolePunch(..) => (PacketKind::HolePunch, [].as_slice()),
};
writer
.write_all(
PacketHeader {
kind,
size: slice.len() as u16,
user_id: id,
}
.as_bytes(),
)
.await
.context("writing header")?;
writer.write_all(slice).await.context("writing data")?;
writer.flush().await.context("flushing writer")
}

76
src/protocol.rs Normal file
View file

@ -0,0 +1,76 @@
use std::{
ffi::CStr,
fmt::{Debug, Display},
};
use anyhow::{bail, Context};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
#[derive(Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C)]
pub struct Bool(u8);
impl Bool {
pub fn new(value: bool) -> Bool {
Bool(if value { 1 } else { 0 })
}
pub fn get(&self) -> bool {
self.0 != 0
}
}
impl From<bool> for Bool {
fn from(value: bool) -> Self {
Bool::new(value)
}
}
impl From<Bool> for bool {
fn from(value: Bool) -> Self {
value.get()
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C)]
pub struct String<const N: usize>([u8; N]);
impl<const N: usize> String<N> {
pub fn as_str(&self) -> &str {
self.try_as_str().expect("wasn't a string")
}
pub fn try_as_str(&self) -> anyhow::Result<&str> {
let cstr = CStr::from_bytes_until_nul(&self.0).context("interpreting bytes as c-string")?;
cstr.to_str().context("verifying string has utf-8")
// let str = str::from_utf8(&self.0).context("verifying string has utf-8")?;
// Ok(str.trim_end_matches('\0'))
}
pub fn assert_valid(&self) -> anyhow::Result<()> {
self.try_as_str().map(drop)
}
}
impl<const N: usize> TryFrom<&str> for String<N> {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut buf = [0; N];
if value.len() > N {
bail!("seggs")
}
value.write_to_prefix(&mut buf).unwrap();
Ok(Self(buf))
}
}
impl<const N: usize> Display for String<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(self.try_as_str().expect("failed to parse string"), f)
}
}
impl<const N: usize> Debug for String<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self.try_as_str().expect("failed to parse string"), f)
}
}

170
src/server/mod.rs Normal file
View file

@ -0,0 +1,170 @@
mod packet;
mod prox;
use std::{collections::HashMap, sync::LazyLock};
use glam::Vec3;
use prox::ProximityPlayer;
use tokio::sync::RwLock;
use tracing::{error, info_span, Instrument};
use uuid::Uuid;
use wtransport::{Endpoint, Identity, ServerConfig};
use xtra::{Actor, Address, Handler, Mailbox};
use zerocopy::FromZeros;
use crate::{
packet::{CLIENT_NAME_SIZE, STAGE_GAME_NAME_SIZE},
protocol::String,
};
fn listeners() -> &'static RwLock<HashMap<Uuid, Address<ProximityPlayer>>> {
static LISTENERS: LazyLock<RwLock<HashMap<Uuid, Address<ProximityPlayer>>>> = LazyLock::new(Default::default);
&LISTENERS
}
pub fn web_main() -> Address<Manager> {
let span = info_span!("wt");
let manager = xtra::spawn_tokio(
Manager {
players: HashMap::new(),
next_id: 0,
},
Mailbox::bounded(8),
);
tokio::spawn({
let manager = manager.clone();
async move {
if let Err(result) = webtransport_server(manager).await {
error!("{:?}", result);
}
}
.instrument(span)
});
manager
}
async fn webtransport_server(manager: Address<Manager>) -> anyhow::Result<()> {
// let identity = Identity::self_signed(["localhost", "127.0.0.1", "::1"]).unwrap();
let identity = Identity::load_pemfiles("./cert.pem", "./key.pem").await.unwrap();
let config = ServerConfig::builder().with_bind_default(4433).with_identity(identity).build();
let endpoint = Endpoint::server(config)?;
loop {
let connection = endpoint.accept().await;
ProximityPlayer::spawn(connection, manager.clone());
}
}
#[derive(Actor)]
pub struct Manager {
players: HashMap<u128, PlayerInstance>,
next_id: u32,
}
impl Manager {
pub async fn broadcast<M: Copy + Send + 'static>(&self, message: M)
where
ProximityPlayer: Handler<M>,
{
for player in listeners().read().await.values() {
let _ = player.send(message).detach().await;
}
}
}
#[derive(Debug, Clone, Copy)]
struct PlayerInstance {
id: u32,
name: String<CLIENT_NAME_SIZE>,
position: Vec3,
stage: String<STAGE_GAME_NAME_SIZE>,
}
struct RequestState;
impl Handler<RequestState> for Manager {
type Return = HashMap<u128, PlayerInstance>;
async fn handle(&mut self, _: RequestState, _: &mut xtra::Context<Self>) -> Self::Return {
self.players.clone()
}
}
pub struct PlayerConnected {
pub id: u128,
pub name: String<CLIENT_NAME_SIZE>,
}
impl Handler<PlayerConnected> for Manager {
type Return = ();
async fn handle(&mut self, message: PlayerConnected, _: &mut xtra::Context<Self>) -> Self::Return {
self.players.insert(
message.id,
PlayerInstance {
id: {
let id = self.next_id;
self.next_id += 1;
id
},
name: message.name,
position: Vec3::ZERO,
stage: String::new_zeroed(),
},
);
}
}
#[derive(Clone, Copy)]
pub struct PlayerDisconnected {
pub id: u128,
}
impl Handler<PlayerDisconnected> for Manager {
type Return = ();
async fn handle(&mut self, message: PlayerDisconnected, _: &mut xtra::Context<Self>) -> Self::Return {
if self.players.remove(&message.id).is_some() {
self.broadcast(message).await;
}
}
}
#[derive(Clone, Copy)]
pub struct PlayerMoved {
pub id: u128,
pub position: Vec3,
}
impl Handler<PlayerMoved> for Manager {
type Return = ();
async fn handle(&mut self, message: PlayerMoved, _: &mut xtra::Context<Self>) -> Self::Return {
if let Some(player) = self.players.get_mut(&message.id) {
player.position = message.position;
self.broadcast(message).await;
}
}
}
#[derive(Clone, Copy)]
pub struct ChangedStage {
pub id: u128,
pub stage: String<STAGE_GAME_NAME_SIZE>,
}
impl Handler<ChangedStage> for Manager {
type Return = ();
async fn handle(&mut self, message: ChangedStage, _: &mut xtra::Context<Self>) -> Self::Return {
if let Some(player) = self.players.get_mut(&message.id) {
player.stage = message.stage;
self.broadcast(message).await;
}
}
}

30
src/server/packet.rs Normal file
View file

@ -0,0 +1,30 @@
use zerocopy::{Immutable, IntoBytes};
use crate::{
packet::{CLIENT_NAME_SIZE, STAGE_GAME_NAME_SIZE},
protocol::String,
};
use super::PlayerInstance;
#[allow(unused)]
#[derive(Debug, IntoBytes, Immutable)]
#[repr(C, packed)]
pub struct HelloPlayer {
name: String<CLIENT_NAME_SIZE>,
id: u32,
position: [f32; 3],
stage: String<STAGE_GAME_NAME_SIZE>,
}
impl From<PlayerInstance> for HelloPlayer {
fn from(value: PlayerInstance) -> Self {
Self {
id: value.id,
name: value.name,
position: value.position.to_array(),
stage: value.stage,
}
}
}

91
src/server/prox.rs Normal file
View file

@ -0,0 +1,91 @@
use core::str;
use tokio::io::AsyncWriteExt;
use tracing::{info_span, trace, warn, Instrument};
use uuid::Uuid;
use wtransport::endpoint::IncomingSession;
use xtra::{Actor, Address, Handler, Mailbox};
use zerocopy::IntoBytes;
use super::{
listeners, packet::HelloPlayer, ChangedStage, Manager, PlayerConnected, PlayerDisconnected, PlayerMoved, RequestState,
};
pub struct ProximityPlayer {
id: Uuid,
_send: wtransport::SendStream,
_connection: wtransport::Connection,
}
impl ProximityPlayer {
pub fn spawn(session: IncomingSession, manager: Address<Manager>) {
tokio::spawn(
async move {
trace!("proximity chat client connected");
let connection =
session.await.expect("failed to acknowledge session").accept().await.expect("failed to accept session");
let (mut send, mut recv) = connection.accept_bi().await.expect("failed to start channel");
trace!("getting peerjs uuid");
let mut buffer = [0; 36];
recv.read_exact(buffer.as_mut_bytes()).await.expect("failed to read uuid");
let id = Uuid::parse_str(str::from_utf8(&buffer).expect("expected utf8")).expect("failed to parse uuid");
let span = info_span!("", %id);
span.in_scope(||trace!( "uuid parsed"));
let state = manager.send(RequestState).await.unwrap();
send.write_u8(state.len() as u8).await.expect("failed to write length");
for player in state.values() {
trace!("sending player {player:?}");
send.write_all(HelloPlayer::from(*player).as_bytes()).await.expect("failed to write player");
}
let (address, mailbox) = Mailbox::unbounded();
listeners().write().await.insert(id, address);
xtra::run(mailbox, ProximityPlayer { id, _send: send, _connection: connection }).instrument(span).await;
}
.in_current_span(),
);
}
}
impl Actor for ProximityPlayer {
type Stop = ();
async fn stopped(self) -> Self::Stop {
listeners().write().await.remove(&self.id);
}
}
impl Handler<PlayerConnected> for ProximityPlayer {
type Return = ();
async fn handle(&mut self, _message: PlayerConnected, _: &mut xtra::Context<Self>) -> Self::Return {
warn!("todo: implement player connected")
}
}
impl Handler<PlayerDisconnected> for ProximityPlayer {
type Return = ();
async fn handle(&mut self, _message: PlayerDisconnected, _: &mut xtra::Context<Self>) -> Self::Return {
warn!("todo: implement player disconnected")
}
}
impl Handler<PlayerMoved> for ProximityPlayer {
type Return = ();
async fn handle(&mut self, _message: PlayerMoved, _: &mut xtra::Context<Self>) -> Self::Return {
warn!("todo: implement player moved")
}
}
impl Handler<ChangedStage> for ProximityPlayer {
type Return = ();
async fn handle(&mut self, _message: ChangedStage, _: &mut xtra::Context<Self>) -> Self::Return {
warn!("todo: implement changed stage")
}
}

5
todo.txt Normal file
View file

@ -0,0 +1,5 @@
players -> manager (accumulate player state)
manager -> proximity player (relay state changes)
proximity player -> web (relay state changes, signaling)
web -> proximity player (signaling)