Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions protocol/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ use std::ops::{Index, IndexMut};
use std::slice::Iter;

use array_init::array_init;
use nalgebra::Point3;
use nalgebra::{Point2, Point3};
use strum::EnumCount;
use tokio::io;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};

use crate::{ReadCwData, WriteCwData};
use crate::utils::constants::SIZE_BLOCK;
use crate::utils::constants::{SIZE_BLOCK, SIZE_ZONE};
use crate::utils::io_extensions::{ReadArbitrary, WriteArbitrary};

pub mod io_extensions;
Expand Down Expand Up @@ -52,6 +52,13 @@ pub fn sound_position_of(position: Point3<i64>) -> Point3<f32> { //todo: move to
position.map(|scalar| scalar as f32 / SIZE_BLOCK as f32)
}

#[must_use]
pub fn zone_of(position: Point3<i64>) -> Point2<i32> {
position
.xy()
.map(|scalar| scalar.div_euclid(SIZE_ZONE) as i32)
}

///ideally this would be done with a `#[derive()]` macro instead,
///but the boilerplate required for that is completely overkill for this use case
#[macro_export]
Expand Down
1 change: 1 addition & 0 deletions server/src/addon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl Addons {
pub async fn start(&self) {
self.listforge_api.run().await;
self.discord_integration.run();
self.models.prepare().await;
freeze_time();
events::start();
}
Expand Down
25 changes: 14 additions & 11 deletions server/src/addon/command_manager/commands/test.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
use std::ops::{Div as _, Mul as _, Sub as _};
use std::str::SplitWhitespace;

use protocol::packet::creature_update::{Affiliation, Appearance, AppearanceFlag};
use protocol::packet::{CreatureUpdate, StatusEffect};
use protocol::packet::world_update::block::Kind;
use protocol::packet::status_effect;
use strum::IntoEnumIterator;
use tap::{Pipe, Tap};

use protocol::nalgebra::{Point3, Vector3};
use protocol::packet::WorldUpdate;
use protocol::packet::{CreatureUpdate, StatusEffect, WorldUpdate};
use protocol::packet::common::{CreatureId, Hitbox, Race};
use protocol::packet::creature_update::{Affiliation, Appearance, AppearanceFlag};
use protocol::packet::status_effect;
use protocol::packet::world_update::{Block, WorldObject};
use protocol::packet::world_update::block::Kind;
use protocol::packet::world_update::block::Kind::*;
use protocol::packet::world_update::sound;
use protocol::packet::world_update::world_object::Kind::{Crate, FireTrap};
use protocol::utils::constants::{SIZE_BLOCK, SIZE_ZONE};
use protocol::utils::flagset::FlagSet;
use strum::IntoEnumIterator;
use protocol::packet::world_update::sound;
use tap::{Pipe, Tap};
use protocol::utils::zone_of;

use crate::addon::{command_manager::{Command, CommandResult}, models, play_sound_at_player};
use crate::addon::command_manager::commands::Test;
Expand Down Expand Up @@ -282,11 +283,13 @@ async fn model(params: &mut SplitWhitespace<'_>, server: &Server, caller: &Playe
};
let mut blocks = models::parse_model(file);

let offset = caller
let position = caller
.character
.read()
.await
.position
.position;

let offset = position
.div(SIZE_BLOCK)
.cast()
.coords;
Expand All @@ -295,7 +298,7 @@ async fn model(params: &mut SplitWhitespace<'_>, server: &Server, caller: &Playe
block.position += offset;
}

server.broadcast(&WorldUpdate::from(blocks), None).await;
server.broadcast_near(&WorldUpdate::from(blocks), zone_of(position)).await;

Ok(())
}
Expand Down
80 changes: 49 additions & 31 deletions server/src/addon/models.rs

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am afraid this is a classical case of premature optimization. I understand that this is trying to prevent the server from doing the same data serialization over and over, but I have a few concerns with this:

  1. What is the rationale behind using Arc<[u8]> instead of Vec<u8>?
  2. By pre-serializing the blocks into a raw packet, they can no longer be included in a WorldUpdate that also contains other data (e.g. loot), which means this change is reducing computational load on the server at the cost of increased bandwidth, which is likely a much tighter bottle neck than CPU time
  3. While there is currently no use case for retaining the information of which blocks make up a given model yet, the emphasis here is on "yet". I think there is a good chance that there will be a need for it in the future

Original file line number Diff line number Diff line change
@@ -1,58 +1,76 @@
use std::ops::Div as _;
use std::collections::HashMap;
use std::path;
use std::sync::{Arc, OnceLock};

use config::{Config, ConfigError};
use protocol::utils::constants::{SIZE_BLOCK, SIZE_ZONE};
use protocol::rgb::RGB8;
use protocol::packet::WorldUpdate;
use protocol::packet::world_update::Block;
use protocol::nalgebra::{Point2, Vector3};
use protocol::packet::world_update::block::Kind::*;
use tap::Pipe;
use protocol::utils::io_extensions::WritePacket;

mod vox;
mod zox;

const BLOCKS_PER_ZONE: i32 = (SIZE_ZONE / SIZE_BLOCK) as i32;

pub struct Models {
models: Vec<(Point2<i32>, Vec<Block>)>
blocks_by_zone: HashMap<Point2<i32>, Vec<Block>>,
packets_by_zone: OnceLock<HashMap<Point2<i32>, Arc<[u8]>>>
}

impl Models {
pub fn new(config: &Config) -> Result<Self, ConfigError> {
Self {
models: config
.get::<HashMap<String, [i64; 3]>>("models")?
.into_iter()
.map(|(filename, pos)| {
let pos: Vector3<i64> = pos.into();
let zone = pos
.xy()
.div(SIZE_ZONE)
.cast::<i32>()
.into();
let mut blocks_by_zone: HashMap<Point2<i32>, Vec<Block>> = HashMap::new();

for (filename, pos) in config.get::<HashMap<String, [i64; 3]>>("models")? {
let pos: Vector3<i64> = pos.into();
let model_origin = pos
.div(SIZE_BLOCK)
.cast::<i32>();

for mut block in parse_model(&filename) {
block.position += model_origin;
blocks_by_zone
.entry(block.position.xy().map(|scalar| scalar.div_euclid(BLOCKS_PER_ZONE)))
.or_default()
.push(block);
}
}

Ok(Self {
blocks_by_zone,
packets_by_zone: OnceLock::new()
})
}

pub async fn prepare(&self) {
let mut packets_by_zone = HashMap::with_capacity(self.blocks_by_zone.len());

let mut blocks = parse_model(&filename);
let model_origin = pos
.div(SIZE_BLOCK)
.cast::<i32>();
for (zone, blocks) in &self.blocks_by_zone {
let mut packet = vec![];
packet
.write_packet(&WorldUpdate::from(blocks.clone()))
.await
.expect("failed to serialize a world update in-memory");

for block in &mut blocks {
block.position += model_origin;
}
packets_by_zone.insert(*zone, packet.into());
}

(zone, blocks)
})
.collect()
}.pipe(Ok)
assert!(
self.packets_by_zone.set(packets_by_zone).is_ok(),
"models prepared twice"
);
}

pub fn blocks_in(&self, requested_zone: Point2<i32>) -> Vec<Block> {
self.models
.iter()
.filter(|(zone, _blocks)| *zone == requested_zone)
.flat_map(|(_zone, blocks)| blocks)
.cloned()
.collect()
pub fn packet_for(&self, requested_zone: Point2<i32>) -> Option<Arc<[u8]>> {
self.packets_by_zone
.get()?
.get(&requested_zone)
.map(Arc::clone)
}
}

Expand Down
64 changes: 44 additions & 20 deletions server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ use protocol::packet::creature_update::Affiliation;
use protocol::packet::world_update::loot::GroundItem;
use protocol::packet::world_update::Sound;
use protocol::packet::world_update::sound::Kind::*;
use protocol::utils::constants::{SIZE_BLOCK, SIZE_ZONE};
use protocol::utils::constants::SIZE_BLOCK;
use protocol::utils::io_extensions::{ReadPacket, WriteArbitrary, WritePacket};
use protocol::utils::zone_of;

use crate::addon::{Addons, announce_join_leave};
use crate::addon::pvp::map_head;
Expand Down Expand Up @@ -196,30 +197,53 @@ impl Server {
pub async fn broadcast<Packet: FromServer>(&self, packet: &Packet, player_to_skip: Option<&Player>)
where Vec<u8>: WriteCwData<Packet>//todo: specialization could obsolete this
{
let mut data = vec![];

data.write_packet(packet).await.expect("failed to serialize a packet in-memory");

_ = self.players
let recipients = self.players
.read()
.await
.iter()
.filter(|player| !player_to_skip.is_some_and(|pts| ptr::eq(player.as_ref(), pts)))
.map(async |player| {
let mut writer = player
.writer
.write()
.await;

writer.write_all(&data).await?;
writer.flush().await
})
.map(Arc::clone)
.collect();

self.send_to_all(packet, recipients).await;
}

pub async fn broadcast_near<Packet: FromServer>(&self, packet: &Packet, zone: Point2<i32>)
where Vec<u8>: WriteCwData<Packet>
{
let candidates = self.players
.read()
.await
.iter()
.map(Arc::clone)
.collect::<Vec<_>>();

let mut recipients = vec![];
for player in candidates {
if player.is_ready_for(zone).await {
recipients.push(player);
}
}

self.send_to_all(packet, recipients).await;
}

async fn send_to_all<Packet: FromServer>(&self, packet: &Packet, recipients: Vec<Arc<Player>>)
where Vec<u8>: WriteCwData<Packet>
{
let mut data = vec![];

data.write_packet(packet).await.expect("failed to serialize a packet in-memory");

_ = recipients
.iter()
.map(async |player| player.send_raw(&data).await)
.pipe(join_all)
.await;
}

pub async fn add_drop(&self, item: Item, position: Point3<i64>, rotation: f32) {
let zone = position.xy().map(|scalar| (scalar / SIZE_ZONE) as i32);
let zone = zone_of(position);

let mut loot = self.loot.write().await;
let zone_loot = loot.entry(zone).or_insert(vec![]);
Expand All @@ -236,15 +260,15 @@ impl Server {
zone_loot_copy[zone_loot.len() - 1].droptime = 500;
drop(loot);

self.broadcast(&WorldUpdate {
self.broadcast_near(&WorldUpdate {
loot: HashMap::from([(zone, zone_loot_copy)]),
sounds: vec![Sound::at(position, Drop)],
..Default::default()
}, None).await;
}, zone).await;

tokio::spawn(async move {
sleep(Duration::from_millis(500)).await;
SERVER.broadcast(&WorldUpdate::from(Sound::at(position, DropItem)), None).await;
SERVER.broadcast_near(&WorldUpdate::from(Sound::at(position, DropItem)), zone).await;
});
}

Expand All @@ -267,7 +291,7 @@ impl Server {
}
drop(drops_guard);

self.broadcast(&WorldUpdate::from((zone, zone_drops_owned)), None).await;
self.broadcast_near(&WorldUpdate::from((zone, zone_drops_owned)), zone).await;

Some(removed_drop.item)
}
Expand Down
8 changes: 8 additions & 0 deletions server/src/server/handle_packet/creature_update.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::atomic::Ordering;

use protocol::packet::CreatureUpdate;
use protocol::utils::zone_of;

use crate::addon::{anti_cheat, kill_feed, pvp};
use crate::addon::fix_cutoff_animations;
Expand All @@ -26,6 +27,13 @@ impl HandlePacket<CreatureUpdate> for Server {
character.update(&packet);
let character = character.downgrade();

// client only requests the zone that its player is located at
// entering a new zone is the only sign that neighboring ones need to be revealed
let current_zone = zone_of(character.position);
if zone_of(snapshot.position) != current_zone {
self.schedule_neighborhood_reveal(source.id, current_zone);
}

if !filter(&mut packet, &snapshot, &character) {
return;
}
Expand Down
Loading