Merge branch 'christof/plugin_network' into 'master'

Load missing plugins from the server

See merge request veloren/veloren!4256
This commit is contained in:
Christof Petig 2024-03-14 20:35:41 +00:00
commit 24c511b5a4
28 changed files with 348 additions and 32 deletions

View File

@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Sand and crystal cave biome - Sand and crystal cave biome
- In commands that reference assets you can now use `#name` and press tab to cycle through assets with that name. - In commands that reference assets you can now use `#name` and press tab to cycle through assets with that name.
- Allow moving and resizing the chat with left and right mouse button respectively - Allow moving and resizing the chat with left and right mouse button respectively
- Missing plugins are requested from the server and cached locally
### Changed ### Changed

3
Cargo.lock generated
View File

@ -7114,10 +7114,12 @@ dependencies = [
"bytes", "bytes",
"futures", "futures",
"hashbrown 0.13.2", "hashbrown 0.13.2",
"hex",
"num_cpus", "num_cpus",
"rayon", "rayon",
"scopeguard", "scopeguard",
"serde", "serde",
"sha2",
"specs", "specs",
"tar", "tar",
"timer-queue", "timer-queue",
@ -7364,6 +7366,7 @@ dependencies = [
"rodio", "rodio",
"ron", "ron",
"serde", "serde",
"sha2",
"shaderc", "shaderc",
"slab", "slab",
"specs", "specs",

View File

@ -154,6 +154,8 @@ rayon = { version = "1.5" }
clap = { version = "4.2", features = ["derive"]} clap = { version = "4.2", features = ["derive"]}
async-trait = "0.1.42" async-trait = "0.1.42"
sha2 = "0.10"
hex = "0.4.3"
[patch.crates-io] [patch.crates-io]
shred = { git = "https://github.com/amethyst/shred.git", rev = "5d52c6fc390dd04c12158633e77591f6523d1f85" } shred = { git = "https://github.com/amethyst/shred.git", rev = "5d52c6fc390dd04c12158633e77591f6523d1f85" }

View File

@ -67,6 +67,7 @@ fn main() {
|provider| provider == "https://auth.veloren.net", |provider| provider == "https://auth.veloren.net",
&|_| {}, &|_| {},
|_| {}, |_| {},
Default::default(),
)) ))
.expect("Failed to create client instance"); .expect("Failed to create client instance");

View File

@ -75,6 +75,7 @@ pub fn make_client(
|_| true, |_| true,
&|_| {}, &|_| {},
|_| {}, |_| {},
Default::default(),
)) ))
.ok() .ok()
} }

View File

@ -31,7 +31,7 @@ use common::{
GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent, GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent,
MapMarkerChange, PresenceKind, UtteranceKind, MapMarkerChange, PresenceKind, UtteranceKind,
}, },
event::{EventBus, LocalEvent, UpdateCharacterMetadata}, event::{EventBus, LocalEvent, PluginHash, UpdateCharacterMetadata},
grid::Grid, grid::Grid,
link::Is, link::Is,
lod, lod,
@ -64,6 +64,8 @@ use common_net::{
}, },
sync::WorldSyncExt, sync::WorldSyncExt,
}; };
#[cfg(feature = "plugins")]
use common_state::plugin::PluginMgr;
use common_state::State; use common_state::State;
use common_systems::add_local_systems; use common_systems::add_local_systems;
use comp::BuffKind; use comp::BuffKind;
@ -82,6 +84,7 @@ use std::{
collections::{BTreeMap, VecDeque}, collections::{BTreeMap, VecDeque},
fmt::Debug, fmt::Debug,
mem, mem,
path::PathBuf,
sync::Arc, sync::Arc,
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
@ -120,6 +123,7 @@ pub enum Event {
MapMarker(comp::MapMarkerUpdate), MapMarker(comp::MapMarkerUpdate),
StartSpectate(Vec3<f32>), StartSpectate(Vec3<f32>),
SpectatePosition(Vec3<f32>), SpectatePosition(Vec3<f32>),
PluginDataReceived(Vec<u8>),
} }
#[derive(Debug)] #[derive(Debug)]
@ -326,6 +330,10 @@ pub struct Client {
dt_adjustment: f64, dt_adjustment: f64,
connected_server_constants: ServerConstants, connected_server_constants: ServerConstants,
/// Requested but not yet received plugins
missing_plugins: HashSet<PluginHash>,
/// Locally cached plugins needed by the server
local_plugins: Vec<PathBuf>,
} }
/// Holds data related to the current players characters, as well as some /// Holds data related to the current players characters, as well as some
@ -392,6 +400,7 @@ impl Client {
auth_trusted: impl FnMut(&str) -> bool, auth_trusted: impl FnMut(&str) -> bool,
init_stage_update: &(dyn Fn(ClientInitStage) + Send + Sync), init_stage_update: &(dyn Fn(ClientInitStage) + Send + Sync),
add_foreign_systems: impl Fn(&mut DispatcherBuilder) + Send + 'static, add_foreign_systems: impl Fn(&mut DispatcherBuilder) + Send + 'static,
config_dir: PathBuf,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let network = Network::new(Pid::new(), &runtime); let network = Network::new(Pid::new(), &runtime);
@ -580,6 +589,7 @@ impl Client {
server_constants, server_constants,
repair_recipe_book, repair_recipe_book,
description, description,
active_plugins,
} = loop { } = loop {
tokio::select! { tokio::select! {
// Spawn in a blocking thread (leaving the network thread free). This is mostly // Spawn in a blocking thread (leaving the network thread free). This is mostly
@ -614,6 +624,25 @@ impl Client {
add_foreign_systems(dispatch_builder); add_foreign_systems(dispatch_builder);
}, },
); );
let mut missing_plugins: Vec<PluginHash> = Vec::new();
let mut local_plugins: Vec<PathBuf> = Vec::new();
#[cfg(feature = "plugins")]
{
let already_present = state.ecs().read_resource::<PluginMgr>().plugin_list();
for hash in active_plugins.iter() {
if !already_present.contains(hash) {
// look in config_dir first (cache)
if let Ok(local_path) = common_state::plugin::find_cached(&config_dir, hash)
{
local_plugins.push(local_path);
} else {
//tracing::info!("cache not found {local_path:?}");
tracing::info!("Server requires plugin {hash:x?}");
missing_plugins.push(*hash);
}
}
}
}
// Client-only components // Client-only components
state.ecs_mut().register::<comp::Last<CharacterState>>(); state.ecs_mut().register::<comp::Last<CharacterState>>();
let entity = state.ecs_mut().apply_entity_package(entity_package); let entity = state.ecs_mut().apply_entity_package(entity_package);
@ -887,6 +916,8 @@ impl Client {
repair_recipe_book, repair_recipe_book,
max_group_size, max_group_size,
client_timeout, client_timeout,
missing_plugins,
local_plugins,
)) ))
}); });
@ -904,12 +935,18 @@ impl Client {
repair_recipe_book, repair_recipe_book,
max_group_size, max_group_size,
client_timeout, client_timeout,
missing_plugins,
local_plugins,
) = loop { ) = loop {
tokio::select! { tokio::select! {
res = &mut task => break res.expect("Client thread should not panic")?, res = &mut task => break res.expect("Client thread should not panic")?,
_ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?, _ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
} }
}; };
let missing_plugins_set = missing_plugins.iter().cloned().collect();
if !missing_plugins.is_empty() {
stream.send(ClientGeneral::RequestPlugins(missing_plugins))?;
}
ping_stream.send(PingMsg::Ping)?; ping_stream.send(PingMsg::Ping)?;
debug!("Initial sync done"); debug!("Initial sync done");
@ -990,6 +1027,8 @@ impl Client {
dt_adjustment: 1.0, dt_adjustment: 1.0,
connected_server_constants: server_constants, connected_server_constants: server_constants,
missing_plugins: missing_plugins_set,
local_plugins,
}) })
} }
@ -1123,7 +1162,8 @@ impl Client {
// Always possible // Always possible
ClientGeneral::ChatMsg(_) ClientGeneral::ChatMsg(_)
| ClientGeneral::Command(_, _) | ClientGeneral::Command(_, _)
| ClientGeneral::Terminate => &mut self.general_stream, | ClientGeneral::Terminate
| ClientGeneral::RequestPlugins(_) => &mut self.general_stream,
}; };
#[cfg(feature = "tracy")] #[cfg(feature = "tracy")]
{ {
@ -2539,6 +2579,11 @@ impl Client {
ServerGeneral::Notification(n) => { ServerGeneral::Notification(n) => {
frontend_events.push(Event::Notification(n)); frontend_events.push(Event::Notification(n));
}, },
ServerGeneral::PluginData(d) => {
let plugin_len = d.len();
tracing::info!(?plugin_len, "plugin data");
frontend_events.push(Event::PluginDataReceived(d));
},
_ => unreachable!("Not a general msg"), _ => unreachable!("Not a general msg"),
} }
Ok(()) Ok(())
@ -3182,6 +3227,20 @@ impl Client {
Ok(()) Ok(())
} }
/// another plugin data received, is this the last one
pub fn plugin_received(&mut self, hash: PluginHash) -> usize {
if !self.missing_plugins.remove(&hash) {
tracing::warn!(?hash, "received unrequested plugin");
}
self.missing_plugins.len()
}
/// number of requested plugins
pub fn num_missing_plugins(&self) -> usize { self.missing_plugins.len() }
/// extract list of locally cached plugins to load
pub fn take_local_plugins(&mut self) -> Vec<PathBuf> { std::mem::take(&mut self.local_plugins) }
} }
impl Drop for Client { impl Drop for Client {
@ -3247,6 +3306,7 @@ mod tests {
|suggestion: &str| suggestion == auth_server, |suggestion: &str| suggestion == auth_server,
&|_| {}, &|_| {},
|_| {}, |_| {},
PathBuf::default(),
)); ));
let localisation = LocalizationHandle::load_expect("en"); let localisation = LocalizationHandle::load_expect("en");

View File

@ -33,7 +33,7 @@ chrono = { workspace = true }
chrono-tz = { workspace = true } chrono-tz = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
sha2 = "0.10" sha2 = { workspace = true }
# Strum # Strum
strum = { workspace = true } strum = { workspace = true }

View File

@ -1,5 +1,8 @@
use super::{world_msg::SiteId, PingMsg}; use super::{world_msg::SiteId, PingMsg};
use common::{character::CharacterId, comp, comp::Skill, terrain::block::Block, ViewDistances}; use common::{
character::CharacterId, comp, comp::Skill, event::PluginHash, terrain::block::Block,
ViewDistances,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vek::*; use vek::*;
@ -95,6 +98,7 @@ pub enum ClientGeneral {
RequestLossyTerrainCompression { RequestLossyTerrainCompression {
lossy_terrain_compression: bool, lossy_terrain_compression: bool,
}, },
RequestPlugins(Vec<PluginHash>),
} }
impl ClientMsg { impl ClientMsg {
@ -143,6 +147,7 @@ impl ClientMsg {
| ClientGeneral::Terminate | ClientGeneral::Terminate
// LodZoneRequest is required by the char select screen // LodZoneRequest is required by the char select screen
| ClientGeneral::LodZoneRequest { .. } => true, | ClientGeneral::LodZoneRequest { .. } => true,
| ClientGeneral::RequestPlugins(_) => true,
} }
}, },
ClientMsg::Ping(_) => true, ClientMsg::Ping(_) => true,

View File

@ -7,7 +7,7 @@ use common::{
calendar::Calendar, calendar::Calendar,
character::{self, CharacterItem}, character::{self, CharacterItem},
comp::{self, body::Gender, invite::InviteKind, item::MaterialStatManifest, Content}, comp::{self, body::Gender, invite::InviteKind, item::MaterialStatManifest, Content},
event::UpdateCharacterMetadata, event::{PluginHash, UpdateCharacterMetadata},
lod, lod,
outcome::Outcome, outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook}, recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
@ -75,6 +75,7 @@ pub enum ServerInit {
ability_map: comp::item::tool::AbilityMap, ability_map: comp::item::tool::AbilityMap,
server_constants: ServerConstants, server_constants: ServerConstants,
description: ServerDescription, description: ServerDescription,
active_plugins: Vec<PluginHash>,
}, },
} }
@ -219,6 +220,8 @@ pub enum ServerGeneral {
/// Suggest the client to spectate a position. Called after client has /// Suggest the client to spectate a position. Called after client has
/// requested teleport etc. /// requested teleport etc.
SpectatePosition(Vec3<f32>), SpectatePosition(Vec3<f32>),
/// Plugin data requested from the server
PluginData(Vec<u8>),
} }
impl ServerGeneral { impl ServerGeneral {
@ -358,6 +361,7 @@ impl ServerMsg {
| ServerGeneral::Disconnect(_) | ServerGeneral::Disconnect(_)
| ServerGeneral::Notification(_) | ServerGeneral::Notification(_)
| ServerGeneral::LodZoneUpdate { .. } => true, | ServerGeneral::LodZoneUpdate { .. } => true,
ServerGeneral::PluginData(_) => true,
} }
}, },
ServerMsg::Ping(_) => true, ServerMsg::Ping(_) => true,

View File

@ -27,6 +27,8 @@ use uuid::Uuid;
use vek::*; use vek::*;
pub type SiteId = u64; pub type SiteId = u64;
/// Plugin identifier (sha256)
pub type PluginHash = [u8; 32];
pub enum LocalEvent { pub enum LocalEvent {
/// Applies upward force to entity's `Vel` /// Applies upward force to entity's `Vel`
@ -424,6 +426,10 @@ pub struct ToggleSpriteLightEvent {
pub pos: Vec3<i32>, pub pos: Vec3<i32>,
pub enable: bool, pub enable: bool,
} }
pub struct RequestPluginsEvent {
pub entity: EcsEntity,
pub plugins: Vec<PluginHash>,
}
pub struct EventBus<E> { pub struct EventBus<E> {
queue: Mutex<VecDeque<E>>, queue: Mutex<VecDeque<E>>,
@ -548,6 +554,7 @@ pub fn register_event_busses(ecs: &mut World) {
ecs.insert(EventBus::<StartTeleportingEvent>::default()); ecs.insert(EventBus::<StartTeleportingEvent>::default());
ecs.insert(EventBus::<ToggleSpriteLightEvent>::default()); ecs.insert(EventBus::<ToggleSpriteLightEvent>::default());
ecs.insert(EventBus::<TransformEvent>::default()); ecs.insert(EventBus::<TransformEvent>::default());
ecs.insert(EventBus::<RequestPluginsEvent>::default());
} }
/// Define ecs read data for event busses. And a way to convert them all to /// Define ecs read data for event busses. And a way to convert them all to

View File

@ -6,7 +6,7 @@ version = "0.10.0"
[features] [features]
simd = ["vek/platform_intrinsics"] simd = ["vek/platform_intrinsics"]
plugins = ["common-assets/plugins", "toml", "wasmtime", "wasmtime-wasi", "tar", "bincode", "serde"] plugins = ["common-assets/plugins", "toml", "wasmtime", "wasmtime-wasi", "tar", "bincode", "serde", "dep:sha2", "dep:hex"]
default = ["simd"] default = ["simd"]
@ -40,6 +40,8 @@ wasmtime-wasi = { version = "17.0.0", optional = true }
async-trait = { workspace = true } async-trait = { workspace = true }
bytes = "^1" bytes = "^1"
futures = "0.3.30" futures = "0.3.30"
sha2 = { workspace = true, optional = true }
hex = { workspace = true, optional = true }
# Tweak running code # Tweak running code
#inline_tweak = { version = "1.0.8", features = ["release_tweak"] } #inline_tweak = { version = "1.0.8", features = ["release_tweak"] }

View File

@ -3,12 +3,12 @@ pub mod memory_manager;
pub mod module; pub mod module;
use bincode::ErrorKind; use bincode::ErrorKind;
use common::{assets::ASSETS_PATH, uid::Uid}; use common::{assets::ASSETS_PATH, event::PluginHash, uid::Uid};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fs, fs,
io::Read, io::{Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use tracing::{error, info}; use tracing::{error, info};
@ -19,6 +19,8 @@ use self::{
module::PluginModule, module::PluginModule,
}; };
use sha2::Digest;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PluginData { pub struct PluginData {
name: String, name: String,
@ -26,17 +28,64 @@ pub struct PluginData {
dependencies: HashSet<String>, dependencies: HashSet<String>,
} }
fn compute_hash(data: &[u8]) -> PluginHash {
let shasum = sha2::Sha256::digest(data);
let mut shasum_iter = shasum.iter();
// a newer generic-array supports into_array ...
let shasum: PluginHash = std::array::from_fn(|_| *shasum_iter.next().unwrap());
shasum
}
fn cache_file_name(
mut base_dir: PathBuf,
hash: &PluginHash,
create_dir: bool,
) -> Result<PathBuf, std::io::Error> {
base_dir.push("server-plugins");
if create_dir {
std::fs::create_dir_all(base_dir.as_path())?;
}
let name = hex::encode(hash);
base_dir.push(name);
base_dir.set_extension("plugin.tar");
Ok(base_dir)
}
// write received plugin to disk cache
pub fn store_server_plugin(base_dir: &Path, data: Vec<u8>) -> Result<PathBuf, std::io::Error> {
let shasum = compute_hash(data.as_slice());
let result = cache_file_name(base_dir.to_path_buf(), &shasum, true)?;
let mut file = std::fs::File::create(result.as_path())?;
file.write_all(data.as_slice())?;
Ok(result)
}
pub fn find_cached(base_dir: &Path, hash: &PluginHash) -> Result<PathBuf, std::io::Error> {
let local_path = cache_file_name(base_dir.to_path_buf(), hash, false)?;
if local_path.as_path().exists() {
Ok(local_path)
} else {
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}
}
pub struct Plugin { pub struct Plugin {
data: PluginData, data: PluginData,
modules: Vec<PluginModule>, modules: Vec<PluginModule>,
#[allow(dead_code)] #[allow(dead_code)]
files: HashMap<PathBuf, Vec<u8>>, hash: PluginHash,
#[allow(dead_code)]
path: PathBuf,
#[allow(dead_code)]
data_buf: Vec<u8>,
} }
impl Plugin { impl Plugin {
pub fn from_reader<R: Read>(mut reader: R) -> Result<Self, PluginError> { pub fn from_path(path_buf: PathBuf) -> Result<Self, PluginError> {
let mut reader = fs::File::open(path_buf.as_path()).map_err(PluginError::Io)?;
let mut buf = Vec::new(); let mut buf = Vec::new();
reader.read_to_end(&mut buf).map_err(PluginError::Io)?; reader.read_to_end(&mut buf).map_err(PluginError::Io)?;
let shasum = compute_hash(buf.as_slice());
let mut files = tar::Archive::new(&*buf) let mut files = tar::Archive::new(&*buf)
.entries() .entries()
@ -73,10 +122,14 @@ impl Plugin {
}) })
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
let data_buf = fs::read(&path_buf).map_err(PluginError::Io)?;
Ok(Plugin { Ok(Plugin {
data, data,
modules, modules,
files, hash: shasum,
path: path_buf,
data_buf,
}) })
} }
@ -111,6 +164,12 @@ impl Plugin {
}); });
result result
} }
/// get the path to the plugin file
pub fn path(&self) -> &Path { self.path.as_path() }
/// Get the data of this plugin
pub fn data_buf(&self) -> &[u8] { &self.data_buf }
} }
#[derive(Default)] #[derive(Default)]
@ -140,14 +199,12 @@ impl PluginMgr {
.unwrap_or(false) .unwrap_or(false)
{ {
info!("Loading plugin at {:?}", entry.path()); info!("Loading plugin at {:?}", entry.path());
Plugin::from_reader(fs::File::open(entry.path()).map_err(PluginError::Io)?).map( Plugin::from_path(entry.path()).map(|plugin| {
|plugin| {
if let Err(e) = common::assets::register_tar(entry.path()) { if let Err(e) = common::assets::register_tar(entry.path()) {
error!("Plugin {:?} tar error {e:?}", entry.path()); error!("Plugin {:?} tar error {e:?}", entry.path());
} }
Some(plugin) Some(plugin)
}, })
)
} else { } else {
Ok(None) Ok(None)
} }
@ -169,6 +226,37 @@ impl PluginMgr {
Ok(Self { plugins }) Ok(Self { plugins })
} }
/// Add a plugin received from the server
pub fn load_server_plugin(&mut self, path: PathBuf) -> Result<PluginHash, PluginError> {
Plugin::from_path(path.clone()).map(|plugin| {
if let Err(e) = common::assets::register_tar(path.clone()) {
error!("Plugin {:?} tar error {e:?}", path.as_path());
}
let hash = plugin.hash;
self.plugins.push(plugin);
hash
})
}
pub fn cache_server_plugin(
&mut self,
base_dir: &Path,
data: Vec<u8>,
) -> Result<PluginHash, PluginError> {
let path = store_server_plugin(base_dir, data).map_err(PluginError::Io)?;
self.load_server_plugin(path)
}
/// list all registered plugins
pub fn plugin_list(&self) -> Vec<PluginHash> {
self.plugins.iter().map(|plugin| plugin.hash).collect()
}
/// retrieve a specific plugin
pub fn find(&self, hash: &PluginHash) -> Option<&Plugin> {
self.plugins.iter().find(|plugin| &plugin.hash == hash)
}
pub fn load_event( pub fn load_event(
&mut self, &mut self,
ecs: &EcsWorld, ecs: &EcsWorld,

View File

@ -213,7 +213,8 @@ impl Client {
| ServerGeneral::CreateEntity(_) | ServerGeneral::CreateEntity(_)
| ServerGeneral::DeleteEntity(_) | ServerGeneral::DeleteEntity(_)
| ServerGeneral::Disconnect(_) | ServerGeneral::Disconnect(_)
| ServerGeneral::Notification(_) => { | ServerGeneral::Notification(_)
| ServerGeneral::PluginData(_) => {
PreparedMsg::new(3, &g, &self.general_stream_params) PreparedMsg::new(3, &g, &self.general_stream_params)
}, },
} }

View File

@ -1,6 +1,8 @@
use crate::client::Client; use crate::client::Client;
use common::event::RequestSiteInfoEvent; use common::event::RequestSiteInfoEvent;
use common_net::msg::{world_msg::EconomyInfo, ServerGeneral}; use common_net::msg::{world_msg::EconomyInfo, ServerGeneral};
#[cfg(feature = "plugins")]
use common_state::plugin::PluginMgr;
use specs::{DispatcherBuilder, ReadExpect, ReadStorage}; use specs::{DispatcherBuilder, ReadExpect, ReadStorage};
use std::collections::HashMap; use std::collections::HashMap;
use world::IndexOwned; use world::IndexOwned;
@ -9,6 +11,8 @@ use super::{event_dispatch, ServerEvent};
pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) { pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
event_dispatch::<RequestSiteInfoEvent>(builder); event_dispatch::<RequestSiteInfoEvent>(builder);
#[cfg(feature = "plugins")]
event_dispatch::<common::event::RequestPluginsEvent>(builder);
} }
#[cfg(not(feature = "worldgen"))] #[cfg(not(feature = "worldgen"))]
@ -64,3 +68,33 @@ impl ServerEvent for RequestSiteInfoEvent {
} }
} }
} }
/// Send missing plugins to the client
#[cfg(feature = "plugins")]
impl ServerEvent for common::event::RequestPluginsEvent {
type SystemData<'a> = (ReadExpect<'a, PluginMgr>, ReadStorage<'a, Client>);
fn handle(
events: impl ExactSizeIterator<Item = Self>,
(plugin_mgr, clients): Self::SystemData<'_>,
) {
for mut ev in events {
let Some(client) = clients.get(ev.entity) else {
continue;
};
for hash in ev.plugins.drain(..) {
if let Some(plugin) = plugin_mgr.find(&hash) {
let buf = Vec::from(plugin.data_buf());
// TODO: @perf We could possibly make this more performant by caching prepared
// messages for each plugin.
client
.send(ServerGeneral::PluginData(buf))
.unwrap_or_else(|e| {
tracing::warn!("Error {e} sending plugin {hash:?} to client")
});
}
}
}
}
}

View File

@ -117,12 +117,15 @@ use std::{
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
#[cfg(not(feature = "worldgen"))]
use test_world::{IndexOwned, World};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tracing::{debug, error, info, trace, warn}; use tracing::{debug, error, info, trace, warn};
use vek::*; use vek::*;
pub use world::{civ::WorldCivStage, sim::WorldSimStage, WorldGenerateStage}; pub use world::{civ::WorldCivStage, sim::WorldSimStage, WorldGenerateStage};
#[cfg(not(feature = "worldgen"))]
use {
common_net::msg::WorldMapMsg,
test_world::{IndexOwned, World},
};
use crate::{ use crate::{
persistence::{DatabaseSettings, SqlLogMode}, persistence::{DatabaseSettings, SqlLogMode},
@ -308,6 +311,7 @@ impl Server {
horizons: [(vec![0], vec![0]), (vec![0], vec![0])], horizons: [(vec![0], vec![0]), (vec![0], vec![0])],
alt: Grid::new(Vec2::new(1, 1), 1), alt: Grid::new(Vec2::new(1, 1), 1),
sites: Vec::new(), sites: Vec::new(),
possible_starting_sites: Vec::new(),
pois: Vec::new(), pois: Vec::new(),
default_chunk: Arc::new(world.generate_oob_chunk()), default_chunk: Arc::new(world.generate_oob_chunk()),
}; };
@ -318,7 +322,10 @@ impl Server {
let mut state = State::server( let mut state = State::server(
Arc::clone(&pools), Arc::clone(&pools),
#[cfg(feature = "worldgen")]
world.sim().map_size_lg(), world.sim().map_size_lg(),
#[cfg(not(feature = "worldgen"))]
common::terrain::map::MapSizeLg::new(Vec2::one()).unwrap(),
Arc::clone(&map.default_chunk), Arc::clone(&map.default_chunk),
|dispatcher_builder| { |dispatcher_builder| {
add_local_systems(dispatcher_builder); add_local_systems(dispatcher_builder);

View File

@ -37,7 +37,9 @@ impl Lod {
} }
#[cfg(not(feature = "worldgen"))] #[cfg(not(feature = "worldgen"))]
pub fn from_world(world: &World, index: IndexRef) -> Self { Self::default() } pub fn from_world(world: &World, index: IndexRef, _threadpool: &rayon::ThreadPool) -> Self {
Self::default()
}
pub fn zone(&self, zone_pos: Vec2<i32>) -> &lod::Zone { pub fn zone(&self, zone_pos: Vec2<i32>) -> &lod::Zone {
self.zones.get(&zone_pos).unwrap_or(&EMPTY_ZONE) self.zones.get(&zone_pos).unwrap_or(&EMPTY_ZONE)

View File

@ -182,6 +182,7 @@ impl Sys {
offhand.clone(), offhand.clone(),
body, body,
character_updater, character_updater,
#[cfg(feature = "worldgen")]
start_site.and_then(|site_idx| { start_site.and_then(|site_idx| {
// TODO: This corresponds to the ID generation logic in // TODO: This corresponds to the ID generation logic in
// `world/src/lib.rs`. Really, we should have // `world/src/lib.rs`. Really, we should have
@ -214,6 +215,8 @@ impl Sys {
) )
}) })
}), }),
#[cfg(not(feature = "worldgen"))]
None,
) { ) {
debug!( debug!(
?error, ?error,

View File

@ -17,7 +17,7 @@ event_emitters! {
command: event::CommandEvent, command: event::CommandEvent,
client_disconnect: event::ClientDisconnectEvent, client_disconnect: event::ClientDisconnectEvent,
chat: event::ChatEvent, chat: event::ChatEvent,
plugins: event::RequestPluginsEvent,
} }
} }
@ -72,6 +72,10 @@ impl Sys {
common::comp::DisconnectReason::ClientRequested, common::comp::DisconnectReason::ClientRequested,
)); ));
}, },
ClientGeneral::RequestPlugins(plugins) => {
tracing::info!("Plugin request {plugins:x?}, {}", player.is_some());
emitters.emit(event::RequestPluginsEvent { entity, plugins });
},
_ => { _ => {
debug!("Kicking possible misbehaving client due to invalid message request"); debug!("Kicking possible misbehaving client due to invalid message request");
emitters.emit(event::ClientDisconnectEvent( emitters.emit(event::ClientDisconnectEvent(

View File

@ -259,7 +259,8 @@ impl Sys {
| ClientGeneral::LodZoneRequest { .. } | ClientGeneral::LodZoneRequest { .. }
| ClientGeneral::ChatMsg(_) | ClientGeneral::ChatMsg(_)
| ClientGeneral::Command(..) | ClientGeneral::Command(..)
| ClientGeneral::Terminate => { | ClientGeneral::Terminate
| ClientGeneral::RequestPlugins(_) => {
debug!("Kicking possibly misbehaving client due to invalid client in game request"); debug!("Kicking possibly misbehaving client due to invalid client in game request");
emitters.emit(event::ClientDisconnectEvent( emitters.emit(event::ClientDisconnectEvent(
entity, entity,

View File

@ -6,12 +6,12 @@ use crate::{
EditableSettings, Settings, EditableSettings, Settings,
}; };
use common::{ use common::{
comp::{self, Admin, Health, Player, Stats}, comp::{self, Admin, Player, Stats},
event::{ClientDisconnectEvent, EventBus, MakeAdminEvent}, event::{ClientDisconnectEvent, EventBus, MakeAdminEvent},
recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book}, recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book},
resources::TimeOfDay, resources::TimeOfDay,
shared_server_config::ServerConstants, shared_server_config::ServerConstants,
uid::{IdMaps, Uid}, uid::Uid,
}; };
use common_base::prof_span; use common_base::prof_span;
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
@ -52,9 +52,8 @@ pub struct ReadData<'a> {
ability_map: ReadExpect<'a, comp::item::tool::AbilityMap>, ability_map: ReadExpect<'a, comp::item::tool::AbilityMap>,
map: ReadExpect<'a, WorldMapMsg>, map: ReadExpect<'a, WorldMapMsg>,
trackers: TrackedStorages<'a>, trackers: TrackedStorages<'a>,
_healths: ReadStorage<'a, Health>, // used by plugin feature #[allow(dead_code)]
_plugin_mgr: ReadPlugin<'a>, // used by plugin feature plugin_mgr: ReadPlugin<'a>, // only used by plugins feature
_id_maps: Read<'a, IdMaps>, // used by plugin feature
} }
/// This system will handle new messages from clients /// This system will handle new messages from clients
@ -330,6 +329,10 @@ impl<'a> System<'a> for Sys {
// Tell the client its request was successful. // Tell the client its request was successful.
client.send(Ok(()))?; client.send(Ok(()))?;
#[cfg(feature = "plugins")]
let active_plugins = read_data.plugin_mgr.plugin_list();
#[cfg(not(feature = "plugins"))]
let active_plugins = Vec::default();
let server_descriptions = &read_data.editable_settings.server_description; let server_descriptions = &read_data.editable_settings.server_description;
let description = ServerDescription { let description = ServerDescription {
@ -358,6 +361,7 @@ impl<'a> System<'a> for Sys {
day_cycle_coefficient: read_data.settings.day_cycle_coefficient() day_cycle_coefficient: read_data.settings.day_cycle_coefficient()
}, },
description, description,
active_plugins,
})?; })?;
debug!("Done initial sync with client."); debug!("Done initial sync with client.");

View File

@ -717,11 +717,14 @@ where
let world_aabr_in_chunks = Aabr { let world_aabr_in_chunks = Aabr {
min: Vec2::zero(), min: Vec2::zero(),
// NOTE: Cast is correct because chunk coordinates must fit in an i32 (actually, i16). // NOTE: Cast is correct because chunk coordinates must fit in an i32 (actually, i16).
#[cfg(feature = "worldgen")]
max: world max: world
.sim() .sim()
.get_size() .get_size()
.map(|x| x.saturating_sub(1)) .map(|x| x.saturating_sub(1))
.as_::<i32>(), .as_::<i32>(),
#[cfg(not(feature = "worldgen"))]
max: Vec2::one(),
}; };
let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) = let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =

View File

@ -45,6 +45,7 @@ impl<'a> System<'a> for Sys {
): Self::SystemData, ): Self::SystemData,
) { ) {
let max_view_distance = server_settings.max_view_distance.unwrap_or(u32::MAX); let max_view_distance = server_settings.max_view_distance.unwrap_or(u32::MAX);
#[cfg(feature = "worldgen")]
let (presences_position_entities, _) = super::terrain::prepare_player_presences( let (presences_position_entities, _) = super::terrain::prepare_player_presences(
&world, &world,
max_view_distance, max_view_distance,
@ -53,6 +54,8 @@ impl<'a> System<'a> for Sys {
&presences, &presences,
&clients, &clients,
); );
#[cfg(not(feature = "worldgen"))]
let presences_position_entities: Vec<((vek::Vec2<i16>, i32), specs::Entity)> = Vec::new();
let real_max_view_distance = let real_max_view_distance =
super::terrain::convert_to_loaded_vd(u32::MAX, max_view_distance); super::terrain::convert_to_loaded_vd(u32::MAX, max_view_distance);

View File

@ -2,11 +2,13 @@ use common::{
calendar::Calendar, calendar::Calendar,
generation::{ChunkSupplement, EntityInfo}, generation::{ChunkSupplement, EntityInfo},
resources::TimeOfDay, resources::TimeOfDay,
rtsim::ChunkResource,
terrain::{ terrain::{
Block, BlockKind, MapSizeLg, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, Block, BlockKind, MapSizeLg, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize,
}, },
vol::{ReadVol, RectVolSize, WriteVol}, vol::{ReadVol, RectVolSize, WriteVol},
}; };
use enum_map::EnumMap;
use rand::{prelude::*, rngs::SmallRng}; use rand::{prelude::*, rngs::SmallRng};
use std::time::Duration; use std::time::Duration;
use vek::*; use vek::*;
@ -48,6 +50,7 @@ impl World {
&self, &self,
_index: IndexRef, _index: IndexRef,
chunk_pos: Vec2<i32>, chunk_pos: Vec2<i32>,
_rtsim_resources: Option<EnumMap<ChunkResource, f32>>,
_should_continue: impl FnMut() -> bool, _should_continue: impl FnMut() -> bool,
_time: Option<(TimeOfDay, Calendar)>, _time: Option<(TimeOfDay, Calendar)>,
) -> Result<(TerrainChunk, ChunkSupplement), ()> { ) -> Result<(TerrainChunk, ChunkSupplement), ()> {
@ -71,4 +74,6 @@ impl World {
supplement, supplement,
)) ))
} }
pub fn get_location_name(&self, _index: IndexRef, _wpos2d: Vec2<i32>) -> Option<String> { None }
} }

View File

@ -127,6 +127,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
num_cpus = "1.0" num_cpus = "1.0"
inline_tweak = { workspace = true } inline_tweak = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
sha2 = { workspace = true }
# Discord RPC # Discord RPC
discord-sdk = { version = "0.3.0", optional = true } discord-sdk = { version = "0.3.0", optional = true }

View File

@ -12,6 +12,8 @@ use crate::{
use client::{self, Client}; use client::{self, Client};
use common::{comp, event::UpdateCharacterMetadata, resources::DeltaTime}; use common::{comp, event::UpdateCharacterMetadata, resources::DeltaTime};
use common_base::span; use common_base::span;
#[cfg(feature = "plugins")]
use common_state::plugin::PluginMgr;
use specs::WorldExt; use specs::WorldExt;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use tracing::error; use tracing::error;
@ -69,7 +71,9 @@ impl CharSelectionState {
impl PlayState for CharSelectionState { impl PlayState for CharSelectionState {
fn enter(&mut self, global_state: &mut GlobalState, _: Direction) { fn enter(&mut self, global_state: &mut GlobalState, _: Direction) {
// Load the player's character list // Load the player's character list
if self.client.borrow().num_missing_plugins() == 0 {
self.client.borrow_mut().load_character_list(); self.client.borrow_mut().load_character_list();
}
// Updated localization in case the selected language was changed // Updated localization in case the selected language was changed
self.char_selection_ui.update_language(global_state.i18n); self.char_selection_ui.update_language(global_state.i18n);
@ -274,6 +278,27 @@ impl PlayState for CharSelectionState {
Rc::clone(&self.client), Rc::clone(&self.client),
))); )));
}, },
client::Event::PluginDataReceived(data) => {
#[cfg(feature = "plugins")]
{
tracing::info!("plugin data {}", data.len());
let mut client = self.client.borrow_mut();
let hash = client
.state()
.ecs()
.write_resource::<PluginMgr>()
.cache_server_plugin(&global_state.config_dir, data);
match hash {
Ok(hash) => {
if client.plugin_received(hash) == 0 {
// now load characters (plugins might contain items)
client.load_character_list();
}
},
Err(e) => tracing::error!(?e, "cache_server_plugin"),
}
}
},
// TODO: See if we should handle StartSpectate here instead. // TODO: See if we should handle StartSpectate here instead.
_ => {}, _ => {},
} }

View File

@ -5,6 +5,7 @@ use client::{
}; };
use crossbeam_channel::{unbounded, Receiver, Sender, TryRecvError}; use crossbeam_channel::{unbounded, Receiver, Sender, TryRecvError};
use std::{ use std::{
path::Path,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Arc,
@ -50,6 +51,7 @@ impl ClientInit {
password: String, password: String,
runtime: Arc<runtime::Runtime>, runtime: Arc<runtime::Runtime>,
locale: Option<String>, locale: Option<String>,
config_dir: &Path,
) -> Self { ) -> Self {
let (tx, rx) = unbounded(); let (tx, rx) = unbounded();
let (trust_tx, trust_rx) = unbounded(); let (trust_tx, trust_rx) = unbounded();
@ -58,6 +60,7 @@ impl ClientInit {
let cancel2 = Arc::clone(&cancel); let cancel2 = Arc::clone(&cancel);
let runtime2 = Arc::clone(&runtime); let runtime2 = Arc::clone(&runtime);
let config_dir = config_dir.to_path_buf();
runtime.spawn(async move { runtime.spawn(async move {
let trust_fn = |auth_server: &str| { let trust_fn = |auth_server: &str| {
@ -89,6 +92,7 @@ impl ClientInit {
let _ = init_stage_tx.send(stage); let _ = init_stage_tx.send(stage);
}, },
crate::ecs::sys::add_local_systems, crate::ecs::sys::add_local_systems,
config_dir.clone(),
) )
.await .await
{ {

View File

@ -18,10 +18,13 @@ use client::{
use client_init::{ClientInit, Error as InitError, Msg as InitMsg}; use client_init::{ClientInit, Error as InitError, Msg as InitMsg};
use common::comp; use common::comp;
use common_base::span; use common_base::span;
#[cfg(feature = "plugins")]
use common_state::plugin::PluginMgr;
use i18n::LocalizationHandle; use i18n::LocalizationHandle;
#[cfg(feature = "singleplayer")] #[cfg(feature = "singleplayer")]
use server::ServerInitStage; use server::ServerInitStage;
use std::sync::Arc; use specs::WorldExt;
use std::{path::Path, sync::Arc};
use tokio::runtime; use tokio::runtime;
use tracing::error; use tracing::error;
use ui::{Event as MainMenuEvent, MainMenuUi}; use ui::{Event as MainMenuEvent, MainMenuUi};
@ -129,6 +132,7 @@ impl PlayState for MainMenuState {
global_state.settings.language.selected_language.clone(), global_state.settings.language.selected_language.clone(),
), ),
&global_state.i18n, &global_state.i18n,
&global_state.config_dir,
); );
}, },
Ok(Err(e)) => { Ok(Err(e)) => {
@ -216,6 +220,18 @@ impl PlayState for MainMenuState {
// Poll client creation. // Poll client creation.
match self.init.client().and_then(|init| init.poll()) { match self.init.client().and_then(|init| init.poll()) {
Some(InitMsg::Done(Ok(mut client))) => { Some(InitMsg::Done(Ok(mut client))) => {
// load local plugins needed by the server
#[cfg(feature = "plugins")]
for path in client.take_local_plugins().drain(..) {
if let Err(e) = client
.state_mut()
.ecs_mut()
.write_resource::<PluginMgr>()
.load_server_plugin(path)
{
tracing::error!(?e, "load local plugin");
}
}
// Register voxygen components / resources // Register voxygen components / resources
crate::ecs::init(client.state_mut().ecs_mut()); crate::ecs::init(client.state_mut().ecs_mut());
self.init = InitState::Pipeline(Box::new(client)); self.init = InitState::Pipeline(Box::new(client));
@ -267,6 +283,29 @@ impl PlayState for MainMenuState {
); );
self.init = InitState::None; self.init = InitState::None;
}, },
client::Event::PluginDataReceived(data) => {
#[cfg(feature = "plugins")]
{
tracing::info!("plugin data {}", data.len());
if let InitState::Pipeline(client) = &mut self.init {
let hash = client
.state()
.ecs()
.write_resource::<PluginMgr>()
.cache_server_plugin(&global_state.config_dir, data);
match hash {
Ok(hash) => {
if client.plugin_received(hash) == 0 {
// now load characters (plugins might contain
// items)
client.load_character_list();
}
},
Err(e) => tracing::error!(?e, "cache_server_plugin"),
}
}
}
},
_ => {}, _ => {},
} }
} }
@ -378,6 +417,7 @@ impl PlayState for MainMenuState {
.send_to_server .send_to_server
.then_some(global_state.settings.language.selected_language.clone()), .then_some(global_state.settings.language.selected_language.clone()),
&global_state.i18n, &global_state.i18n,
&global_state.config_dir,
); );
}, },
MainMenuEvent::CancelLoginAttempt => { MainMenuEvent::CancelLoginAttempt => {
@ -609,6 +649,7 @@ fn attempt_login(
runtime: &Arc<runtime::Runtime>, runtime: &Arc<runtime::Runtime>,
locale: Option<String>, locale: Option<String>,
localized_strings: &LocalizationHandle, localized_strings: &LocalizationHandle,
config_dir: &Path,
) { ) {
let localization = localized_strings.read(); let localization = localized_strings.read();
if let Err(err) = comp::Player::alias_validate(&username) { if let Err(err) = comp::Player::alias_validate(&username) {
@ -641,6 +682,7 @@ fn attempt_login(
password, password,
Arc::clone(runtime), Arc::clone(runtime),
locale, locale,
config_dir,
)); ));
} }
} }

View File

@ -447,6 +447,9 @@ impl SessionState {
client::Event::SpectatePosition(pos) => { client::Event::SpectatePosition(pos) => {
self.scene.camera_mut().force_focus_pos(pos); self.scene.camera_mut().force_focus_pos(pos);
}, },
client::Event::PluginDataReceived(data) => {
tracing::warn!("Received plugin data at wrong time {}", data.len());
},
} }
} }