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
- 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

3
Cargo.lock generated
View File

@ -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",

View File

@ -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" }

View File

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

View File

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

View File

@ -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<f32>),
SpectatePosition(Vec3<f32>),
PluginDataReceived(Vec<u8>),
}
#[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<PluginHash>,
/// Locally cached plugins needed by the server
local_plugins: Vec<PathBuf>,
}
/// 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<Self, Error> {
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<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
state.ecs_mut().register::<comp::Last<CharacterState>>();
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<PathBuf> { 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");

View File

@ -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 }

View File

@ -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<PluginHash>),
}
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,

View File

@ -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<PluginHash>,
},
}
@ -219,6 +220,8 @@ pub enum ServerGeneral {
/// Suggest the client to spectate a position. Called after client has
/// requested teleport etc.
SpectatePosition(Vec3<f32>),
/// Plugin data requested from the server
PluginData(Vec<u8>),
}
impl ServerGeneral {
@ -358,6 +361,7 @@ impl ServerMsg {
| ServerGeneral::Disconnect(_)
| ServerGeneral::Notification(_)
| ServerGeneral::LodZoneUpdate { .. } => true,
ServerGeneral::PluginData(_) => true,
}
},
ServerMsg::Ping(_) => true,

View File

@ -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<i32>,
pub enable: bool,
}
pub struct RequestPluginsEvent {
pub entity: EcsEntity,
pub plugins: Vec<PluginHash>,
}
pub struct EventBus<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::<ToggleSpriteLightEvent>::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

View File

@ -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"] }

View File

@ -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<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 {
data: PluginData,
modules: Vec<PluginModule>,
#[allow(dead_code)]
files: HashMap<PathBuf, Vec<u8>>,
hash: PluginHash,
#[allow(dead_code)]
path: PathBuf,
#[allow(dead_code)]
data_buf: Vec<u8>,
}
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();
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::<Result<_, _>>()?;
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| {
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<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(
&mut self,
ecs: &EcsWorld,

View File

@ -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)
},
}

View File

@ -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::<RequestSiteInfoEvent>(builder);
#[cfg(feature = "plugins")]
event_dispatch::<common::event::RequestPluginsEvent>(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<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,
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);

View File

@ -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<i32>) -> &lod::Zone {
self.zones.get(&zone_pos).unwrap_or(&EMPTY_ZONE)

View File

@ -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,

View File

@ -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(

View File

@ -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,

View File

@ -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.");

View File

@ -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_::<i32>(),
#[cfg(not(feature = "worldgen"))]
max: Vec2::one(),
};
let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =

View File

@ -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<i16>, i32), specs::Entity)> = Vec::new();
let real_max_view_distance =
super::terrain::convert_to_loaded_vd(u32::MAX, max_view_distance);

View File

@ -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<i32>,
_rtsim_resources: Option<EnumMap<ChunkResource, f32>>,
_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<i32>) -> Option<String> { None }
}

View File

@ -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 }

View File

@ -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
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::<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.
_ => {},
}

View File

@ -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<runtime::Runtime>,
locale: Option<String>,
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
{

View File

@ -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::<PluginMgr>()
.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::<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
.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<runtime::Runtime>,
locale: Option<String>,
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,
));
}
}

View File

@ -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());
},
}
}