diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5d302f47..abfa5e9cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. - Allow moving and resizing the chat with left and right mouse button respectively +- Missing plugins are requested from the server and cached locally ### Changed diff --git a/Cargo.lock b/Cargo.lock index de6b420bc8..0df615346e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7114,10 +7114,12 @@ dependencies = [ "bytes", "futures", "hashbrown 0.13.2", + "hex", "num_cpus", "rayon", "scopeguard", "serde", + "sha2", "specs", "tar", "timer-queue", @@ -7364,6 +7366,7 @@ dependencies = [ "rodio", "ron", "serde", + "sha2", "shaderc", "slab", "specs", diff --git a/Cargo.toml b/Cargo.toml index d13e9ccbbb..c8d02902e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,6 +154,8 @@ rayon = { version = "1.5" } clap = { version = "4.2", features = ["derive"]} async-trait = "0.1.42" +sha2 = "0.10" +hex = "0.4.3" [patch.crates-io] shred = { git = "https://github.com/amethyst/shred.git", rev = "5d52c6fc390dd04c12158633e77591f6523d1f85" } diff --git a/client/examples/chat-cli/main.rs b/client/examples/chat-cli/main.rs index 841b0ed49a..381c331e89 100644 --- a/client/examples/chat-cli/main.rs +++ b/client/examples/chat-cli/main.rs @@ -67,6 +67,7 @@ fn main() { |provider| provider == "https://auth.veloren.net", &|_| {}, |_| {}, + Default::default(), )) .expect("Failed to create client instance"); diff --git a/client/src/bin/bot/main.rs b/client/src/bin/bot/main.rs index beb6c41642..f54a927147 100644 --- a/client/src/bin/bot/main.rs +++ b/client/src/bin/bot/main.rs @@ -75,6 +75,7 @@ pub fn make_client( |_| true, &|_| {}, |_| {}, + Default::default(), )) .ok() } diff --git a/client/src/lib.rs b/client/src/lib.rs index 87f3bfaed1..c08cb3a0bc 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -31,7 +31,7 @@ use common::{ GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent, MapMarkerChange, PresenceKind, UtteranceKind, }, - event::{EventBus, LocalEvent, UpdateCharacterMetadata}, + event::{EventBus, LocalEvent, PluginHash, UpdateCharacterMetadata}, grid::Grid, link::Is, lod, @@ -64,6 +64,8 @@ use common_net::{ }, sync::WorldSyncExt, }; +#[cfg(feature = "plugins")] +use common_state::plugin::PluginMgr; use common_state::State; use common_systems::add_local_systems; use comp::BuffKind; @@ -82,6 +84,7 @@ use std::{ collections::{BTreeMap, VecDeque}, fmt::Debug, mem, + path::PathBuf, sync::Arc, time::{Duration, Instant, SystemTime}, }; @@ -120,6 +123,7 @@ pub enum Event { MapMarker(comp::MapMarkerUpdate), StartSpectate(Vec3), SpectatePosition(Vec3), + PluginDataReceived(Vec), } #[derive(Debug)] @@ -326,6 +330,10 @@ pub struct Client { dt_adjustment: f64, connected_server_constants: ServerConstants, + /// Requested but not yet received plugins + missing_plugins: HashSet, + /// Locally cached plugins needed by the server + local_plugins: Vec, } /// Holds data related to the current players characters, as well as some @@ -392,6 +400,7 @@ impl Client { auth_trusted: impl FnMut(&str) -> bool, init_stage_update: &(dyn Fn(ClientInitStage) + Send + Sync), add_foreign_systems: impl Fn(&mut DispatcherBuilder) + Send + 'static, + config_dir: PathBuf, ) -> Result { let network = Network::new(Pid::new(), &runtime); @@ -580,6 +589,7 @@ impl Client { server_constants, repair_recipe_book, description, + active_plugins, } = loop { tokio::select! { // Spawn in a blocking thread (leaving the network thread free). This is mostly @@ -614,6 +624,25 @@ impl Client { add_foreign_systems(dispatch_builder); }, ); + let mut missing_plugins: Vec = Vec::new(); + let mut local_plugins: Vec = Vec::new(); + #[cfg(feature = "plugins")] + { + let already_present = state.ecs().read_resource::().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 state.ecs_mut().register::>(); let entity = state.ecs_mut().apply_entity_package(entity_package); @@ -887,6 +916,8 @@ impl Client { repair_recipe_book, max_group_size, client_timeout, + missing_plugins, + local_plugins, )) }); @@ -904,12 +935,18 @@ impl Client { repair_recipe_book, max_group_size, client_timeout, + missing_plugins, + local_plugins, ) = loop { tokio::select! { res = &mut task => break res.expect("Client thread should not panic")?, _ = 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)?; debug!("Initial sync done"); @@ -990,6 +1027,8 @@ impl Client { dt_adjustment: 1.0, connected_server_constants: server_constants, + missing_plugins: missing_plugins_set, + local_plugins, }) } @@ -1123,7 +1162,8 @@ impl Client { // Always possible ClientGeneral::ChatMsg(_) | ClientGeneral::Command(_, _) - | ClientGeneral::Terminate => &mut self.general_stream, + | ClientGeneral::Terminate + | ClientGeneral::RequestPlugins(_) => &mut self.general_stream, }; #[cfg(feature = "tracy")] { @@ -2539,6 +2579,11 @@ impl Client { ServerGeneral::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"), } Ok(()) @@ -3182,6 +3227,20 @@ impl Client { 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 { std::mem::take(&mut self.local_plugins) } } impl Drop for Client { @@ -3247,6 +3306,7 @@ mod tests { |suggestion: &str| suggestion == auth_server, &|_| {}, |_| {}, + PathBuf::default(), )); let localisation = LocalizationHandle::load_expect("en"); diff --git a/common/Cargo.toml b/common/Cargo.toml index 607103a468..3e55266347 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -33,7 +33,7 @@ chrono = { workspace = true } chrono-tz = { workspace = true } itertools = { workspace = true } serde_json = { workspace = true } -sha2 = "0.10" +sha2 = { workspace = true } # Strum strum = { workspace = true } diff --git a/common/net/src/msg/client.rs b/common/net/src/msg/client.rs index fc02f6b2a2..c4081df13b 100644 --- a/common/net/src/msg/client.rs +++ b/common/net/src/msg/client.rs @@ -1,5 +1,8 @@ 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 vek::*; @@ -95,6 +98,7 @@ pub enum ClientGeneral { RequestLossyTerrainCompression { lossy_terrain_compression: bool, }, + RequestPlugins(Vec), } impl ClientMsg { @@ -143,6 +147,7 @@ impl ClientMsg { | ClientGeneral::Terminate // LodZoneRequest is required by the char select screen | ClientGeneral::LodZoneRequest { .. } => true, + | ClientGeneral::RequestPlugins(_) => true, } }, ClientMsg::Ping(_) => true, diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 211891842c..e6832a8884 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -7,7 +7,7 @@ use common::{ calendar::Calendar, character::{self, CharacterItem}, comp::{self, body::Gender, invite::InviteKind, item::MaterialStatManifest, Content}, - event::UpdateCharacterMetadata, + event::{PluginHash, UpdateCharacterMetadata}, lod, outcome::Outcome, recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook}, @@ -75,6 +75,7 @@ pub enum ServerInit { ability_map: comp::item::tool::AbilityMap, server_constants: ServerConstants, description: ServerDescription, + active_plugins: Vec, }, } @@ -219,6 +220,8 @@ pub enum ServerGeneral { /// Suggest the client to spectate a position. Called after client has /// requested teleport etc. SpectatePosition(Vec3), + /// Plugin data requested from the server + PluginData(Vec), } impl ServerGeneral { @@ -358,6 +361,7 @@ impl ServerMsg { | ServerGeneral::Disconnect(_) | ServerGeneral::Notification(_) | ServerGeneral::LodZoneUpdate { .. } => true, + ServerGeneral::PluginData(_) => true, } }, ServerMsg::Ping(_) => true, diff --git a/common/src/event.rs b/common/src/event.rs index 35a24c364c..3e7c1213e0 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -27,6 +27,8 @@ use uuid::Uuid; use vek::*; pub type SiteId = u64; +/// Plugin identifier (sha256) +pub type PluginHash = [u8; 32]; pub enum LocalEvent { /// Applies upward force to entity's `Vel` @@ -424,6 +426,10 @@ pub struct ToggleSpriteLightEvent { pub pos: Vec3, pub enable: bool, } +pub struct RequestPluginsEvent { + pub entity: EcsEntity, + pub plugins: Vec, +} pub struct EventBus { queue: Mutex>, @@ -548,6 +554,7 @@ pub fn register_event_busses(ecs: &mut World) { ecs.insert(EventBus::::default()); ecs.insert(EventBus::::default()); ecs.insert(EventBus::::default()); + ecs.insert(EventBus::::default()); } /// Define ecs read data for event busses. And a way to convert them all to diff --git a/common/state/Cargo.toml b/common/state/Cargo.toml index e267f50639..a510ea1328 100644 --- a/common/state/Cargo.toml +++ b/common/state/Cargo.toml @@ -6,7 +6,7 @@ version = "0.10.0" [features] 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"] @@ -40,6 +40,8 @@ wasmtime-wasi = { version = "17.0.0", optional = true } async-trait = { workspace = true } bytes = "^1" futures = "0.3.30" +sha2 = { workspace = true, optional = true } +hex = { workspace = true, optional = true } # Tweak running code #inline_tweak = { version = "1.0.8", features = ["release_tweak"] } diff --git a/common/state/src/plugin/mod.rs b/common/state/src/plugin/mod.rs index c5bec966d3..d4ce8dcf0c 100644 --- a/common/state/src/plugin/mod.rs +++ b/common/state/src/plugin/mod.rs @@ -3,12 +3,12 @@ pub mod memory_manager; pub mod module; use bincode::ErrorKind; -use common::{assets::ASSETS_PATH, uid::Uid}; +use common::{assets::ASSETS_PATH, event::PluginHash, uid::Uid}; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fs, - io::Read, + io::{Read, Write}, path::{Path, PathBuf}, }; use tracing::{error, info}; @@ -19,6 +19,8 @@ use self::{ module::PluginModule, }; +use sha2::Digest; + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PluginData { name: String, @@ -26,17 +28,64 @@ pub struct PluginData { dependencies: HashSet, } +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 { + 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) -> Result { + 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 { + 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 { data: PluginData, modules: Vec, #[allow(dead_code)] - files: HashMap>, + hash: PluginHash, + #[allow(dead_code)] + path: PathBuf, + #[allow(dead_code)] + data_buf: Vec, } impl Plugin { - pub fn from_reader(mut reader: R) -> Result { + pub fn from_path(path_buf: PathBuf) -> Result { + let mut reader = fs::File::open(path_buf.as_path()).map_err(PluginError::Io)?; let mut buf = Vec::new(); reader.read_to_end(&mut buf).map_err(PluginError::Io)?; + let shasum = compute_hash(buf.as_slice()); let mut files = tar::Archive::new(&*buf) .entries() @@ -73,10 +122,14 @@ impl Plugin { }) .collect::>()?; + let data_buf = fs::read(&path_buf).map_err(PluginError::Io)?; + Ok(Plugin { data, modules, - files, + hash: shasum, + path: path_buf, + data_buf, }) } @@ -111,6 +164,12 @@ impl Plugin { }); 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)] @@ -140,14 +199,12 @@ impl PluginMgr { .unwrap_or(false) { info!("Loading plugin at {:?}", entry.path()); - Plugin::from_reader(fs::File::open(entry.path()).map_err(PluginError::Io)?).map( - |plugin| { - if let Err(e) = common::assets::register_tar(entry.path()) { - error!("Plugin {:?} tar error {e:?}", entry.path()); - } - Some(plugin) - }, - ) + Plugin::from_path(entry.path()).map(|plugin| { + if let Err(e) = common::assets::register_tar(entry.path()) { + error!("Plugin {:?} tar error {e:?}", entry.path()); + } + Some(plugin) + }) } else { Ok(None) } @@ -169,6 +226,37 @@ impl PluginMgr { Ok(Self { plugins }) } + /// Add a plugin received from the server + pub fn load_server_plugin(&mut self, path: PathBuf) -> Result { + 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, + ) -> Result { + 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 { + 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( &mut self, ecs: &EcsWorld, diff --git a/server/src/client.rs b/server/src/client.rs index 4b28ba8c93..6df53d894e 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -213,7 +213,8 @@ impl Client { | ServerGeneral::CreateEntity(_) | ServerGeneral::DeleteEntity(_) | ServerGeneral::Disconnect(_) - | ServerGeneral::Notification(_) => { + | ServerGeneral::Notification(_) + | ServerGeneral::PluginData(_) => { PreparedMsg::new(3, &g, &self.general_stream_params) }, } diff --git a/server/src/events/information.rs b/server/src/events/information.rs index 97d062e38a..b5e742d46c 100644 --- a/server/src/events/information.rs +++ b/server/src/events/information.rs @@ -1,6 +1,8 @@ use crate::client::Client; use common::event::RequestSiteInfoEvent; use common_net::msg::{world_msg::EconomyInfo, ServerGeneral}; +#[cfg(feature = "plugins")] +use common_state::plugin::PluginMgr; use specs::{DispatcherBuilder, ReadExpect, ReadStorage}; use std::collections::HashMap; use world::IndexOwned; @@ -9,6 +11,8 @@ use super::{event_dispatch, ServerEvent}; pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) { event_dispatch::(builder); + #[cfg(feature = "plugins")] + event_dispatch::(builder); } #[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, + (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") + }); + } + } + } + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 88371b9414..b2d74f8a0d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -117,12 +117,15 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -#[cfg(not(feature = "worldgen"))] -use test_world::{IndexOwned, World}; use tokio::runtime::Runtime; use tracing::{debug, error, info, trace, warn}; use vek::*; pub use world::{civ::WorldCivStage, sim::WorldSimStage, WorldGenerateStage}; +#[cfg(not(feature = "worldgen"))] +use { + common_net::msg::WorldMapMsg, + test_world::{IndexOwned, World}, +}; use crate::{ persistence::{DatabaseSettings, SqlLogMode}, @@ -308,6 +311,7 @@ impl Server { horizons: [(vec![0], vec![0]), (vec![0], vec![0])], alt: Grid::new(Vec2::new(1, 1), 1), sites: Vec::new(), + possible_starting_sites: Vec::new(), pois: Vec::new(), default_chunk: Arc::new(world.generate_oob_chunk()), }; @@ -318,7 +322,10 @@ impl Server { let mut state = State::server( Arc::clone(&pools), + #[cfg(feature = "worldgen")] world.sim().map_size_lg(), + #[cfg(not(feature = "worldgen"))] + common::terrain::map::MapSizeLg::new(Vec2::one()).unwrap(), Arc::clone(&map.default_chunk), |dispatcher_builder| { add_local_systems(dispatcher_builder); diff --git a/server/src/lod.rs b/server/src/lod.rs index 513fec45fa..60e0c57461 100644 --- a/server/src/lod.rs +++ b/server/src/lod.rs @@ -37,7 +37,9 @@ impl Lod { } #[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) -> &lod::Zone { self.zones.get(&zone_pos).unwrap_or(&EMPTY_ZONE) diff --git a/server/src/sys/msg/character_screen.rs b/server/src/sys/msg/character_screen.rs index a9ac9f26c0..4ea0812412 100644 --- a/server/src/sys/msg/character_screen.rs +++ b/server/src/sys/msg/character_screen.rs @@ -182,6 +182,7 @@ impl Sys { offhand.clone(), body, character_updater, + #[cfg(feature = "worldgen")] start_site.and_then(|site_idx| { // TODO: This corresponds to the ID generation logic in // `world/src/lib.rs`. Really, we should have @@ -214,6 +215,8 @@ impl Sys { ) }) }), + #[cfg(not(feature = "worldgen"))] + None, ) { debug!( ?error, diff --git a/server/src/sys/msg/general.rs b/server/src/sys/msg/general.rs index 423811b126..1c1e82a3ae 100644 --- a/server/src/sys/msg/general.rs +++ b/server/src/sys/msg/general.rs @@ -17,7 +17,7 @@ event_emitters! { command: event::CommandEvent, client_disconnect: event::ClientDisconnectEvent, chat: event::ChatEvent, - + plugins: event::RequestPluginsEvent, } } @@ -72,6 +72,10 @@ impl Sys { 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"); emitters.emit(event::ClientDisconnectEvent( diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index 29dacca4c3..830dc4a0ee 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -259,7 +259,8 @@ impl Sys { | ClientGeneral::LodZoneRequest { .. } | ClientGeneral::ChatMsg(_) | ClientGeneral::Command(..) - | ClientGeneral::Terminate => { + | ClientGeneral::Terminate + | ClientGeneral::RequestPlugins(_) => { debug!("Kicking possibly misbehaving client due to invalid client in game request"); emitters.emit(event::ClientDisconnectEvent( entity, diff --git a/server/src/sys/msg/register.rs b/server/src/sys/msg/register.rs index db4fb10286..024920ef01 100644 --- a/server/src/sys/msg/register.rs +++ b/server/src/sys/msg/register.rs @@ -6,12 +6,12 @@ use crate::{ EditableSettings, Settings, }; use common::{ - comp::{self, Admin, Health, Player, Stats}, + comp::{self, Admin, Player, Stats}, event::{ClientDisconnectEvent, EventBus, MakeAdminEvent}, recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book}, resources::TimeOfDay, shared_server_config::ServerConstants, - uid::{IdMaps, Uid}, + uid::Uid, }; use common_base::prof_span; use common_ecs::{Job, Origin, Phase, System}; @@ -52,9 +52,8 @@ pub struct ReadData<'a> { ability_map: ReadExpect<'a, comp::item::tool::AbilityMap>, map: ReadExpect<'a, WorldMapMsg>, trackers: TrackedStorages<'a>, - _healths: ReadStorage<'a, Health>, // used by plugin feature - _plugin_mgr: ReadPlugin<'a>, // used by plugin feature - _id_maps: Read<'a, IdMaps>, // used by plugin feature + #[allow(dead_code)] + plugin_mgr: ReadPlugin<'a>, // only used by plugins feature } /// 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. 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 description = ServerDescription { @@ -358,6 +361,7 @@ impl<'a> System<'a> for Sys { day_cycle_coefficient: read_data.settings.day_cycle_coefficient() }, description, + active_plugins, })?; debug!("Done initial sync with client."); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 6fed09913e..b637822b22 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -717,11 +717,14 @@ where let world_aabr_in_chunks = Aabr { min: Vec2::zero(), // NOTE: Cast is correct because chunk coordinates must fit in an i32 (actually, i16). + #[cfg(feature = "worldgen")] max: world .sim() .get_size() .map(|x| x.saturating_sub(1)) .as_::(), + #[cfg(not(feature = "worldgen"))] + max: Vec2::one(), }; let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) = diff --git a/server/src/sys/terrain_sync.rs b/server/src/sys/terrain_sync.rs index cfc32fe04e..74606e50d6 100644 --- a/server/src/sys/terrain_sync.rs +++ b/server/src/sys/terrain_sync.rs @@ -45,6 +45,7 @@ impl<'a> System<'a> for Sys { ): Self::SystemData, ) { 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( &world, max_view_distance, @@ -53,6 +54,8 @@ impl<'a> System<'a> for Sys { &presences, &clients, ); + #[cfg(not(feature = "worldgen"))] + let presences_position_entities: Vec<((vek::Vec2, i32), specs::Entity)> = Vec::new(); let real_max_view_distance = super::terrain::convert_to_loaded_vd(u32::MAX, max_view_distance); diff --git a/server/src/test_world.rs b/server/src/test_world.rs index 6cb3c1d992..eaaf930a6e 100644 --- a/server/src/test_world.rs +++ b/server/src/test_world.rs @@ -2,11 +2,13 @@ use common::{ calendar::Calendar, generation::{ChunkSupplement, EntityInfo}, resources::TimeOfDay, + rtsim::ChunkResource, terrain::{ Block, BlockKind, MapSizeLg, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, }, vol::{ReadVol, RectVolSize, WriteVol}, }; +use enum_map::EnumMap; use rand::{prelude::*, rngs::SmallRng}; use std::time::Duration; use vek::*; @@ -48,6 +50,7 @@ impl World { &self, _index: IndexRef, chunk_pos: Vec2, + _rtsim_resources: Option>, _should_continue: impl FnMut() -> bool, _time: Option<(TimeOfDay, Calendar)>, ) -> Result<(TerrainChunk, ChunkSupplement), ()> { @@ -71,4 +74,6 @@ impl World { supplement, )) } + + pub fn get_location_name(&self, _index: IndexRef, _wpos2d: Vec2) -> Option { None } } diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 36885d0d20..c8a372ea9e 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -127,6 +127,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } num_cpus = "1.0" inline_tweak = { workspace = true } itertools = { workspace = true } +sha2 = { workspace = true } # Discord RPC discord-sdk = { version = "0.3.0", optional = true } diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 431f411c64..d2b8d52de1 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -12,6 +12,8 @@ use crate::{ use client::{self, Client}; use common::{comp, event::UpdateCharacterMetadata, resources::DeltaTime}; use common_base::span; +#[cfg(feature = "plugins")] +use common_state::plugin::PluginMgr; use specs::WorldExt; use std::{cell::RefCell, rc::Rc}; use tracing::error; @@ -69,7 +71,9 @@ impl CharSelectionState { impl PlayState for CharSelectionState { fn enter(&mut self, global_state: &mut GlobalState, _: Direction) { // Load the player's character list - self.client.borrow_mut().load_character_list(); + if self.client.borrow().num_missing_plugins() == 0 { + self.client.borrow_mut().load_character_list(); + } // Updated localization in case the selected language was changed self.char_selection_ui.update_language(global_state.i18n); @@ -274,6 +278,27 @@ impl PlayState for CharSelectionState { 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::() + .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. _ => {}, } diff --git a/voxygen/src/menu/main/client_init.rs b/voxygen/src/menu/main/client_init.rs index fe7620d6f6..15a0b7cfd3 100644 --- a/voxygen/src/menu/main/client_init.rs +++ b/voxygen/src/menu/main/client_init.rs @@ -5,6 +5,7 @@ use client::{ }; use crossbeam_channel::{unbounded, Receiver, Sender, TryRecvError}; use std::{ + path::Path, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -50,6 +51,7 @@ impl ClientInit { password: String, runtime: Arc, locale: Option, + config_dir: &Path, ) -> Self { let (tx, rx) = unbounded(); let (trust_tx, trust_rx) = unbounded(); @@ -58,6 +60,7 @@ impl ClientInit { let cancel2 = Arc::clone(&cancel); let runtime2 = Arc::clone(&runtime); + let config_dir = config_dir.to_path_buf(); runtime.spawn(async move { let trust_fn = |auth_server: &str| { @@ -89,6 +92,7 @@ impl ClientInit { let _ = init_stage_tx.send(stage); }, crate::ecs::sys::add_local_systems, + config_dir.clone(), ) .await { diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 86cc02230b..42cb84f9d6 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -18,10 +18,13 @@ use client::{ use client_init::{ClientInit, Error as InitError, Msg as InitMsg}; use common::comp; use common_base::span; +#[cfg(feature = "plugins")] +use common_state::plugin::PluginMgr; use i18n::LocalizationHandle; #[cfg(feature = "singleplayer")] use server::ServerInitStage; -use std::sync::Arc; +use specs::WorldExt; +use std::{path::Path, sync::Arc}; use tokio::runtime; use tracing::error; use ui::{Event as MainMenuEvent, MainMenuUi}; @@ -129,6 +132,7 @@ impl PlayState for MainMenuState { global_state.settings.language.selected_language.clone(), ), &global_state.i18n, + &global_state.config_dir, ); }, Ok(Err(e)) => { @@ -216,6 +220,18 @@ impl PlayState for MainMenuState { // Poll client creation. match self.init.client().and_then(|init| init.poll()) { 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::() + .load_server_plugin(path) + { + tracing::error!(?e, "load local plugin"); + } + } // Register voxygen components / resources crate::ecs::init(client.state_mut().ecs_mut()); self.init = InitState::Pipeline(Box::new(client)); @@ -267,6 +283,29 @@ impl PlayState for MainMenuState { ); 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::() + .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 .then_some(global_state.settings.language.selected_language.clone()), &global_state.i18n, + &global_state.config_dir, ); }, MainMenuEvent::CancelLoginAttempt => { @@ -609,6 +649,7 @@ fn attempt_login( runtime: &Arc, locale: Option, localized_strings: &LocalizationHandle, + config_dir: &Path, ) { let localization = localized_strings.read(); if let Err(err) = comp::Player::alias_validate(&username) { @@ -641,6 +682,7 @@ fn attempt_login( password, Arc::clone(runtime), locale, + config_dir, )); } } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index b74d3b12b7..843465fd46 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -447,6 +447,9 @@ impl SessionState { client::Event::SpectatePosition(pos) => { self.scene.camera_mut().force_focus_pos(pos); }, + client::Event::PluginDataReceived(data) => { + tracing::warn!("Received plugin data at wrong time {}", data.len()); + }, } }