mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added rtsim saving, chunk resources, chunk resource depletion
This commit is contained in:
parent
d5e324bded
commit
c168ff2f9b
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -1839,6 +1839,27 @@ dependencies = [
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1"
|
||||
dependencies = [
|
||||
"enum-map-derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map-derive"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.43",
|
||||
"quote 1.0.21",
|
||||
"syn 1.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumset"
|
||||
version = "1.0.11"
|
||||
@ -6655,6 +6676,8 @@ dependencies = [
|
||||
"crossbeam-utils 0.8.11",
|
||||
"csv",
|
||||
"dot_vox",
|
||||
"enum-iterator 1.1.3",
|
||||
"enum-map",
|
||||
"fxhash",
|
||||
"hashbrown 0.12.3",
|
||||
"indexmap",
|
||||
@ -6888,9 +6911,11 @@ dependencies = [
|
||||
name = "veloren-rtsim"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"enum-map",
|
||||
"hashbrown 0.12.3",
|
||||
"ron 0.8.0",
|
||||
"serde",
|
||||
"vek 0.15.8",
|
||||
"veloren-common",
|
||||
"veloren-world",
|
||||
]
|
||||
@ -6907,6 +6932,7 @@ dependencies = [
|
||||
"chrono-tz",
|
||||
"crossbeam-channel",
|
||||
"drop_guard",
|
||||
"enum-map",
|
||||
"enumset",
|
||||
"futures-util",
|
||||
"hashbrown 0.12.3",
|
||||
@ -7120,6 +7146,7 @@ dependencies = [
|
||||
"csv",
|
||||
"deflate",
|
||||
"enum-iterator 1.1.3",
|
||||
"enum-map",
|
||||
"fallible-iterator",
|
||||
"flate2",
|
||||
"fxhash",
|
||||
|
@ -1855,6 +1855,7 @@ impl Client {
|
||||
true,
|
||||
None,
|
||||
&self.connected_server_constants,
|
||||
|_, _, _, _| {},
|
||||
);
|
||||
// TODO: avoid emitting these in the first place
|
||||
let _ = self
|
||||
|
@ -26,6 +26,8 @@ common-base = { package = "veloren-common-base", path = "base" }
|
||||
serde = { version = "1.0.110", features = ["derive", "rc"] }
|
||||
|
||||
# Util
|
||||
enum-iterator = "1.1.3"
|
||||
enum-map = "2.4"
|
||||
vek = { version = "0.15.8", features = ["serde"] }
|
||||
cfg-if = "1.0.0"
|
||||
chrono = "0.4.22"
|
||||
|
@ -8,7 +8,9 @@ use crate::{
|
||||
lottery::LootSpec,
|
||||
npc::{self, NPC_NAMES},
|
||||
trade::SiteInformation,
|
||||
rtsim,
|
||||
};
|
||||
use enum_map::EnumMap;
|
||||
use serde::Deserialize;
|
||||
use vek::*;
|
||||
|
||||
@ -449,6 +451,7 @@ impl EntityInfo {
|
||||
#[derive(Default)]
|
||||
pub struct ChunkSupplement {
|
||||
pub entities: Vec<EntityInfo>,
|
||||
pub rtsim_max_resources: EnumMap<rtsim::ChunkResource, usize>,
|
||||
}
|
||||
|
||||
impl ChunkSupplement {
|
||||
|
@ -4,6 +4,7 @@
|
||||
// module in `server`.
|
||||
|
||||
use specs::Component;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use vek::*;
|
||||
|
||||
use crate::comp::dialogue::MoodState;
|
||||
@ -82,3 +83,10 @@ impl RtSimController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)]
|
||||
pub enum ChunkResource {
|
||||
Grass,
|
||||
Flax,
|
||||
Cotton,
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use crate::{
|
||||
consts::FRIC_GROUND,
|
||||
lottery::LootSpec,
|
||||
make_case_elim,
|
||||
rtsim,
|
||||
};
|
||||
use num_derive::FromPrimitive;
|
||||
use num_traits::FromPrimitive;
|
||||
@ -195,6 +196,26 @@ impl Block {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the rtsim resource, if any, that this block corresponds to. If you want the scarcity of a block to change with rtsim's resource depletion tracking, you can do so by editing this function.
|
||||
#[inline]
|
||||
pub fn get_rtsim_resource(&self) -> Option<rtsim::ChunkResource> {
|
||||
match self.get_sprite()? {
|
||||
SpriteKind::LongGrass
|
||||
| SpriteKind::MediumGrass
|
||||
| SpriteKind::ShortGrass
|
||||
| SpriteKind::LargeGrass
|
||||
| SpriteKind::GrassSnow
|
||||
| SpriteKind::GrassBlue
|
||||
| SpriteKind::SavannaGrass
|
||||
| SpriteKind::TallSavannaGrass
|
||||
| SpriteKind::RedSavannaGrass
|
||||
| SpriteKind::JungleRedGrass => Some(rtsim::ChunkResource::Grass),
|
||||
SpriteKind::WildFlax => Some(rtsim::ChunkResource::Flax),
|
||||
SpriteKind::Cotton => Some(rtsim::ChunkResource::Cotton),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_glow(&self) -> Option<u8> {
|
||||
match self.kind() {
|
||||
|
@ -524,7 +524,12 @@ impl State {
|
||||
}
|
||||
|
||||
// Apply terrain changes
|
||||
pub fn apply_terrain_changes(&self) { self.apply_terrain_changes_internal(false); }
|
||||
pub fn apply_terrain_changes(
|
||||
&self,
|
||||
mut block_update: impl FnMut(&specs::World, Vec3<i32>, Block, Block),
|
||||
) {
|
||||
self.apply_terrain_changes_internal(false, block_update);
|
||||
}
|
||||
|
||||
/// `during_tick` is true if and only if this is called from within
|
||||
/// [State::tick].
|
||||
@ -534,7 +539,11 @@ impl State {
|
||||
/// from within both the client and the server ticks, right after
|
||||
/// handling terrain messages; currently, client sets it to true and
|
||||
/// server to false.
|
||||
fn apply_terrain_changes_internal(&self, during_tick: bool) {
|
||||
fn apply_terrain_changes_internal(
|
||||
&self,
|
||||
during_tick: bool,
|
||||
mut block_update: impl FnMut(&specs::World, Vec3<i32>, Block, Block),
|
||||
) {
|
||||
span!(
|
||||
_guard,
|
||||
"apply_terrain_changes",
|
||||
@ -575,14 +584,17 @@ impl State {
|
||||
}
|
||||
// Apply block modifications
|
||||
// Only include in `TerrainChanges` if successful
|
||||
modified_blocks.retain(|pos, block| {
|
||||
let res = terrain.set(*pos, *block);
|
||||
modified_blocks.retain(|pos, new_block| {
|
||||
let res = terrain.map(*pos, |old_block| {
|
||||
block_update(&self.ecs, *pos, old_block, *new_block);
|
||||
*new_block
|
||||
});
|
||||
if let (&Ok(old_block), true) = (&res, during_tick) {
|
||||
// NOTE: If the changes are applied during the tick, we push the *old* value as
|
||||
// the modified block (since it otherwise can't be recovered after the tick).
|
||||
// Otherwise, the changes will be applied after the tick, so we push the *new*
|
||||
// value.
|
||||
*block = old_block;
|
||||
*new_block = old_block;
|
||||
}
|
||||
res.is_ok()
|
||||
});
|
||||
@ -597,6 +609,7 @@ impl State {
|
||||
update_terrain_and_regions: bool,
|
||||
mut metrics: Option<&mut StateTickMetrics>,
|
||||
server_constants: &ServerConstants,
|
||||
block_update: impl FnMut(&specs::World, Vec3<i32>, Block, Block),
|
||||
) {
|
||||
span!(_guard, "tick", "State::tick");
|
||||
|
||||
@ -643,7 +656,7 @@ impl State {
|
||||
drop(guard);
|
||||
|
||||
if update_terrain_and_regions {
|
||||
self.apply_terrain_changes_internal(true);
|
||||
self.apply_terrain_changes_internal(true, block_update);
|
||||
}
|
||||
|
||||
// Process local events
|
||||
|
@ -9,3 +9,5 @@ world = { package = "veloren-world", path = "../world" }
|
||||
ron = "0.8"
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] }
|
||||
enum-map = { version = "2.4", features = ["serde"] }
|
||||
vek = { version = "0.15.8", features = ["serde"] }
|
||||
|
@ -1,13 +1,16 @@
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ActorId {
|
||||
pub idx: u32,
|
||||
pub gen: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Actor {}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Actors {
|
||||
pub actors: HashMap<ActorId, Actor>,
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
use serde::{
|
||||
de::{DeserializeOwned, Error},
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Default, PartialEq, Eq, Hash)]
|
||||
pub struct V<T>(pub T);
|
||||
|
||||
impl<T: Serialize> Serialize for V<T> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Version> Deserialize<'de> for V<T> {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
T::try_from_value_compat(ron::Value::deserialize(deserializer)?)
|
||||
.map(Self)
|
||||
.map_err(|e| D::Error::custom(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, T: Latest<U>> Latest<U> for V<T> {
|
||||
fn to_unversioned(self) -> U { self.0.to_unversioned() }
|
||||
|
||||
fn from_unversioned(x: &U) -> Self { Self(T::from_unversioned(x)) }
|
||||
}
|
||||
|
||||
pub trait Latest<T> {
|
||||
fn to_unversioned(self) -> T;
|
||||
fn from_unversioned(x: &T) -> Self;
|
||||
}
|
||||
|
||||
pub trait Version: Sized + DeserializeOwned {
|
||||
type Prev: Version;
|
||||
|
||||
fn migrate(prev: Self::Prev) -> Self;
|
||||
|
||||
fn try_from_value_compat(value: ron::Value) -> Result<Self, ron::Error> {
|
||||
value.clone().into_rust().or_else(|e| {
|
||||
Ok(Self::migrate(
|
||||
<Self as Version>::Prev::try_from_value_compat(value).map_err(|_| e)?,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum Bottom {}
|
||||
|
||||
impl Version for Bottom {
|
||||
type Prev = Self;
|
||||
|
||||
fn migrate(prev: Self::Prev) -> Self { prev }
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
pub mod helper;
|
||||
pub mod version;
|
||||
|
||||
pub mod actor;
|
||||
pub mod nature;
|
||||
|
||||
@ -11,9 +8,10 @@ pub use self::{
|
||||
|
||||
use self::helper::Latest;
|
||||
use ron::error::SpannedResult;
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Data {
|
||||
pub nature: Nature,
|
||||
pub actors: Actors,
|
||||
@ -21,10 +19,10 @@ pub struct Data {
|
||||
|
||||
impl Data {
|
||||
pub fn from_reader<R: Read>(reader: R) -> SpannedResult<Self> {
|
||||
ron::de::from_reader(reader).map(version::LatestData::to_unversioned)
|
||||
ron::de::from_reader(reader)
|
||||
}
|
||||
|
||||
pub fn write_to<W: Write>(&self, writer: W) -> Result<(), ron::Error> {
|
||||
ron::ser::to_writer(writer, &version::LatestData::from_unversioned(self))
|
||||
ron::ser::to_writer(writer, self)
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,44 @@
|
||||
pub struct Nature {}
|
||||
use serde::{Serialize, Deserialize};
|
||||
use enum_map::EnumMap;
|
||||
use common::{
|
||||
grid::Grid,
|
||||
rtsim::ChunkResource,
|
||||
};
|
||||
use world::World;
|
||||
use vek::*;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Nature {
|
||||
chunks: Grid<Chunk>,
|
||||
}
|
||||
|
||||
impl Nature {
|
||||
pub fn generate(world: &World) -> Self {
|
||||
Self {
|
||||
chunks: Grid::populate_from(
|
||||
world.sim().get_size().map(|e| e as i32),
|
||||
|pos| Chunk {
|
||||
res: EnumMap::<_, f32>::default().map(|_, _| 1.0),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Clean up this API a bit
|
||||
pub fn get_chunk_resources(&self, key: Vec2<i32>) -> EnumMap<ChunkResource, f32> {
|
||||
self.chunks
|
||||
.get(key)
|
||||
.map(|c| c.res)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
pub fn set_chunk_resources(&mut self, key: Vec2<i32>, res: EnumMap<ChunkResource, f32>) {
|
||||
if let Some(chunk) = self.chunks.get_mut(key) {
|
||||
chunk.res = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Chunk {
|
||||
res: EnumMap<ChunkResource, f32>,
|
||||
}
|
||||
|
@ -1,85 +0,0 @@
|
||||
use super::*;
|
||||
use crate::data::{Actor, ActorId, Actors};
|
||||
use hashbrown::HashMap;
|
||||
|
||||
// ActorId
|
||||
|
||||
impl Latest<ActorId> for ActorIdV0 {
|
||||
fn to_unversioned(self) -> ActorId {
|
||||
ActorId {
|
||||
idx: self.idx,
|
||||
gen: self.gen,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_unversioned(id: &ActorId) -> Self {
|
||||
Self {
|
||||
idx: id.idx,
|
||||
gen: id.gen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct ActorIdV0 {
|
||||
pub idx: u32,
|
||||
pub gen: u32,
|
||||
}
|
||||
|
||||
impl Version for ActorIdV0 {
|
||||
type Prev = Bottom;
|
||||
|
||||
fn migrate(x: Self::Prev) -> Self { match x {} }
|
||||
}
|
||||
|
||||
// Actor
|
||||
|
||||
impl Latest<Actor> for ActorV0 {
|
||||
fn to_unversioned(self) -> Actor { Actor {} }
|
||||
|
||||
fn from_unversioned(actor: &Actor) -> Self { Self {} }
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct ActorV0 {}
|
||||
|
||||
impl Version for ActorV0 {
|
||||
type Prev = Bottom;
|
||||
|
||||
fn migrate(x: Self::Prev) -> Self { match x {} }
|
||||
}
|
||||
|
||||
// Actors
|
||||
|
||||
impl Latest<Actors> for ActorsV0 {
|
||||
fn to_unversioned(self) -> Actors {
|
||||
Actors {
|
||||
actors: self
|
||||
.actors
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_unversioned(), v.to_unversioned()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_unversioned(actors: &Actors) -> Self {
|
||||
Self {
|
||||
actors: actors
|
||||
.actors
|
||||
.iter()
|
||||
.map(|(k, v)| (Latest::from_unversioned(k), Latest::from_unversioned(v)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ActorsV0 {
|
||||
actors: HashMap<V<ActorIdV0>, V<ActorV0>>,
|
||||
}
|
||||
|
||||
impl Version for ActorsV0 {
|
||||
type Prev = Bottom;
|
||||
|
||||
fn migrate(x: Self::Prev) -> Self { match x {} }
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
// # Hey, you! Yes, you!
|
||||
//
|
||||
// Don't touch anything in this module, or any sub-modules. No, really. Bad
|
||||
// stuff will happen.
|
||||
//
|
||||
// You're only an exception to this rule if you fulfil the following criteria:
|
||||
//
|
||||
// - You *really* understand exactly how the versioning system in `helper.rs`
|
||||
// works, what assumptions it makes, and how all of this can go badly wrong.
|
||||
//
|
||||
// - You are creating a new version of a data structure, and *not* modifying an
|
||||
// existing one.
|
||||
//
|
||||
// - You've thought really carefully about things and you've come to the
|
||||
// conclusion that there's just no way to add the feature you want to add
|
||||
// without creating a new version of the data structure in question.
|
||||
//
|
||||
// Please note that in *very specific* cases, it is possible to make a change to
|
||||
// an existing data structure that is backward-compatible. For example, adding a
|
||||
// new variant to an enum or a new field to a struct (where said field is
|
||||
// annotated with `#[serde(default)]`) is generally considered to be a
|
||||
// backward-compatible change.
|
||||
//
|
||||
// That said, here's how to make a breaking change to one of the structures in
|
||||
// this module, or submodules.
|
||||
//
|
||||
// 1) Duplicate the latest version of the data structure and the `Version` impl
|
||||
// for it (later versions should be kept at the top of each file).
|
||||
//
|
||||
// 2) Rename the duplicated version, incrementing the version number (i.e: V0
|
||||
// becomes V1).
|
||||
//
|
||||
// 3) Change the `type Prev =` associated type in the new `Version` impl to the
|
||||
// previous versions' type. You will need to write an implementation of
|
||||
// `migrate` that migrates from the old version to the new version.
|
||||
//
|
||||
// 4) *Change* the existing `Latest` impl so that it uses the new version you
|
||||
// have created.
|
||||
//
|
||||
// 5) If your data structure is contained within another data structure, you
|
||||
// will need to similarly update the parent data structure too, also
|
||||
// following these instructions.
|
||||
//
|
||||
// The *golden rule* is that, once merged to master, an old version's type must
|
||||
// not be changed!
|
||||
|
||||
pub mod actor;
|
||||
pub mod nature;
|
||||
|
||||
use super::{
|
||||
helper::{Bottom, Latest, Version, V},
|
||||
Data,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type LatestData = DataV0;
|
||||
|
||||
impl Latest<Data> for LatestData {
|
||||
fn to_unversioned(self) -> Data {
|
||||
Data {
|
||||
nature: self.nature.to_unversioned(),
|
||||
actors: self.actors.to_unversioned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_unversioned(data: &Data) -> Self {
|
||||
Self {
|
||||
nature: Latest::from_unversioned(&data.nature),
|
||||
actors: Latest::from_unversioned(&data.actors),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DataV0 {
|
||||
nature: V<nature::NatureV0>,
|
||||
actors: V<actor::ActorsV0>,
|
||||
}
|
||||
|
||||
impl Version for DataV0 {
|
||||
type Prev = Bottom;
|
||||
|
||||
fn migrate(x: Self::Prev) -> Self { match x {} }
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
use super::*;
|
||||
use crate::data::Nature;
|
||||
|
||||
impl Latest<crate::data::Nature> for NatureV0 {
|
||||
fn to_unversioned(self) -> Nature { Nature {} }
|
||||
|
||||
fn from_unversioned(nature: &Nature) -> Self { Self {} }
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct NatureV0 {}
|
||||
|
||||
impl Version for NatureV0 {
|
||||
type Prev = Bottom;
|
||||
|
||||
fn migrate(x: Self::Prev) -> Self { match x {} }
|
||||
}
|
@ -5,7 +5,7 @@ use world::World;
|
||||
impl Data {
|
||||
pub fn generate(world: &World) -> Self {
|
||||
Self {
|
||||
nature: Nature {},
|
||||
nature: Nature::generate(world),
|
||||
actors: Actors {
|
||||
actors: HashMap::default(),
|
||||
},
|
||||
|
@ -64,6 +64,7 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253
|
||||
slab = "0.4"
|
||||
rand_distr = "0.4.0"
|
||||
enumset = "1.0.8"
|
||||
enum-map = "2.4"
|
||||
noise = { version = "0.7", default-features = false }
|
||||
censor = "0.2"
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
use crate::metrics::ChunkGenMetrics;
|
||||
use crate::{
|
||||
metrics::ChunkGenMetrics,
|
||||
rtsim2::RtSim,
|
||||
};
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
use crate::test_world::{IndexOwned, World};
|
||||
use common::{
|
||||
@ -44,6 +47,10 @@ impl ChunkGenerator {
|
||||
key: Vec2<i32>,
|
||||
slowjob_pool: &SlowJobPool,
|
||||
world: Arc<World>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
rtsim: &RtSim,
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
rtsim: &(),
|
||||
index: IndexOwned,
|
||||
time: (TimeOfDay, Calendar),
|
||||
) {
|
||||
@ -56,10 +63,17 @@ impl ChunkGenerator {
|
||||
v.insert(Arc::clone(&cancel));
|
||||
let chunk_tx = self.chunk_tx.clone();
|
||||
self.metrics.chunks_requested.inc();
|
||||
|
||||
// Get state for this chunk from rtsim
|
||||
#[cfg(feature = "worldgen")]
|
||||
let rtsim_resources = Some(rtsim.get_chunk_resources(key));
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
let rtsim_resources = None;
|
||||
|
||||
slowjob_pool.spawn("CHUNK_GENERATOR", move || {
|
||||
let index = index.as_index_ref();
|
||||
let payload = world
|
||||
.generate_chunk(index, key, || cancel.load(Ordering::Relaxed), Some(time))
|
||||
.generate_chunk(index, key, rtsim_resources, || cancel.load(Ordering::Relaxed), Some(time))
|
||||
// FIXME: Since only the first entity who cancels a chunk is notified, we end up
|
||||
// delaying chunk re-requests for up to 3 seconds for other clients, which isn't
|
||||
// great. We *could* store all the other requesting clients here, but it could
|
||||
|
@ -82,7 +82,7 @@ use common::{
|
||||
rtsim::RtSimEntity,
|
||||
shared_server_config::ServerConstants,
|
||||
slowjob::SlowJobPool,
|
||||
terrain::{TerrainChunk, TerrainChunkSize},
|
||||
terrain::{TerrainChunk, TerrainChunkSize, Block},
|
||||
vol::RectRasterableVol,
|
||||
};
|
||||
use common_ecs::run_now;
|
||||
@ -342,6 +342,7 @@ impl Server {
|
||||
pool.configure("CHUNK_DROP", |_n| 1);
|
||||
pool.configure("CHUNK_GENERATOR", |n| n / 2 + n / 4);
|
||||
pool.configure("CHUNK_SERIALIZER", |n| n / 2);
|
||||
pool.configure("RTSIM_SAVE", |_| 1);
|
||||
}
|
||||
state
|
||||
.ecs_mut()
|
||||
@ -700,6 +701,13 @@ impl Server {
|
||||
|
||||
let before_state_tick = Instant::now();
|
||||
|
||||
fn on_block_update(ecs: &specs::World, wpos: Vec3<i32>, old_block: Block, new_block: Block) {
|
||||
// When a resource block updates, inform rtsim
|
||||
if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() {
|
||||
ecs.write_resource::<rtsim2::RtSim>().hook_block_update(wpos, old_block, new_block);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Tick the server's LocalState.
|
||||
// 5) Fetch any generated `TerrainChunk`s and insert them into the terrain.
|
||||
// in sys/terrain.rs
|
||||
@ -726,6 +734,7 @@ impl Server {
|
||||
false,
|
||||
Some(&mut state_tick_metrics),
|
||||
&self.server_constants,
|
||||
on_block_update,
|
||||
);
|
||||
|
||||
let before_handle_events = Instant::now();
|
||||
@ -749,7 +758,7 @@ impl Server {
|
||||
self.state.update_region_map();
|
||||
// NOTE: apply_terrain_changes sends the *new* value since it is not being
|
||||
// synchronized during the tick.
|
||||
self.state.apply_terrain_changes();
|
||||
self.state.apply_terrain_changes(on_block_update);
|
||||
|
||||
let before_sync = Instant::now();
|
||||
|
||||
@ -994,6 +1003,10 @@ impl Server {
|
||||
let mut chunk_generator = ecs.write_resource::<ChunkGenerator>();
|
||||
let client = ecs.read_storage::<Client>();
|
||||
let mut terrain = ecs.write_resource::<common::terrain::TerrainGrid>();
|
||||
#[cfg(feature = "worldgen")]
|
||||
let rtsim = ecs.read_resource::<rtsim2::RtSim>();
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
let rtsim = ();
|
||||
|
||||
// Cancel all pending chunks.
|
||||
chunk_generator.cancel_all();
|
||||
@ -1009,6 +1022,7 @@ impl Server {
|
||||
pos,
|
||||
&slow_jobs,
|
||||
Arc::clone(world),
|
||||
&rtsim,
|
||||
index.clone(),
|
||||
(
|
||||
*ecs.read_resource::<TimeOfDay>(),
|
||||
@ -1172,11 +1186,16 @@ impl Server {
|
||||
pub fn generate_chunk(&mut self, entity: EcsEntity, key: Vec2<i32>) {
|
||||
let ecs = self.state.ecs();
|
||||
let slow_jobs = ecs.read_resource::<SlowJobPool>();
|
||||
#[cfg(feature = "worldgen")]
|
||||
let rtsim = ecs.read_resource::<rtsim2::RtSim>();
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
let rtsim = ();
|
||||
ecs.write_resource::<ChunkGenerator>().generate_chunk(
|
||||
Some(entity),
|
||||
key,
|
||||
&slow_jobs,
|
||||
Arc::clone(&self.world),
|
||||
&rtsim,
|
||||
self.index.clone(),
|
||||
(
|
||||
*ecs.read_resource::<TimeOfDay>(),
|
||||
|
@ -1,17 +1,31 @@
|
||||
pub mod tick;
|
||||
|
||||
use common::grid::Grid;
|
||||
use common::{
|
||||
grid::Grid,
|
||||
slowjob::SlowJobPool,
|
||||
rtsim::ChunkResource,
|
||||
terrain::{TerrainChunk, Block},
|
||||
vol::RectRasterableVol,
|
||||
};
|
||||
use common_ecs::{dispatch, System};
|
||||
use rtsim2::{data::Data, RtState};
|
||||
use specs::{DispatcherBuilder, WorldExt};
|
||||
use std::{fs::File, io, path::PathBuf, sync::Arc};
|
||||
use tracing::info;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
io::{self, Write},
|
||||
};
|
||||
use enum_map::EnumMap;
|
||||
use tracing::{error, warn, info};
|
||||
use vek::*;
|
||||
use world::World;
|
||||
|
||||
pub struct RtSim {
|
||||
file_path: PathBuf,
|
||||
chunk_states: Grid<bool>, // true = loaded
|
||||
last_saved: Option<Instant>,
|
||||
chunk_states: Grid<Option<LoadedChunkState>>,
|
||||
state: RtState,
|
||||
}
|
||||
|
||||
@ -20,21 +34,47 @@ impl RtSim {
|
||||
let file_path = Self::get_file_path(data_dir);
|
||||
|
||||
Ok(Self {
|
||||
chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| false),
|
||||
chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| None),
|
||||
last_saved: None,
|
||||
state: RtState {
|
||||
data: {
|
||||
info!("Looking for rtsim state in {}...", file_path.display());
|
||||
'load: {
|
||||
match File::open(&file_path) {
|
||||
Ok(file) => {
|
||||
info!("Rtsim state found. Attending to load...");
|
||||
Data::from_reader(file)?
|
||||
info!("Rtsim state found. Attempting to load...");
|
||||
match Data::from_reader(file) {
|
||||
Ok(data) => { info!("Rtsim state loaded."); break 'load data },
|
||||
Err(e) => {
|
||||
error!("Rtsim state failed to load: {}", e);
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let mut backup_path = file_path.clone();
|
||||
backup_path.set_extension(if i == 0 {
|
||||
format!("ron_backup_{}", i)
|
||||
} else {
|
||||
"ron_backup".to_string()
|
||||
});
|
||||
if !backup_path.exists() {
|
||||
fs::rename(&file_path, &backup_path)?;
|
||||
warn!("Failed rtsim state was moved to {}", backup_path.display());
|
||||
info!("A fresh rtsim state will now be generated.");
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
},
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
info!("No rtsim state found. Generating from initial world state...");
|
||||
Data::generate(&world)
|
||||
}
|
||||
},
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound =>
|
||||
info!("No rtsim state found. Generating from initial world state..."),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
let data = Data::generate(&world);
|
||||
info!("Rtsim state generated.");
|
||||
data
|
||||
}
|
||||
},
|
||||
},
|
||||
file_path,
|
||||
@ -52,17 +92,88 @@ impl RtSim {
|
||||
path
|
||||
}
|
||||
|
||||
pub fn hook_load_chunk(&mut self, key: Vec2<i32>) {
|
||||
if let Some(is_loaded) = self.chunk_states.get_mut(key) {
|
||||
*is_loaded = true;
|
||||
pub fn hook_load_chunk(&mut self, key: Vec2<i32>, max_res: EnumMap<ChunkResource, usize>) {
|
||||
if let Some(chunk_state) = self.chunk_states.get_mut(key) {
|
||||
*chunk_state = Some(LoadedChunkState { max_res });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hook_unload_chunk(&mut self, key: Vec2<i32>) {
|
||||
if let Some(is_loaded) = self.chunk_states.get_mut(key) {
|
||||
*is_loaded = false;
|
||||
if let Some(chunk_state) = self.chunk_states.get_mut(key) {
|
||||
*chunk_state = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self, slowjob_pool: &SlowJobPool) {
|
||||
info!("Beginning rtsim state save...");
|
||||
let file_path = self.file_path.clone();
|
||||
let data = self.state.data.clone();
|
||||
info!("Starting rtsim save job...");
|
||||
// TODO: Use slow job
|
||||
// slowjob_pool.spawn("RTSIM_SAVE", move || {
|
||||
std::thread::spawn(move || {
|
||||
let tmp_file_name = "state_tmp.ron";
|
||||
if let Err(e) = file_path
|
||||
.parent()
|
||||
.map(|dir| {
|
||||
fs::create_dir_all(dir)?;
|
||||
// We write to a temporary file and then rename to avoid corruption.
|
||||
Ok(dir.join(tmp_file_name))
|
||||
})
|
||||
.unwrap_or_else(|| Ok(tmp_file_name.into()))
|
||||
.and_then(|tmp_file_path| {
|
||||
Ok((File::create(&tmp_file_path)?, tmp_file_path))
|
||||
})
|
||||
.map_err(|e: io::Error| ron::Error::from(e))
|
||||
.and_then(|(mut file, tmp_file_path)| {
|
||||
info!("Writing rtsim state to file...");
|
||||
data.write_to(&mut file)?;
|
||||
file.flush()?;
|
||||
drop(file);
|
||||
fs::rename(tmp_file_path, file_path)?;
|
||||
info!("Rtsim state saved.");
|
||||
Ok(())
|
||||
})
|
||||
{
|
||||
error!("Saving rtsim state failed: {}", e);
|
||||
}
|
||||
});
|
||||
self.last_saved = Some(Instant::now());
|
||||
}
|
||||
|
||||
// TODO: Clean up this API a bit
|
||||
pub fn get_chunk_resources(&self, key: Vec2<i32>) -> EnumMap<ChunkResource, f32> {
|
||||
self.state.data.nature.get_chunk_resources(key)
|
||||
}
|
||||
pub fn hook_block_update(&mut self, wpos: Vec3<i32>, old_block: Block, new_block: Block) {
|
||||
let key = wpos
|
||||
.xy()
|
||||
.map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32));
|
||||
if let Some(Some(chunk_state)) = self.chunk_states.get(key) {
|
||||
let mut chunk_res = self.get_chunk_resources(key);
|
||||
// Remove resources
|
||||
if let Some(res) = old_block.get_rtsim_resource() {
|
||||
if chunk_state.max_res[res] > 0 {
|
||||
chunk_res[res] = (chunk_res[res] - 1.0 / chunk_state.max_res[res] as f32).max(0.0);
|
||||
println!("Subbing {} to resources", 1.0 / chunk_state.max_res[res] as f32);
|
||||
}
|
||||
}
|
||||
// Add resources
|
||||
if let Some(res) = new_block.get_rtsim_resource() {
|
||||
if chunk_state.max_res[res] > 0 {
|
||||
chunk_res[res] = (chunk_res[res] + 1.0 / chunk_state.max_res[res] as f32).min(1.0);
|
||||
println!("Added {} to resources", 1.0 / chunk_state.max_res[res] as f32);
|
||||
}
|
||||
}
|
||||
println!("Chunk resources are {:?}", chunk_res);
|
||||
self.state.data.nature.set_chunk_resources(key, chunk_res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadedChunkState {
|
||||
// The maximum possible number of each resource in this chunk
|
||||
max_res: EnumMap<ChunkResource, usize>,
|
||||
}
|
||||
|
||||
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||
|
@ -7,10 +7,11 @@ use common::{
|
||||
event::{EventBus, ServerEvent},
|
||||
generation::{BodyBuilder, EntityConfig, EntityInfo},
|
||||
resources::{DeltaTime, Time},
|
||||
slowjob::SlowJobPool,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sys;
|
||||
@ -22,6 +23,7 @@ impl<'a> System<'a> for Sys {
|
||||
WriteExpect<'a, RtSim>,
|
||||
ReadExpect<'a, Arc<world::World>>,
|
||||
ReadExpect<'a, world::IndexOwned>,
|
||||
ReadExpect<'a, SlowJobPool>,
|
||||
);
|
||||
|
||||
const NAME: &'static str = "rtsim::tick";
|
||||
@ -30,9 +32,14 @@ impl<'a> System<'a> for Sys {
|
||||
|
||||
fn run(
|
||||
_job: &mut Job<Self>,
|
||||
(dt, time, server_event_bus, mut rtsim, world, index): Self::SystemData,
|
||||
(dt, time, server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData,
|
||||
) {
|
||||
let rtsim = &mut *rtsim;
|
||||
|
||||
if rtsim.last_saved.map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) {
|
||||
rtsim.save(&slow_jobs);
|
||||
}
|
||||
|
||||
// rtsim.tick += 1;
|
||||
|
||||
// Update unloaded rtsim entities, in groups at a time
|
||||
|
@ -7,6 +7,7 @@ use crate::{
|
||||
presence::{Presence, RepositionOnChunkLoad},
|
||||
settings::Settings,
|
||||
sys::sentinel::DeletedEntities,
|
||||
rtsim2::RtSim,
|
||||
wiring, BattleModeBuffer, SpawnPoint,
|
||||
};
|
||||
use common::{
|
||||
@ -497,6 +498,10 @@ impl StateExt for State {
|
||||
{
|
||||
let ecs = self.ecs();
|
||||
let slow_jobs = ecs.write_resource::<SlowJobPool>();
|
||||
#[cfg(feature = "worldgen")]
|
||||
let rtsim = ecs.read_resource::<RtSim>();
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
let rtsim = ();
|
||||
let mut chunk_generator =
|
||||
ecs.write_resource::<crate::chunk_generator::ChunkGenerator>();
|
||||
let chunk_pos = self.terrain().pos_key(pos.0.map(|e| e as i32));
|
||||
@ -517,7 +522,7 @@ impl StateExt for State {
|
||||
#[cfg(feature = "worldgen")]
|
||||
{
|
||||
let time = (*ecs.read_resource::<TimeOfDay>(), (*ecs.read_resource::<Calendar>()).clone());
|
||||
chunk_generator.generate_chunk(None, chunk_key, &slow_jobs, Arc::clone(world), index.clone(), time);
|
||||
chunk_generator.generate_chunk(None, chunk_key, &slow_jobs, Arc::clone(world), &rtsim, index.clone(), time);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ impl<'a> System<'a> for Sys {
|
||||
mut terrain_changes,
|
||||
mut chunk_requests,
|
||||
//mut rtsim,
|
||||
mut rtsim2,
|
||||
mut rtsim,
|
||||
mut _terrain_persistence,
|
||||
mut positions,
|
||||
presences,
|
||||
@ -137,6 +137,7 @@ impl<'a> System<'a> for Sys {
|
||||
request.key,
|
||||
&slow_jobs,
|
||||
Arc::clone(&world),
|
||||
&rtsim,
|
||||
index.clone(),
|
||||
(*time_of_day, calendar.clone()),
|
||||
)
|
||||
@ -182,7 +183,7 @@ impl<'a> System<'a> for Sys {
|
||||
} else {
|
||||
terrain_changes.new_chunks.insert(key);
|
||||
#[cfg(feature = "worldgen")]
|
||||
rtsim2.hook_load_chunk(key);
|
||||
rtsim.hook_load_chunk(key, supplement.rtsim_max_resources);
|
||||
}
|
||||
|
||||
// Handle chunk supplement
|
||||
@ -387,7 +388,7 @@ impl<'a> System<'a> for Sys {
|
||||
terrain.remove(key).map(|chunk| {
|
||||
terrain_changes.removed_chunks.insert(key);
|
||||
#[cfg(feature = "worldgen")]
|
||||
rtsim2.hook_unload_chunk(key);
|
||||
rtsim.hook_unload_chunk(key);
|
||||
chunk
|
||||
})
|
||||
})
|
||||
|
@ -21,6 +21,7 @@ common-dynlib = {package = "veloren-common-dynlib", path = "../common/dynlib", o
|
||||
bincode = "1.3.1"
|
||||
bitvec = "1.0.1"
|
||||
enum-iterator = "1.1.3"
|
||||
enum-map = "2.4"
|
||||
fxhash = "0.2.1"
|
||||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
itertools = "0.10"
|
||||
|
@ -141,6 +141,7 @@ pub struct Canvas<'a> {
|
||||
pub(crate) info: CanvasInfo<'a>,
|
||||
pub(crate) chunk: &'a mut TerrainChunk,
|
||||
pub(crate) entities: Vec<EntityInfo>,
|
||||
pub(crate) rtsim_resource_blocks: Vec<Vec3<i32>>,
|
||||
}
|
||||
|
||||
impl<'a> Canvas<'a> {
|
||||
@ -159,11 +160,20 @@ impl<'a> Canvas<'a> {
|
||||
}
|
||||
|
||||
pub fn set(&mut self, pos: Vec3<i32>, block: Block) {
|
||||
if block.get_rtsim_resource().is_some() {
|
||||
self.rtsim_resource_blocks.push(pos);
|
||||
}
|
||||
let _ = self.chunk.set(pos - self.wpos(), block);
|
||||
}
|
||||
|
||||
pub fn map(&mut self, pos: Vec3<i32>, f: impl FnOnce(Block) -> Block) {
|
||||
let _ = self.chunk.map(pos - self.wpos(), f);
|
||||
let _ = self.chunk.map(pos - self.wpos(), |b| {
|
||||
let new_block = f(b);
|
||||
if new_block.get_rtsim_resource().is_some() {
|
||||
self.rtsim_resource_blocks.push(pos);
|
||||
}
|
||||
new_block
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_sprite_cfg(&mut self, pos: Vec3<i32>, sprite_cfg: SpriteCfg) {
|
||||
|
@ -53,8 +53,10 @@ use common::{
|
||||
Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid,
|
||||
},
|
||||
vol::{ReadVol, RectVolSize, WriteVol},
|
||||
rtsim::ChunkResource,
|
||||
};
|
||||
use common_net::msg::{world_msg, WorldMapMsg};
|
||||
use enum_map::EnumMap;
|
||||
use rand::{prelude::*, Rng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use rayon::iter::ParallelIterator;
|
||||
@ -235,7 +237,7 @@ impl World {
|
||||
// Unwrapping because generate_chunk only returns err when should_continue evals
|
||||
// to true
|
||||
let (tc, _cs) = self
|
||||
.generate_chunk(index, chunk_pos, || false, None)
|
||||
.generate_chunk(index, chunk_pos, None, || false, None)
|
||||
.unwrap();
|
||||
|
||||
tc.find_accessible_pos(spawn_wpos, ascending)
|
||||
@ -246,6 +248,7 @@ impl World {
|
||||
&self,
|
||||
index: IndexRef,
|
||||
chunk_pos: Vec2<i32>,
|
||||
rtsim_resources: Option<EnumMap<ChunkResource, f32>>,
|
||||
// TODO: misleading name
|
||||
mut should_continue: impl FnMut() -> bool,
|
||||
time: Option<(TimeOfDay, Calendar)>,
|
||||
@ -377,6 +380,7 @@ impl World {
|
||||
},
|
||||
chunk: &mut chunk,
|
||||
entities: Vec::new(),
|
||||
rtsim_resource_blocks: Vec::new(),
|
||||
};
|
||||
|
||||
if index.features.train_tracks {
|
||||
@ -416,9 +420,12 @@ impl World {
|
||||
.iter()
|
||||
.for_each(|site| index.sites[*site].apply_to(&mut canvas, &mut dynamic_rng));
|
||||
|
||||
let mut rtsim_resource_blocks = std::mem::take(&mut canvas.rtsim_resource_blocks);
|
||||
let mut supplement = ChunkSupplement {
|
||||
entities: canvas.entities,
|
||||
entities: std::mem::take(&mut canvas.entities),
|
||||
rtsim_max_resources: Default::default(),
|
||||
};
|
||||
drop(canvas);
|
||||
|
||||
let gen_entity_pos = |dynamic_rng: &mut ChaCha8Rng| {
|
||||
let lpos2d = TerrainChunkSize::RECT_SIZE
|
||||
@ -485,6 +492,33 @@ impl World {
|
||||
// Finally, defragment to minimize space consumption.
|
||||
chunk.defragment();
|
||||
|
||||
// Before we finish, we check candidate rtsim resource blocks, deduplicating positions and only keeping those
|
||||
// that actually do have resources. Although this looks potentially very expensive, only blocks that are rtsim
|
||||
// resources (i.e: a relatively small number of sprites) are processed here.
|
||||
if let Some(rtsim_resources) = rtsim_resources {
|
||||
rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array());
|
||||
rtsim_resource_blocks.dedup();
|
||||
for wpos in rtsim_resource_blocks {
|
||||
chunk.map(
|
||||
wpos - chunk_wpos2d.with_z(0),
|
||||
|block| if let Some(res) = block.get_rtsim_resource() {
|
||||
// Note: this represents the upper limit, not the actual number spanwed, so we increment this before deciding whether we're going to spawn the resource.
|
||||
supplement.rtsim_max_resources[res] += 1;
|
||||
// Throw a dice to determine whether this resource should actually spawn
|
||||
// TODO: Don't throw a dice, try to generate the *exact* correct number
|
||||
if dynamic_rng.gen_bool(rtsim_resources[res] as f64) {
|
||||
block
|
||||
} else {
|
||||
block.into_vacant()
|
||||
}
|
||||
} else {
|
||||
block
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok((chunk, supplement))
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user