mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Significantly optimizing terrain::Sys::run.
This commit is contained in:
parent
59d8266f2a
commit
3b424e9049
@ -31,7 +31,7 @@ struct Opt {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let opt = Opt::from_args();
|
let opt = Opt::from_args();
|
||||||
// Start logging
|
// Start logging
|
||||||
common_frontend::init_stdout(None);
|
let _guards = common_frontend::init_stdout(None);
|
||||||
// Run clients and stuff
|
// Run clients and stuff
|
||||||
//
|
//
|
||||||
// NOTE: "swarm0" is assumed to be an admin already
|
// NOTE: "swarm0" is assumed to be an admin already
|
||||||
@ -49,6 +49,8 @@ fn main() {
|
|||||||
|
|
||||||
let finished_init = Arc::new(AtomicU32::new(0));
|
let finished_init = Arc::new(AtomicU32::new(0));
|
||||||
let runtime = Arc::new(Runtime::new().unwrap());
|
let runtime = Arc::new(Runtime::new().unwrap());
|
||||||
|
let mut pools = common_state::State::pools(common::resources::GameMode::Client);
|
||||||
|
pools.0 = 0;
|
||||||
|
|
||||||
// TODO: calculate and log the required chunks per second to maintain the
|
// TODO: calculate and log the required chunks per second to maintain the
|
||||||
// selected scenario with full vd loaded
|
// selected scenario with full vd loaded
|
||||||
@ -58,6 +60,7 @@ fn main() {
|
|||||||
0,
|
0,
|
||||||
to_adminify,
|
to_adminify,
|
||||||
&runtime,
|
&runtime,
|
||||||
|
&pools,
|
||||||
opt,
|
opt,
|
||||||
&finished_init,
|
&finished_init,
|
||||||
);
|
);
|
||||||
@ -68,14 +71,13 @@ fn main() {
|
|||||||
index as u32,
|
index as u32,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
&runtime,
|
&runtime,
|
||||||
|
&pools,
|
||||||
opt,
|
opt,
|
||||||
&finished_init,
|
&finished_init,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
loop {
|
std::thread::park();
|
||||||
thread::sleep(Duration::from_secs_f32(1.0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_client_new_thread(
|
fn run_client_new_thread(
|
||||||
@ -83,13 +85,15 @@ fn run_client_new_thread(
|
|||||||
index: u32,
|
index: u32,
|
||||||
to_adminify: Vec<String>,
|
to_adminify: Vec<String>,
|
||||||
runtime: &Arc<Runtime>,
|
runtime: &Arc<Runtime>,
|
||||||
|
pools: &common_state::Pools,
|
||||||
opt: Opt,
|
opt: Opt,
|
||||||
finished_init: &Arc<AtomicU32>,
|
finished_init: &Arc<AtomicU32>,
|
||||||
) {
|
) {
|
||||||
let runtime = Arc::clone(runtime);
|
let runtime = Arc::clone(runtime);
|
||||||
|
let pools = pools.clone();
|
||||||
let finished_init = Arc::clone(finished_init);
|
let finished_init = Arc::clone(finished_init);
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
if let Err(err) = run_client(username, index, to_adminify, runtime, opt, finished_init) {
|
if let Err(err) = run_client(username, index, to_adminify, pools, runtime, opt, finished_init) {
|
||||||
tracing::error!("swarm member {} exited with an error: {:?}", index, err);
|
tracing::error!("swarm member {} exited with an error: {:?}", index, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -99,29 +103,34 @@ fn run_client(
|
|||||||
username: String,
|
username: String,
|
||||||
index: u32,
|
index: u32,
|
||||||
to_adminify: Vec<String>,
|
to_adminify: Vec<String>,
|
||||||
|
pools: common_state::Pools,
|
||||||
runtime: Arc<Runtime>,
|
runtime: Arc<Runtime>,
|
||||||
opt: Opt,
|
opt: Opt,
|
||||||
finished_init: Arc<AtomicU32>,
|
finished_init: Arc<AtomicU32>,
|
||||||
) -> Result<(), veloren_client::Error> {
|
) -> Result<(), veloren_client::Error> {
|
||||||
// Connect to localhost
|
let mut client = loop {
|
||||||
let addr = ConnectionArgs::Tcp {
|
// Connect to localhost
|
||||||
prefer_ipv6: false,
|
let addr = ConnectionArgs::Tcp {
|
||||||
hostname: "localhost".into(),
|
prefer_ipv6: false,
|
||||||
|
hostname: "localhost".into(),
|
||||||
|
};
|
||||||
|
let runtime_clone = Arc::clone(&runtime);
|
||||||
|
// NOTE: use a no-auth server
|
||||||
|
match runtime
|
||||||
|
.block_on(Client::new(
|
||||||
|
addr,
|
||||||
|
runtime_clone,
|
||||||
|
&mut None,
|
||||||
|
pools.clone(),
|
||||||
|
&username,
|
||||||
|
"",
|
||||||
|
|_| false,
|
||||||
|
)) {
|
||||||
|
Err(e) => tracing::warn!(?e, "Client {} disconnected", index),
|
||||||
|
Ok(client) => break client,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let runtime_clone = Arc::clone(&runtime);
|
drop(pools);
|
||||||
// NOTE: use a no-auth server
|
|
||||||
let mut client = runtime
|
|
||||||
.block_on(Client::new(
|
|
||||||
addr,
|
|
||||||
runtime_clone,
|
|
||||||
&mut None,
|
|
||||||
common_state::State::pools(common::resources::GameMode::Client),
|
|
||||||
&username,
|
|
||||||
"",
|
|
||||||
|_| false,
|
|
||||||
))
|
|
||||||
.expect("Failed to connect to the server");
|
|
||||||
client.set_view_distance(opt.vd);
|
|
||||||
|
|
||||||
let mut clock = common::clock::Clock::new(Duration::from_secs_f32(1.0 / 30.0));
|
let mut clock = common::clock::Clock::new(Duration::from_secs_f32(1.0 / 30.0));
|
||||||
|
|
||||||
@ -165,6 +174,8 @@ fn run_client(
|
|||||||
.expect("Why is this an option?"),
|
.expect("Why is this an option?"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
client.set_view_distance(opt.vd);
|
||||||
|
|
||||||
// If this is the admin client then adminify the other swarm members
|
// If this is the admin client then adminify the other swarm members
|
||||||
if !to_adminify.is_empty() {
|
if !to_adminify.is_empty() {
|
||||||
// Wait for other clients to connect
|
// Wait for other clients to connect
|
||||||
|
@ -390,315 +390,316 @@ impl Client {
|
|||||||
component_recipe_book,
|
component_recipe_book,
|
||||||
max_group_size,
|
max_group_size,
|
||||||
client_timeout,
|
client_timeout,
|
||||||
) = match loop {
|
) = loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = register_stream.recv() => break res?,
|
// Spawn in a blocking thread (leaving the network thread free). This is mostly
|
||||||
|
// useful for bots.
|
||||||
|
res = register_stream.recv() => {
|
||||||
|
let ServerInit::GameSync {
|
||||||
|
entity_package,
|
||||||
|
time_of_day,
|
||||||
|
max_group_size,
|
||||||
|
client_timeout,
|
||||||
|
world_map,
|
||||||
|
recipe_book,
|
||||||
|
component_recipe_book,
|
||||||
|
material_stats,
|
||||||
|
ability_map,
|
||||||
|
} = res?;
|
||||||
|
|
||||||
|
break tokio::task::spawn_blocking(move || {
|
||||||
|
let map_size_lg = common::terrain::MapSizeLg::new(world_map.dimensions_lg)
|
||||||
|
.map_err(|_| {
|
||||||
|
Error::Other(format!(
|
||||||
|
"Server sent bad world map dimensions: {:?}",
|
||||||
|
world_map.dimensions_lg,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let sea_level = world_map.default_chunk.get_min_z() as f32;
|
||||||
|
|
||||||
|
// Initialize `State`
|
||||||
|
let mut state = State::client(pools, map_size_lg, world_map.default_chunk);
|
||||||
|
// Client-only components
|
||||||
|
state.ecs_mut().register::<comp::Last<CharacterState>>();
|
||||||
|
state.ecs_mut().write_resource::<SlowJobPool>()
|
||||||
|
.configure(&"TERRAIN_DROP", |_n| 1);
|
||||||
|
/* state.ecs_mut().write_resource::<SlowJobPool>()
|
||||||
|
.configure("TERRAIN_DESERIALIZING", |n| n / 2); */
|
||||||
|
let entity = state.ecs_mut().apply_entity_package(entity_package);
|
||||||
|
*state.ecs_mut().write_resource() = time_of_day;
|
||||||
|
*state.ecs_mut().write_resource() = PlayerEntity(Some(entity));
|
||||||
|
state.ecs_mut().insert(material_stats);
|
||||||
|
state.ecs_mut().insert(ability_map);
|
||||||
|
|
||||||
|
let map_size = map_size_lg.chunks();
|
||||||
|
let max_height = world_map.max_height;
|
||||||
|
let rgba = world_map.rgba;
|
||||||
|
let alt = world_map.alt;
|
||||||
|
if rgba.size() != map_size.map(|e| e as i32) {
|
||||||
|
return Err(Error::Other("Server sent a bad world map image".into()));
|
||||||
|
}
|
||||||
|
if alt.size() != map_size.map(|e| e as i32) {
|
||||||
|
return Err(Error::Other("Server sent a bad altitude map.".into()));
|
||||||
|
}
|
||||||
|
let [west, east] = world_map.horizons;
|
||||||
|
let scale_angle =
|
||||||
|
|a: u8| (a as f32 / 255.0 * <f32 as FloatConst>::FRAC_PI_2()).tan();
|
||||||
|
let scale_height = |h: u8| h as f32 / 255.0 * max_height;
|
||||||
|
let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
|
||||||
|
|
||||||
|
debug!("Preparing image...");
|
||||||
|
let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| {
|
||||||
|
(
|
||||||
|
angles.iter().copied().map(scale_angle).collect::<Vec<_>>(),
|
||||||
|
heights
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(scale_height)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let horizons = [unzip_horizons(&west), unzip_horizons(&east)];
|
||||||
|
|
||||||
|
// Redraw map (with shadows this time).
|
||||||
|
let mut world_map_rgba = vec![0u32; rgba.size().product() as usize];
|
||||||
|
let mut world_map_topo = vec![0u32; rgba.size().product() as usize];
|
||||||
|
let mut map_config = common::terrain::map::MapConfig::orthographic(
|
||||||
|
map_size_lg,
|
||||||
|
core::ops::RangeInclusive::new(0.0, max_height),
|
||||||
|
);
|
||||||
|
map_config.horizons = Some(&horizons);
|
||||||
|
let rescale_height = |h: f32| h / max_height;
|
||||||
|
let bounds_check = |pos: Vec2<i32>| {
|
||||||
|
pos.reduce_partial_min() >= 0
|
||||||
|
&& pos.x < map_size.x as i32
|
||||||
|
&& pos.y < map_size.y as i32
|
||||||
|
};
|
||||||
|
fn sample_pos(
|
||||||
|
map_config: &MapConfig,
|
||||||
|
pos: Vec2<i32>,
|
||||||
|
alt: &Grid<u32>,
|
||||||
|
rgba: &Grid<u32>,
|
||||||
|
map_size: &Vec2<u16>,
|
||||||
|
map_size_lg: &common::terrain::MapSizeLg,
|
||||||
|
max_height: f32,
|
||||||
|
) -> common::terrain::map::MapSample {
|
||||||
|
let rescale_height = |h: f32| h / max_height;
|
||||||
|
let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
|
||||||
|
let bounds_check = |pos: Vec2<i32>| {
|
||||||
|
pos.reduce_partial_min() >= 0
|
||||||
|
&& pos.x < map_size.x as i32
|
||||||
|
&& pos.y < map_size.y as i32
|
||||||
|
};
|
||||||
|
let MapConfig {
|
||||||
|
gain,
|
||||||
|
is_contours,
|
||||||
|
is_height_map,
|
||||||
|
is_stylized_topo,
|
||||||
|
..
|
||||||
|
} = *map_config;
|
||||||
|
let mut is_contour_line = false;
|
||||||
|
let mut is_border = false;
|
||||||
|
let (rgb, alt, downhill_wpos) = if bounds_check(pos) {
|
||||||
|
let posi = pos.y as usize * map_size.x as usize + pos.x as usize;
|
||||||
|
let [r, g, b, _a] = rgba[pos].to_le_bytes();
|
||||||
|
let is_water = r == 0 && b > 102 && g < 77;
|
||||||
|
let alti = alt[pos];
|
||||||
|
// Compute contours (chunks are assigned in the river code below)
|
||||||
|
let altj = rescale_height(scale_height_big(alti));
|
||||||
|
let contour_interval = 150.0;
|
||||||
|
let chunk_contour = (altj * gain / contour_interval) as u32;
|
||||||
|
|
||||||
|
// Compute downhill.
|
||||||
|
let downhill = {
|
||||||
|
let mut best = -1;
|
||||||
|
let mut besth = alti;
|
||||||
|
for nposi in neighbors(*map_size_lg, posi) {
|
||||||
|
let nbh = alt.raw()[nposi];
|
||||||
|
let nalt = rescale_height(scale_height_big(nbh));
|
||||||
|
let nchunk_contour = (nalt * gain / contour_interval) as u32;
|
||||||
|
if !is_contour_line && chunk_contour > nchunk_contour {
|
||||||
|
is_contour_line = true;
|
||||||
|
}
|
||||||
|
let [nr, ng, nb, _na] = rgba.raw()[nposi].to_le_bytes();
|
||||||
|
let n_is_water = nr == 0 && nb > 102 && ng < 77;
|
||||||
|
|
||||||
|
if !is_border && is_water && !n_is_water {
|
||||||
|
is_border = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if nbh < besth {
|
||||||
|
besth = nbh;
|
||||||
|
best = nposi as isize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
};
|
||||||
|
let downhill_wpos = if downhill < 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Vec2::new(
|
||||||
|
(downhill as usize % map_size.x as usize) as i32,
|
||||||
|
(downhill as usize / map_size.x as usize) as i32,
|
||||||
|
) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
(Rgb::new(r, g, b), alti, downhill_wpos)
|
||||||
|
} else {
|
||||||
|
(Rgb::zero(), 0, None)
|
||||||
|
};
|
||||||
|
let alt = f64::from(rescale_height(scale_height_big(alt)));
|
||||||
|
let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
|
||||||
|
let downhill_wpos = downhill_wpos
|
||||||
|
.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
|
||||||
|
let is_path = rgb.r == 0x37 && rgb.g == 0x29 && rgb.b == 0x23;
|
||||||
|
let rgb = rgb.map(|e: u8| e as f64 / 255.0);
|
||||||
|
let is_water = rgb.r == 0.0 && rgb.b > 0.4 && rgb.g < 0.3;
|
||||||
|
|
||||||
|
let rgb = if is_height_map {
|
||||||
|
if is_path {
|
||||||
|
// Path color is Rgb::new(0x37, 0x29, 0x23)
|
||||||
|
Rgb::new(0.9, 0.9, 0.63)
|
||||||
|
} else if is_water {
|
||||||
|
Rgb::new(0.23, 0.47, 0.53)
|
||||||
|
} else if is_contours && is_contour_line {
|
||||||
|
// Color contour lines
|
||||||
|
Rgb::new(0.15, 0.15, 0.15)
|
||||||
|
} else {
|
||||||
|
// Color hill shading
|
||||||
|
let lightness = (alt + 0.2).min(1.0) as f64;
|
||||||
|
Rgb::new(lightness, 0.9 * lightness, 0.5 * lightness)
|
||||||
|
}
|
||||||
|
} else if is_stylized_topo {
|
||||||
|
if is_path {
|
||||||
|
Rgb::new(0.9, 0.9, 0.63)
|
||||||
|
} else if is_water {
|
||||||
|
if is_border {
|
||||||
|
Rgb::new(0.10, 0.34, 0.50)
|
||||||
|
} else {
|
||||||
|
Rgb::new(0.23, 0.47, 0.63)
|
||||||
|
}
|
||||||
|
} else if is_contour_line {
|
||||||
|
Rgb::new(0.25, 0.25, 0.25)
|
||||||
|
} else {
|
||||||
|
// Stylized colors
|
||||||
|
Rgb::new(
|
||||||
|
(rgb.r + 0.25).min(1.0),
|
||||||
|
(rgb.g + 0.23).min(1.0),
|
||||||
|
(rgb.b + 0.10).min(1.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Rgb::new(rgb.r, rgb.g, rgb.b)
|
||||||
|
}
|
||||||
|
.map(|e| (e * 255.0) as u8);
|
||||||
|
common::terrain::map::MapSample {
|
||||||
|
rgb,
|
||||||
|
alt,
|
||||||
|
downhill_wpos,
|
||||||
|
connections: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generate standard shaded map
|
||||||
|
map_config.is_shaded = true;
|
||||||
|
map_config.generate(
|
||||||
|
|pos| {
|
||||||
|
sample_pos(
|
||||||
|
&map_config,
|
||||||
|
pos,
|
||||||
|
&alt,
|
||||||
|
&rgba,
|
||||||
|
&map_size,
|
||||||
|
&map_size_lg,
|
||||||
|
max_height,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|wpos| {
|
||||||
|
let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32);
|
||||||
|
rescale_height(if bounds_check(pos) {
|
||||||
|
scale_height_big(alt[pos])
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|pos, (r, g, b, a)| {
|
||||||
|
world_map_rgba[pos.y * map_size.x as usize + pos.x] =
|
||||||
|
u32::from_le_bytes([r, g, b, a]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Generate map with topographical lines and stylized colors
|
||||||
|
map_config.is_contours = true;
|
||||||
|
map_config.is_stylized_topo = true;
|
||||||
|
map_config.generate(
|
||||||
|
|pos| {
|
||||||
|
sample_pos(
|
||||||
|
&map_config,
|
||||||
|
pos,
|
||||||
|
&alt,
|
||||||
|
&rgba,
|
||||||
|
&map_size,
|
||||||
|
&map_size_lg,
|
||||||
|
max_height,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|wpos| {
|
||||||
|
let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32);
|
||||||
|
rescale_height(if bounds_check(pos) {
|
||||||
|
scale_height_big(alt[pos])
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|pos, (r, g, b, a)| {
|
||||||
|
world_map_topo[pos.y * map_size.x as usize + pos.x] =
|
||||||
|
u32::from_le_bytes([r, g, b, a]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let make_raw = |rgb| -> Result<_, Error> {
|
||||||
|
let mut raw = vec![0u8; 4 * world_map_rgba.len()];
|
||||||
|
LittleEndian::write_u32_into(rgb, &mut raw);
|
||||||
|
Ok(Arc::new(
|
||||||
|
DynamicImage::ImageRgba8({
|
||||||
|
// Should not fail if the dimensions are correct.
|
||||||
|
let map =
|
||||||
|
image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw);
|
||||||
|
map.ok_or_else(|| Error::Other("Server sent a bad world map image".into()))?
|
||||||
|
})
|
||||||
|
// Flip the image, since Voxygen uses an orientation where rotation from
|
||||||
|
// positive x axis to positive y axis is counterclockwise around the z axis.
|
||||||
|
.flipv(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let lod_base = rgba;
|
||||||
|
let lod_alt = alt;
|
||||||
|
let world_map_rgb_img = make_raw(&world_map_rgba)?;
|
||||||
|
let world_map_topo_img = make_raw(&world_map_topo)?;
|
||||||
|
let world_map_layers = vec![world_map_rgb_img, world_map_topo_img];
|
||||||
|
let horizons = (west.0, west.1, east.0, east.1)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh]))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let lod_horizon = horizons;
|
||||||
|
let map_bounds = Vec2::new(sea_level, max_height);
|
||||||
|
debug!("Done preparing image...");
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
state,
|
||||||
|
lod_base,
|
||||||
|
lod_alt,
|
||||||
|
Grid::from_raw(map_size.map(|e| e as i32), lod_horizon),
|
||||||
|
(world_map_layers, map_size, map_bounds),
|
||||||
|
world_map.sites,
|
||||||
|
world_map.pois,
|
||||||
|
recipe_book,
|
||||||
|
component_recipe_book,
|
||||||
|
max_group_size,
|
||||||
|
client_timeout,
|
||||||
|
))
|
||||||
|
}).await.expect("Client thread should not panic")?;
|
||||||
|
},
|
||||||
_ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
|
_ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
|
||||||
}
|
}
|
||||||
} {
|
|
||||||
ServerInit::GameSync {
|
|
||||||
entity_package,
|
|
||||||
time_of_day,
|
|
||||||
max_group_size,
|
|
||||||
client_timeout,
|
|
||||||
world_map,
|
|
||||||
recipe_book,
|
|
||||||
component_recipe_book,
|
|
||||||
material_stats,
|
|
||||||
ability_map,
|
|
||||||
} => {
|
|
||||||
// Initialize `State`
|
|
||||||
let mut state = State::client(pools);
|
|
||||||
// Client-only components
|
|
||||||
state.ecs_mut().register::<comp::Last<CharacterState>>();
|
|
||||||
state.ecs_mut().write_resource::<SlowJobPool>()
|
|
||||||
.configure(&"TERRAIN_DROP", |_n| 1);
|
|
||||||
/* state.ecs_mut().write_resource::<SlowJobPool>()
|
|
||||||
.configure("TERRAIN_DESERIALIZING", |n| n / 2); */
|
|
||||||
let entity = state.ecs_mut().apply_entity_package(entity_package);
|
|
||||||
*state.ecs_mut().write_resource() = time_of_day;
|
|
||||||
*state.ecs_mut().write_resource() = PlayerEntity(Some(entity));
|
|
||||||
state.ecs_mut().insert(material_stats);
|
|
||||||
state.ecs_mut().insert(ability_map);
|
|
||||||
|
|
||||||
let map_size_lg = common::terrain::MapSizeLg::new(world_map.dimensions_lg)
|
|
||||||
.map_err(|_| {
|
|
||||||
Error::Other(format!(
|
|
||||||
"Server sent bad world map dimensions: {:?}",
|
|
||||||
world_map.dimensions_lg,
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let map_size = map_size_lg.chunks();
|
|
||||||
let max_height = world_map.max_height;
|
|
||||||
let sea_level = world_map.sea_level;
|
|
||||||
let rgba = world_map.rgba;
|
|
||||||
let alt = world_map.alt;
|
|
||||||
if rgba.size() != map_size.map(|e| e as i32) {
|
|
||||||
return Err(Error::Other("Server sent a bad world map image".into()));
|
|
||||||
}
|
|
||||||
if alt.size() != map_size.map(|e| e as i32) {
|
|
||||||
return Err(Error::Other("Server sent a bad altitude map.".into()));
|
|
||||||
}
|
|
||||||
let [west, east] = world_map.horizons;
|
|
||||||
let scale_angle =
|
|
||||||
|a: u8| (a as f32 / 255.0 * <f32 as FloatConst>::FRAC_PI_2()).tan();
|
|
||||||
let scale_height = |h: u8| h as f32 / 255.0 * max_height;
|
|
||||||
let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
|
|
||||||
ping_stream.send(PingMsg::Ping)?;
|
|
||||||
|
|
||||||
debug!("Preparing image...");
|
|
||||||
let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| {
|
|
||||||
(
|
|
||||||
angles.iter().copied().map(scale_angle).collect::<Vec<_>>(),
|
|
||||||
heights
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.map(scale_height)
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let horizons = [unzip_horizons(&west), unzip_horizons(&east)];
|
|
||||||
|
|
||||||
// Redraw map (with shadows this time).
|
|
||||||
let mut world_map_rgba = vec![0u32; rgba.size().product() as usize];
|
|
||||||
let mut world_map_topo = vec![0u32; rgba.size().product() as usize];
|
|
||||||
let mut map_config = common::terrain::map::MapConfig::orthographic(
|
|
||||||
map_size_lg,
|
|
||||||
core::ops::RangeInclusive::new(0.0, max_height),
|
|
||||||
);
|
|
||||||
map_config.horizons = Some(&horizons);
|
|
||||||
let rescale_height = |h: f32| h / max_height;
|
|
||||||
let bounds_check = |pos: Vec2<i32>| {
|
|
||||||
pos.reduce_partial_min() >= 0
|
|
||||||
&& pos.x < map_size.x as i32
|
|
||||||
&& pos.y < map_size.y as i32
|
|
||||||
};
|
|
||||||
ping_stream.send(PingMsg::Ping)?;
|
|
||||||
fn sample_pos(
|
|
||||||
map_config: &MapConfig,
|
|
||||||
pos: Vec2<i32>,
|
|
||||||
alt: &Grid<u32>,
|
|
||||||
rgba: &Grid<u32>,
|
|
||||||
map_size: &Vec2<u16>,
|
|
||||||
map_size_lg: &common::terrain::MapSizeLg,
|
|
||||||
max_height: f32,
|
|
||||||
) -> common::terrain::map::MapSample {
|
|
||||||
let rescale_height = |h: f32| h / max_height;
|
|
||||||
let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
|
|
||||||
let bounds_check = |pos: Vec2<i32>| {
|
|
||||||
pos.reduce_partial_min() >= 0
|
|
||||||
&& pos.x < map_size.x as i32
|
|
||||||
&& pos.y < map_size.y as i32
|
|
||||||
};
|
|
||||||
let MapConfig {
|
|
||||||
gain,
|
|
||||||
is_contours,
|
|
||||||
is_height_map,
|
|
||||||
is_stylized_topo,
|
|
||||||
..
|
|
||||||
} = *map_config;
|
|
||||||
let mut is_contour_line = false;
|
|
||||||
let mut is_border = false;
|
|
||||||
let (rgb, alt, downhill_wpos) = if bounds_check(pos) {
|
|
||||||
let posi = pos.y as usize * map_size.x as usize + pos.x as usize;
|
|
||||||
let [r, g, b, _a] = rgba[pos].to_le_bytes();
|
|
||||||
let is_water = r == 0 && b > 102 && g < 77;
|
|
||||||
let alti = alt[pos];
|
|
||||||
// Compute contours (chunks are assigned in the river code below)
|
|
||||||
let altj = rescale_height(scale_height_big(alti));
|
|
||||||
let contour_interval = 150.0;
|
|
||||||
let chunk_contour = (altj * gain / contour_interval) as u32;
|
|
||||||
|
|
||||||
// Compute downhill.
|
|
||||||
let downhill = {
|
|
||||||
let mut best = -1;
|
|
||||||
let mut besth = alti;
|
|
||||||
for nposi in neighbors(*map_size_lg, posi) {
|
|
||||||
let nbh = alt.raw()[nposi];
|
|
||||||
let nalt = rescale_height(scale_height_big(nbh));
|
|
||||||
let nchunk_contour = (nalt * gain / contour_interval) as u32;
|
|
||||||
if !is_contour_line && chunk_contour > nchunk_contour {
|
|
||||||
is_contour_line = true;
|
|
||||||
}
|
|
||||||
let [nr, ng, nb, _na] = rgba.raw()[nposi].to_le_bytes();
|
|
||||||
let n_is_water = nr == 0 && nb > 102 && ng < 77;
|
|
||||||
|
|
||||||
if !is_border && is_water && !n_is_water {
|
|
||||||
is_border = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if nbh < besth {
|
|
||||||
besth = nbh;
|
|
||||||
best = nposi as isize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
best
|
|
||||||
};
|
|
||||||
let downhill_wpos = if downhill < 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
Vec2::new(
|
|
||||||
(downhill as usize % map_size.x as usize) as i32,
|
|
||||||
(downhill as usize / map_size.x as usize) as i32,
|
|
||||||
) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
(Rgb::new(r, g, b), alti, downhill_wpos)
|
|
||||||
} else {
|
|
||||||
(Rgb::zero(), 0, None)
|
|
||||||
};
|
|
||||||
let alt = f64::from(rescale_height(scale_height_big(alt)));
|
|
||||||
let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
|
|
||||||
let downhill_wpos = downhill_wpos
|
|
||||||
.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
|
|
||||||
let is_path = rgb.r == 0x37 && rgb.g == 0x29 && rgb.b == 0x23;
|
|
||||||
let rgb = rgb.map(|e: u8| e as f64 / 255.0);
|
|
||||||
let is_water = rgb.r == 0.0 && rgb.b > 0.4 && rgb.g < 0.3;
|
|
||||||
|
|
||||||
let rgb = if is_height_map {
|
|
||||||
if is_path {
|
|
||||||
// Path color is Rgb::new(0x37, 0x29, 0x23)
|
|
||||||
Rgb::new(0.9, 0.9, 0.63)
|
|
||||||
} else if is_water {
|
|
||||||
Rgb::new(0.23, 0.47, 0.53)
|
|
||||||
} else if is_contours && is_contour_line {
|
|
||||||
// Color contour lines
|
|
||||||
Rgb::new(0.15, 0.15, 0.15)
|
|
||||||
} else {
|
|
||||||
// Color hill shading
|
|
||||||
let lightness = (alt + 0.2).min(1.0) as f64;
|
|
||||||
Rgb::new(lightness, 0.9 * lightness, 0.5 * lightness)
|
|
||||||
}
|
|
||||||
} else if is_stylized_topo {
|
|
||||||
if is_path {
|
|
||||||
Rgb::new(0.9, 0.9, 0.63)
|
|
||||||
} else if is_water {
|
|
||||||
if is_border {
|
|
||||||
Rgb::new(0.10, 0.34, 0.50)
|
|
||||||
} else {
|
|
||||||
Rgb::new(0.23, 0.47, 0.63)
|
|
||||||
}
|
|
||||||
} else if is_contour_line {
|
|
||||||
Rgb::new(0.25, 0.25, 0.25)
|
|
||||||
} else {
|
|
||||||
// Stylized colors
|
|
||||||
Rgb::new(
|
|
||||||
(rgb.r + 0.25).min(1.0),
|
|
||||||
(rgb.g + 0.23).min(1.0),
|
|
||||||
(rgb.b + 0.10).min(1.0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Rgb::new(rgb.r, rgb.g, rgb.b)
|
|
||||||
}
|
|
||||||
.map(|e| (e * 255.0) as u8);
|
|
||||||
common::terrain::map::MapSample {
|
|
||||||
rgb,
|
|
||||||
alt,
|
|
||||||
downhill_wpos,
|
|
||||||
connections: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Generate standard shaded map
|
|
||||||
map_config.is_shaded = true;
|
|
||||||
map_config.generate(
|
|
||||||
|pos| {
|
|
||||||
sample_pos(
|
|
||||||
&map_config,
|
|
||||||
pos,
|
|
||||||
&alt,
|
|
||||||
&rgba,
|
|
||||||
&map_size,
|
|
||||||
&map_size_lg,
|
|
||||||
max_height,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|wpos| {
|
|
||||||
let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32);
|
|
||||||
rescale_height(if bounds_check(pos) {
|
|
||||||
scale_height_big(alt[pos])
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|pos, (r, g, b, a)| {
|
|
||||||
world_map_rgba[pos.y * map_size.x as usize + pos.x] =
|
|
||||||
u32::from_le_bytes([r, g, b, a]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Generate map with topographical lines and stylized colors
|
|
||||||
map_config.is_contours = true;
|
|
||||||
map_config.is_stylized_topo = true;
|
|
||||||
map_config.generate(
|
|
||||||
|pos| {
|
|
||||||
sample_pos(
|
|
||||||
&map_config,
|
|
||||||
pos,
|
|
||||||
&alt,
|
|
||||||
&rgba,
|
|
||||||
&map_size,
|
|
||||||
&map_size_lg,
|
|
||||||
max_height,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|wpos| {
|
|
||||||
let pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32);
|
|
||||||
rescale_height(if bounds_check(pos) {
|
|
||||||
scale_height_big(alt[pos])
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|pos, (r, g, b, a)| {
|
|
||||||
world_map_topo[pos.y * map_size.x as usize + pos.x] =
|
|
||||||
u32::from_le_bytes([r, g, b, a]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ping_stream.send(PingMsg::Ping)?;
|
|
||||||
let make_raw = |rgb| -> Result<_, Error> {
|
|
||||||
let mut raw = vec![0u8; 4 * world_map_rgba.len()];
|
|
||||||
LittleEndian::write_u32_into(rgb, &mut raw);
|
|
||||||
Ok(Arc::new(
|
|
||||||
DynamicImage::ImageRgba8({
|
|
||||||
// Should not fail if the dimensions are correct.
|
|
||||||
let map =
|
|
||||||
image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw);
|
|
||||||
map.ok_or_else(|| Error::Other("Server sent a bad world map image".into()))?
|
|
||||||
})
|
|
||||||
// Flip the image, since Voxygen uses an orientation where rotation from
|
|
||||||
// positive x axis to positive y axis is counterclockwise around the z axis.
|
|
||||||
.flipv(),
|
|
||||||
))
|
|
||||||
};
|
|
||||||
ping_stream.send(PingMsg::Ping)?;
|
|
||||||
let lod_base = rgba;
|
|
||||||
let lod_alt = alt;
|
|
||||||
let world_map_rgb_img = make_raw(&world_map_rgba)?;
|
|
||||||
let world_map_topo_img = make_raw(&world_map_topo)?;
|
|
||||||
let world_map_layers = vec![world_map_rgb_img, world_map_topo_img];
|
|
||||||
let horizons = (west.0, west.1, east.0, east.1)
|
|
||||||
.into_par_iter()
|
|
||||||
.map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh]))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let lod_horizon = horizons;
|
|
||||||
let map_bounds = Vec2::new(sea_level, max_height);
|
|
||||||
debug!("Done preparing image...");
|
|
||||||
|
|
||||||
(
|
|
||||||
state,
|
|
||||||
lod_base,
|
|
||||||
lod_alt,
|
|
||||||
Grid::from_raw(map_size.map(|e| e as i32), lod_horizon),
|
|
||||||
(world_map_layers, map_size, map_bounds),
|
|
||||||
world_map.sites,
|
|
||||||
world_map.pois,
|
|
||||||
recipe_book,
|
|
||||||
component_recipe_book,
|
|
||||||
max_group_size,
|
|
||||||
client_timeout,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
ping_stream.send(PingMsg::Ping)?;
|
ping_stream.send(PingMsg::Ping)?;
|
||||||
|
|
||||||
@ -1894,7 +1895,20 @@ impl Client {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for key in keys.iter() {
|
for key in keys.iter() {
|
||||||
if self.state.terrain().get_key(*key).is_none() {
|
let dist_to_player =
|
||||||
|
(self.state.terrain().key_pos(*key).map(|x| x as f32)
|
||||||
|
+ TerrainChunkSize::RECT_SIZE.map(|x| x as f32) / 2.0)
|
||||||
|
.distance_squared(pos.0.into());
|
||||||
|
|
||||||
|
let mut terrain = self.state.terrain();
|
||||||
|
if let Some(chunk) = terrain.get_key_arc(*key) {
|
||||||
|
if !skip_mode && !terrain.contains_key_real(*key) {
|
||||||
|
let chunk = Arc::clone(chunk);
|
||||||
|
drop(terrain);
|
||||||
|
self.state.insert_chunk(*key, chunk);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
drop(terrain);
|
||||||
if !skip_mode && !self.pending_chunks.contains_key(key) {
|
if !skip_mode && !self.pending_chunks.contains_key(key) {
|
||||||
const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 8 * 4;
|
const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 8 * 4;
|
||||||
if self.pending_chunks.len() < TOTAL_PENDING_CHUNKS_LIMIT
|
if self.pending_chunks.len() < TOTAL_PENDING_CHUNKS_LIMIT
|
||||||
@ -1911,11 +1925,6 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dist_to_player =
|
|
||||||
(self.state.terrain().key_pos(*key).map(|x| x as f32)
|
|
||||||
+ TerrainChunkSize::RECT_SIZE.map(|x| x as f32) / 2.0)
|
|
||||||
.distance_squared(pos.0.into());
|
|
||||||
|
|
||||||
if dist_to_player < self.loaded_distance {
|
if dist_to_player < self.loaded_distance {
|
||||||
self.loaded_distance = dist_to_player;
|
self.loaded_distance = dist_to_player;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use common::{grid::Grid, trade::Good};
|
use common::{grid::Grid, terrain::TerrainChunk, trade::Good};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, Bytes};
|
use serde_with::{serde_as, Bytes};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
@ -28,8 +29,6 @@ pub struct WorldMapMsg {
|
|||||||
///
|
///
|
||||||
/// NOTE: Invariant: chunk count fits in a u16.
|
/// NOTE: Invariant: chunk count fits in a u16.
|
||||||
pub dimensions_lg: Vec2<u32>,
|
pub dimensions_lg: Vec2<u32>,
|
||||||
/// Sea level (used to provide a base altitude).
|
|
||||||
pub sea_level: f32,
|
|
||||||
/// Max height (used to scale altitudes).
|
/// Max height (used to scale altitudes).
|
||||||
pub max_height: f32,
|
pub max_height: f32,
|
||||||
/// RGB+A; the alpha channel is currently unused, but will be used in the
|
/// RGB+A; the alpha channel is currently unused, but will be used in the
|
||||||
@ -127,6 +126,9 @@ pub struct WorldMapMsg {
|
|||||||
pub horizons: [(Vec<u8>, Vec<u8>); 2],
|
pub horizons: [(Vec<u8>, Vec<u8>); 2],
|
||||||
pub sites: Vec<SiteInfo>,
|
pub sites: Vec<SiteInfo>,
|
||||||
pub pois: Vec<PoiInfo>,
|
pub pois: Vec<PoiInfo>,
|
||||||
|
/// Default chunk (representing the ocean outside the map bounds). Sea level (used to provide
|
||||||
|
/// a base altitude) is the lower bound of this chunk.
|
||||||
|
pub default_chunk: Arc<TerrainChunk>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SiteId = common::trade::SiteId;
|
pub type SiteId = common::trade::SiteId;
|
||||||
|
@ -132,7 +132,7 @@ pub const MAX_WORLD_BLOCKS_LG: Vec2<u32> = Vec2 { x: 19, y: 19 };
|
|||||||
/// [TERRAIN_CHUNK_BLOCKS_LG]))` fits in an i32 (derived from the invariant
|
/// [TERRAIN_CHUNK_BLOCKS_LG]))` fits in an i32 (derived from the invariant
|
||||||
/// on [MAX_WORLD_BLOCKS_LG]).
|
/// on [MAX_WORLD_BLOCKS_LG]).
|
||||||
///
|
///
|
||||||
/// NOTE: As an invariant, each dimension (in chunks) must fit in a u16.
|
/// NOTE: As an invariant, each dimension (in chunks) must fit in a i16.
|
||||||
///
|
///
|
||||||
/// NOTE: As an invariant, the product of dimensions (in chunks) must fit in a
|
/// NOTE: As an invariant, the product of dimensions (in chunks) must fit in a
|
||||||
/// usize.
|
/// usize.
|
||||||
@ -160,12 +160,12 @@ impl MapSizeLg {
|
|||||||
// 0 and ([MAX_WORLD_BLOCKS_LG] - [TERRAIN_CHUNK_BLOCKS_LG])
|
// 0 and ([MAX_WORLD_BLOCKS_LG] - [TERRAIN_CHUNK_BLOCKS_LG])
|
||||||
let is_le_max = map_size_lg.x <= MAX_WORLD_BLOCKS_LG.x - TERRAIN_CHUNK_BLOCKS_LG
|
let is_le_max = map_size_lg.x <= MAX_WORLD_BLOCKS_LG.x - TERRAIN_CHUNK_BLOCKS_LG
|
||||||
&& map_size_lg.y <= MAX_WORLD_BLOCKS_LG.y - TERRAIN_CHUNK_BLOCKS_LG;
|
&& map_size_lg.y <= MAX_WORLD_BLOCKS_LG.y - TERRAIN_CHUNK_BLOCKS_LG;
|
||||||
// Assertion on dimensions: chunks must fit in a u16.
|
// Assertion on dimensions: chunks must fit in a i16.
|
||||||
let chunks_in_range =
|
let chunks_in_range =
|
||||||
/* 1u16.checked_shl(map_size_lg.x).is_some() &&
|
/* 1u15.checked_shl(map_size_lg.x).is_some() &&
|
||||||
1u16.checked_shl(map_size_lg.y).is_some(); */
|
1u15.checked_shl(map_size_lg.y).is_some(); */
|
||||||
map_size_lg.x <= 16 &&
|
map_size_lg.x <= 15 &&
|
||||||
map_size_lg.y <= 16;
|
map_size_lg.y <= 15;
|
||||||
if is_le_max && chunks_in_range {
|
if is_le_max && chunks_in_range {
|
||||||
// Assertion on dimensions: blocks must fit in a i32.
|
// Assertion on dimensions: blocks must fit in a i32.
|
||||||
let blocks_in_range =
|
let blocks_in_range =
|
||||||
@ -197,6 +197,15 @@ impl MapSizeLg {
|
|||||||
|
|
||||||
/// Get the size of an array of the correct size to hold all chunks.
|
/// Get the size of an array of the correct size to hold all chunks.
|
||||||
pub const fn chunks_len(self) -> usize { 1 << (self.0.x + self.0.y) }
|
pub const fn chunks_len(self) -> usize { 1 << (self.0.x + self.0.y) }
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
/// Determine whether a chunk position is in bounds.
|
||||||
|
pub const fn contains_chunk(&self, chunk_key: Vec2<i32>) -> bool {
|
||||||
|
let map_size = self.chunks();
|
||||||
|
chunk_key.x >= 0 && chunk_key.y >= 0 &&
|
||||||
|
chunk_key.x == chunk_key.x & ((map_size.x as i32) - 1) &&
|
||||||
|
chunk_key.y == chunk_key.y & ((map_size.y as i32) - 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MapSizeLg> for Vec2<u32> {
|
impl From<MapSizeLg> for Vec2<u32> {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
terrain::MapSizeLg,
|
||||||
vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol, WriteVol},
|
vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol, WriteVol},
|
||||||
volumes::dyna::DynaError,
|
volumes::dyna::DynaError,
|
||||||
};
|
};
|
||||||
@ -19,6 +20,10 @@ pub enum VolGrid2dError<V: RectRasterableVol> {
|
|||||||
// M = Chunk metadata
|
// M = Chunk metadata
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct VolGrid2d<V: RectRasterableVol> {
|
pub struct VolGrid2d<V: RectRasterableVol> {
|
||||||
|
/// Size of the entire (not just loaded) map.
|
||||||
|
map_size_lg: MapSizeLg,
|
||||||
|
/// Default voxel for use outside of max map bounds.
|
||||||
|
default: Arc<V>,
|
||||||
chunks: HashMap<Vec2<i32>, Arc<V>>,
|
chunks: HashMap<Vec2<i32>, Arc<V>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +57,7 @@ impl<V: RectRasterableVol + ReadVol + Debug> ReadVol for VolGrid2d<V> {
|
|||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn get(&self, pos: Vec3<i32>) -> Result<&V::Vox, VolGrid2dError<V>> {
|
fn get(&self, pos: Vec3<i32>) -> Result<&V::Vox, VolGrid2dError<V>> {
|
||||||
let ck = Self::chunk_key(pos);
|
let ck = Self::chunk_key(pos);
|
||||||
self.chunks
|
self.get_key(ck)
|
||||||
.get(&ck)
|
|
||||||
.ok_or(VolGrid2dError::NoSuchChunk)
|
.ok_or(VolGrid2dError::NoSuchChunk)
|
||||||
.and_then(|chunk| {
|
.and_then(|chunk| {
|
||||||
let co = Self::chunk_offs(pos);
|
let co = Self::chunk_offs(pos);
|
||||||
@ -109,14 +113,14 @@ impl<I: Into<Aabr<i32>>, V: RectRasterableVol + ReadVol + Debug> SampleVol<I> fo
|
|||||||
fn sample(&self, range: I) -> Result<Self::Sample, VolGrid2dError<V>> {
|
fn sample(&self, range: I) -> Result<Self::Sample, VolGrid2dError<V>> {
|
||||||
let range = range.into();
|
let range = range.into();
|
||||||
|
|
||||||
let mut sample = VolGrid2d::new()?;
|
let mut sample = VolGrid2d::new(self.map_size_lg, Arc::clone(&self.default))?;
|
||||||
let chunk_min = Self::chunk_key(range.min);
|
let chunk_min = Self::chunk_key(range.min);
|
||||||
let chunk_max = Self::chunk_key(range.max);
|
let chunk_max = Self::chunk_key(range.max);
|
||||||
for x in chunk_min.x..chunk_max.x + 1 {
|
for x in chunk_min.x..chunk_max.x + 1 {
|
||||||
for y in chunk_min.y..chunk_max.y + 1 {
|
for y in chunk_min.y..chunk_max.y + 1 {
|
||||||
let chunk_key = Vec2::new(x, y);
|
let chunk_key = Vec2::new(x, y);
|
||||||
|
|
||||||
let chunk = self.get_key_arc(chunk_key).cloned();
|
let chunk = self.get_key_arc_real(chunk_key).cloned();
|
||||||
|
|
||||||
if let Some(chunk) = chunk {
|
if let Some(chunk) = chunk {
|
||||||
sample.insert(chunk_key, chunk);
|
sample.insert(chunk_key, chunk);
|
||||||
@ -145,12 +149,14 @@ impl<V: RectRasterableVol + WriteVol + Clone + Debug> WriteVol for VolGrid2d<V>
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<V: RectRasterableVol> VolGrid2d<V> {
|
impl<V: RectRasterableVol> VolGrid2d<V> {
|
||||||
pub fn new() -> Result<Self, VolGrid2dError<V>> {
|
pub fn new(map_size_lg: MapSizeLg, default: Arc<V>) -> Result<Self, VolGrid2dError<V>> {
|
||||||
if Self::chunk_size()
|
if Self::chunk_size()
|
||||||
.map(|e| e.is_power_of_two() && e > 0)
|
.map(|e| e.is_power_of_two() && e > 0)
|
||||||
.reduce_and()
|
.reduce_and()
|
||||||
{
|
{
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
map_size_lg,
|
||||||
|
default,
|
||||||
chunks: HashMap::default(),
|
chunks: HashMap::default(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -167,15 +173,37 @@ impl<V: RectRasterableVol> VolGrid2d<V> {
|
|||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn get_key(&self, key: Vec2<i32>) -> Option<&V> {
|
pub fn get_key(&self, key: Vec2<i32>) -> Option<&V> {
|
||||||
self.chunks.get(&key).map(|arc_chunk| arc_chunk.as_ref())
|
self.get_key_arc(key).map(|arc_chunk| arc_chunk.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_key_real(&self, key: Vec2<i32>) -> Option<&V> {
|
||||||
|
self.get_key_arc_real(key).map(|arc_chunk| arc_chunk.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn contains_key(&self, key: Vec2<i32>) -> bool {
|
pub fn contains_key(&self, key: Vec2<i32>) -> bool {
|
||||||
|
self.contains_key_real(key) ||
|
||||||
|
// Counterintuitively, areas outside the map are *always* considered to be in it, since
|
||||||
|
// they're assigned the default chunk.
|
||||||
|
!self.map_size_lg.contains_chunk(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn contains_key_real(&self, key: Vec2<i32>) -> bool {
|
||||||
self.chunks.contains_key(&key)
|
self.chunks.contains_key(&key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_key_arc(&self, key: Vec2<i32>) -> Option<&Arc<V>> { self.chunks.get(&key) }
|
#[inline(always)]
|
||||||
|
pub fn get_key_arc(&self, key: Vec2<i32>) -> Option<&Arc<V>> {
|
||||||
|
self.get_key_arc_real(key)
|
||||||
|
.or_else(|| if !self.map_size_lg.contains_chunk(key) { Some(&self.default) } else { None })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_key_arc_real(&self, key: Vec2<i32>) -> Option<&Arc<V>> {
|
||||||
|
self.chunks.get(&key)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) { self.chunks.clear(); }
|
pub fn clear(&mut self) { self.chunks.clear(); }
|
||||||
|
|
||||||
@ -231,8 +259,7 @@ impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> {
|
|||||||
// Otherwise retrieve from the hashmap
|
// Otherwise retrieve from the hashmap
|
||||||
let chunk = self
|
let chunk = self
|
||||||
.vol_grid_2d
|
.vol_grid_2d
|
||||||
.chunks
|
.get_key_arc(ck)
|
||||||
.get(&ck)
|
|
||||||
.ok_or(VolGrid2dError::NoSuchChunk)?;
|
.ok_or(VolGrid2dError::NoSuchChunk)?;
|
||||||
// Store most recently looked up chunk in the cache
|
// Store most recently looked up chunk in the cache
|
||||||
self.cache = Some((ck, Arc::clone(chunk)));
|
self.cache = Some((ck, Arc::clone(chunk)));
|
||||||
|
@ -17,7 +17,7 @@ use common::{
|
|||||||
TimeOfDay,
|
TimeOfDay,
|
||||||
},
|
},
|
||||||
slowjob::{self, SlowJobPool},
|
slowjob::{self, SlowJobPool},
|
||||||
terrain::{Block, TerrainChunk, TerrainGrid},
|
terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid},
|
||||||
time::DayPeriod,
|
time::DayPeriod,
|
||||||
trade::Trades,
|
trade::Trades,
|
||||||
vol::{ReadVol, WriteVol},
|
vol::{ReadVol, WriteVol},
|
||||||
@ -179,12 +179,16 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new `State` in client mode.
|
/// Create a new `State` in client mode.
|
||||||
pub fn client(pools: Pools) -> Self { Self::new(GameMode::Client, pools) }
|
pub fn client(pools: Pools, map_size_lg: MapSizeLg, default_chunk: Arc<TerrainChunk>) -> Self {
|
||||||
|
Self::new(GameMode::Client, pools, map_size_lg, default_chunk)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new `State` in server mode.
|
/// Create a new `State` in server mode.
|
||||||
pub fn server(pools: Pools) -> Self { Self::new(GameMode::Server, pools) }
|
pub fn server(pools: Pools, map_size_lg: MapSizeLg, default_chunk: Arc<TerrainChunk>) -> Self {
|
||||||
|
Self::new(GameMode::Server, pools, map_size_lg, default_chunk)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(ecs_role: GameMode, pools: Pools) -> Self {
|
pub fn new(ecs_role: GameMode, pools: Pools, map_size_lg: MapSizeLg, default_chunk: Arc<TerrainChunk>) -> Self {
|
||||||
/* let thread_name_infix = match game_mode {
|
/* let thread_name_infix = match game_mode {
|
||||||
GameMode::Server => "s",
|
GameMode::Server => "s",
|
||||||
GameMode::Client => "c",
|
GameMode::Client => "c",
|
||||||
@ -243,7 +247,7 @@ impl State {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ecs: Self::setup_ecs_world(ecs_role, /*num_cpu as u64*//*, &thread_pool, *//*pools.1*/slowjob/*pools.3*/),
|
ecs: Self::setup_ecs_world(ecs_role, /*num_cpu as u64*//*, &thread_pool, *//*pools.1*/slowjob/*pools.3*/, map_size_lg, default_chunk),
|
||||||
thread_pool: pools.2,
|
thread_pool: pools.2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,7 +255,12 @@ impl State {
|
|||||||
/// Creates ecs world and registers all the common components and resources
|
/// Creates ecs world and registers all the common components and resources
|
||||||
// TODO: Split up registering into server and client (e.g. move
|
// TODO: Split up registering into server and client (e.g. move
|
||||||
// EventBus<ServerEvent> to the server)
|
// EventBus<ServerEvent> to the server)
|
||||||
fn setup_ecs_world(ecs_role: GameMode, /*num_cpu: u64*//*, thread_pool: &Arc<ThreadPool>, */slowjob: SlowJobPool) -> specs::World {
|
fn setup_ecs_world(
|
||||||
|
ecs_role: GameMode, /*num_cpu: u64*//*, thread_pool: &Arc<ThreadPool>, */
|
||||||
|
slowjob: SlowJobPool,
|
||||||
|
map_size_lg: MapSizeLg,
|
||||||
|
default_chunk: Arc<TerrainChunk>,
|
||||||
|
) -> specs::World {
|
||||||
let mut ecs = specs::World::new();
|
let mut ecs = specs::World::new();
|
||||||
// Uids for sync
|
// Uids for sync
|
||||||
ecs.register_sync_marker();
|
ecs.register_sync_marker();
|
||||||
@ -340,7 +349,7 @@ impl State {
|
|||||||
ecs.insert(Time(0.0));
|
ecs.insert(Time(0.0));
|
||||||
ecs.insert(DeltaTime(0.0));
|
ecs.insert(DeltaTime(0.0));
|
||||||
ecs.insert(PlayerEntity(None));
|
ecs.insert(PlayerEntity(None));
|
||||||
ecs.insert(TerrainGrid::new().unwrap());
|
ecs.insert(TerrainGrid::new(map_size_lg, default_chunk).unwrap());
|
||||||
ecs.insert(BlockChange::default());
|
ecs.insert(BlockChange::default());
|
||||||
ecs.insert(crate::build_areas::BuildAreas::default());
|
ecs.insert(crate::build_areas::BuildAreas::default());
|
||||||
ecs.insert(TerrainChanges::default());
|
ecs.insert(TerrainChanges::default());
|
||||||
|
@ -632,8 +632,7 @@ impl<'a> PhysicsData<'a> {
|
|||||||
)| {
|
)| {
|
||||||
let in_loaded_chunk = read
|
let in_loaded_chunk = read
|
||||||
.terrain
|
.terrain
|
||||||
.get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)))
|
.contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)));
|
||||||
.is_some();
|
|
||||||
|
|
||||||
// Apply physics only if in a loaded chunk
|
// Apply physics only if in a loaded chunk
|
||||||
if in_loaded_chunk
|
if in_loaded_chunk
|
||||||
@ -790,8 +789,7 @@ impl<'a> PhysicsData<'a> {
|
|||||||
|
|
||||||
let in_loaded_chunk = read
|
let in_loaded_chunk = read
|
||||||
.terrain
|
.terrain
|
||||||
.get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)))
|
.contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)));
|
||||||
.is_some();
|
|
||||||
|
|
||||||
// Don't move if we're not in a loaded chunk
|
// Don't move if we're not in a loaded chunk
|
||||||
let pos_delta = if in_loaded_chunk {
|
let pos_delta = if in_loaded_chunk {
|
||||||
|
@ -29,7 +29,15 @@ pub fn setup() -> State {
|
|||||||
state.ecs_mut().insert(MaterialStatManifest::with_empty());
|
state.ecs_mut().insert(MaterialStatManifest::with_empty());
|
||||||
state.ecs_mut().read_resource::<Time>();
|
state.ecs_mut().read_resource::<Time>();
|
||||||
state.ecs_mut().read_resource::<DeltaTime>();
|
state.ecs_mut().read_resource::<DeltaTime>();
|
||||||
state.ecs_mut().insert(TerrainGrid::new());
|
state.ecs_mut().insert(TerrainGrid::new(
|
||||||
|
DEFAULT_MAP_SIZE_LG,
|
||||||
|
TerrainChunk::new(
|
||||||
|
0,
|
||||||
|
Block::new(BlockKind::Water, Rgb::zero()),
|
||||||
|
Block::air(SpriteKind::Empty),
|
||||||
|
TerrainChunkMeta::void(),
|
||||||
|
)
|
||||||
|
));
|
||||||
for x in 0..2 {
|
for x in 0..2 {
|
||||||
for y in 0..2 {
|
for y in 0..2 {
|
||||||
generate_chunk(&mut state, Vec2::new(x, y));
|
generate_chunk(&mut state, Vec2::new(x, y));
|
||||||
|
@ -42,6 +42,7 @@ rustls-pemfile = { version = "1", default-features = false }
|
|||||||
atomicwrites = "0.3.0"
|
atomicwrites = "0.3.0"
|
||||||
chrono = { version = "0.4.19", features = ["serde"] }
|
chrono = { version = "0.4.19", features = ["serde"] }
|
||||||
chrono-tz = { version = "0.6", features = ["serde"] }
|
chrono-tz = { version = "0.6", features = ["serde"] }
|
||||||
|
drop_guard = { version = "0.3.0" }
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
@ -60,6 +60,14 @@ impl ChunkGenerator {
|
|||||||
let index = index.as_index_ref();
|
let index = index.as_index_ref();
|
||||||
let payload = world
|
let payload = world
|
||||||
.generate_chunk(index, key, || cancel.load(Ordering::Relaxed), Some(time))
|
.generate_chunk(index, key, || 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
|
||||||
|
// bloat memory a lot. Currently, this isn't much of an issue because we rarely
|
||||||
|
// have large numbers of pending chunks, so most of them are likely to be nearby an
|
||||||
|
// actual player most of the time, but that will eventually change. In the future,
|
||||||
|
// some solution that always pushes chunk updates to players (rather than waiting
|
||||||
|
// for explicit requests) should adequately solve this kind of issue.
|
||||||
.map_err(|_| entity);
|
.map_err(|_| entity);
|
||||||
let _ = chunk_tx.send((key, payload));
|
let _ = chunk_tx.send((key, payload));
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,12 @@ pub(crate) struct ServerInfoPacket {
|
|||||||
pub(crate) type IncomingClient = Client;
|
pub(crate) type IncomingClient = Client;
|
||||||
|
|
||||||
pub(crate) struct ConnectionHandler {
|
pub(crate) struct ConnectionHandler {
|
||||||
|
/// We never actually use this, but if it's dropped before the network has a chance to exit,
|
||||||
|
/// it won't block the main thread, and if it is dropped after the network thread ends, it
|
||||||
|
/// will drop the network here (rather than delaying the network thread). So it emulates
|
||||||
|
/// the effects of storing the network in an Arc, without us losing mutability in the network
|
||||||
|
/// thread.
|
||||||
|
_network_receiver: oneshot::Receiver<Network>,
|
||||||
thread_handle: Option<tokio::task::JoinHandle<()>>,
|
thread_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
pub client_receiver: Receiver<IncomingClient>,
|
pub client_receiver: Receiver<IncomingClient>,
|
||||||
pub info_requester_receiver: Receiver<Sender<ServerInfoPacket>>,
|
pub info_requester_receiver: Receiver<Sender<ServerInfoPacket>>,
|
||||||
@ -27,6 +33,7 @@ pub(crate) struct ConnectionHandler {
|
|||||||
impl ConnectionHandler {
|
impl ConnectionHandler {
|
||||||
pub fn new(network: Network, runtime: &Runtime) -> Self {
|
pub fn new(network: Network, runtime: &Runtime) -> Self {
|
||||||
let (stop_sender, stop_receiver) = oneshot::channel();
|
let (stop_sender, stop_receiver) = oneshot::channel();
|
||||||
|
let (network_sender, _network_receiver) = oneshot::channel();
|
||||||
|
|
||||||
let (client_sender, client_receiver) = unbounded::<IncomingClient>();
|
let (client_sender, client_receiver) = unbounded::<IncomingClient>();
|
||||||
let (info_requester_sender, info_requester_receiver) =
|
let (info_requester_sender, info_requester_receiver) =
|
||||||
@ -37,6 +44,7 @@ impl ConnectionHandler {
|
|||||||
client_sender,
|
client_sender,
|
||||||
info_requester_sender,
|
info_requester_sender,
|
||||||
stop_receiver,
|
stop_receiver,
|
||||||
|
network_sender,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -44,15 +52,24 @@ impl ConnectionHandler {
|
|||||||
client_receiver,
|
client_receiver,
|
||||||
info_requester_receiver,
|
info_requester_receiver,
|
||||||
stop_sender: Some(stop_sender),
|
stop_sender: Some(stop_sender),
|
||||||
|
_network_receiver,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn work(
|
async fn work(
|
||||||
mut network: Network,
|
network: Network,
|
||||||
client_sender: Sender<IncomingClient>,
|
client_sender: Sender<IncomingClient>,
|
||||||
info_requester_sender: Sender<Sender<ServerInfoPacket>>,
|
info_requester_sender: Sender<Sender<ServerInfoPacket>>,
|
||||||
stop_receiver: oneshot::Receiver<()>,
|
stop_receiver: oneshot::Receiver<()>,
|
||||||
|
network_sender: oneshot::Sender<Network>,
|
||||||
) {
|
) {
|
||||||
|
// Emulate the effects of storing the network in an Arc, without losing mutability.
|
||||||
|
let mut network_sender = Some(network_sender);
|
||||||
|
let mut network = drop_guard::guard(network, move |network| {
|
||||||
|
// If the network receiver was already dropped, we just drop the network here, just
|
||||||
|
// like Arc, so we don't care about the result.
|
||||||
|
let _ = network_sender.take().expect("Only used once in drop").send(network);
|
||||||
|
});
|
||||||
let mut stop_receiver = stop_receiver.fuse();
|
let mut stop_receiver = stop_receiver.fuse();
|
||||||
loop {
|
loop {
|
||||||
let participant = match select!(
|
let participant = match select!(
|
||||||
|
@ -238,7 +238,43 @@ impl Server {
|
|||||||
|
|
||||||
let battlemode_buffer = BattleModeBuffer::default();
|
let battlemode_buffer = BattleModeBuffer::default();
|
||||||
|
|
||||||
let mut state = State::server(pools);
|
#[cfg(feature = "worldgen")]
|
||||||
|
let (world, index) = World::generate(
|
||||||
|
settings.world_seed,
|
||||||
|
WorldOpts {
|
||||||
|
seed_elements: true,
|
||||||
|
world_file: if let Some(ref opts) = settings.map_file {
|
||||||
|
opts.clone()
|
||||||
|
} else {
|
||||||
|
// Load default map from assets.
|
||||||
|
FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into())
|
||||||
|
},
|
||||||
|
calendar: Some(settings.calendar_mode.calendar_now()),
|
||||||
|
},
|
||||||
|
&pools.2,
|
||||||
|
);
|
||||||
|
#[cfg(not(feature = "worldgen"))]
|
||||||
|
let (world, index) = World::generate(settings.world_seed);
|
||||||
|
|
||||||
|
#[cfg(feature = "worldgen")]
|
||||||
|
let map = world.get_map_data(index.as_index_ref(), &pools.2);
|
||||||
|
#[cfg(not(feature = "worldgen"))]
|
||||||
|
let map = WorldMapMsg {
|
||||||
|
dimensions_lg: Vec2::zero(),
|
||||||
|
max_height: 1.0,
|
||||||
|
rgba: Grid::new(Vec2::new(1, 1), 1),
|
||||||
|
horizons: [(vec![0], vec![0]), (vec![0], vec![0])],
|
||||||
|
alt: Grid::new(Vec2::new(1, 1), 1),
|
||||||
|
sites: Vec::new(),
|
||||||
|
pois: Vec::new(),
|
||||||
|
default_chunk: Arc::new(world.generate_oob_chunk()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = State::server(
|
||||||
|
pools,
|
||||||
|
world.sim().map_size_lg(),
|
||||||
|
Arc::clone(&map.default_chunk),
|
||||||
|
);
|
||||||
state.ecs_mut().insert(battlemode_buffer);
|
state.ecs_mut().insert(battlemode_buffer);
|
||||||
state.ecs_mut().insert(settings.clone());
|
state.ecs_mut().insert(settings.clone());
|
||||||
state.ecs_mut().insert(editable_settings);
|
state.ecs_mut().insert(editable_settings);
|
||||||
@ -291,6 +327,7 @@ impl Server {
|
|||||||
let pool = state.ecs_mut().write_resource::<SlowJobPool>();
|
let pool = state.ecs_mut().write_resource::<SlowJobPool>();
|
||||||
pool.configure(&"CHUNK_GENERATOR", |n| n / 2 + n / 4);
|
pool.configure(&"CHUNK_GENERATOR", |n| n / 2 + n / 4);
|
||||||
pool.configure(&"CHUNK_SERIALIZER", |n| n / 2);
|
pool.configure(&"CHUNK_SERIALIZER", |n| n / 2);
|
||||||
|
pool.configure(&"CHUNK_DROP", |_n| 1);
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
@ -364,40 +401,6 @@ impl Server {
|
|||||||
debug!(?banned_words_count);
|
debug!(?banned_words_count);
|
||||||
trace!(?banned_words);
|
trace!(?banned_words);
|
||||||
state.ecs_mut().insert(AliasValidator::new(banned_words));
|
state.ecs_mut().insert(AliasValidator::new(banned_words));
|
||||||
|
|
||||||
#[cfg(feature = "worldgen")]
|
|
||||||
let (world, index) = World::generate(
|
|
||||||
settings.world_seed,
|
|
||||||
WorldOpts {
|
|
||||||
seed_elements: true,
|
|
||||||
world_file: if let Some(ref opts) = settings.map_file {
|
|
||||||
opts.clone()
|
|
||||||
} else {
|
|
||||||
// Load default map from assets.
|
|
||||||
FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into())
|
|
||||||
},
|
|
||||||
calendar: Some(settings.calendar_mode.calendar_now()),
|
|
||||||
},
|
|
||||||
state.thread_pool(),
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(feature = "worldgen")]
|
|
||||||
let map = world.get_map_data(index.as_index_ref(), state.thread_pool());
|
|
||||||
|
|
||||||
#[cfg(not(feature = "worldgen"))]
|
|
||||||
let (world, index) = World::generate(settings.world_seed);
|
|
||||||
#[cfg(not(feature = "worldgen"))]
|
|
||||||
let map = WorldMapMsg {
|
|
||||||
dimensions_lg: Vec2::zero(),
|
|
||||||
max_height: 1.0,
|
|
||||||
rgba: Grid::new(Vec2::new(1, 1), 1),
|
|
||||||
horizons: [(vec![0], vec![0]), (vec![0], vec![0])],
|
|
||||||
sea_level: 0.0,
|
|
||||||
alt: Grid::new(Vec2::new(1, 1), 1),
|
|
||||||
sites: Vec::new(),
|
|
||||||
pois: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
state.ecs_mut().insert(map);
|
state.ecs_mut().insert(map);
|
||||||
|
|
||||||
#[cfg(feature = "worldgen")]
|
#[cfg(feature = "worldgen")]
|
||||||
@ -802,10 +805,10 @@ impl Server {
|
|||||||
// so, we delete them. We check for
|
// so, we delete them. We check for
|
||||||
// `home_chunk` in order to avoid duplicating
|
// `home_chunk` in order to avoid duplicating
|
||||||
// the entity under some circumstances.
|
// the entity under some circumstances.
|
||||||
terrain.get_key(chunk_key).is_none() && terrain.get_key(*hc).is_none()
|
terrain.get_key_real(chunk_key).is_none() && terrain.get_key_real(*hc).is_none()
|
||||||
},
|
},
|
||||||
Some(Anchor::Entity(entity)) => !self.state.ecs().is_alive(*entity),
|
Some(Anchor::Entity(entity)) => !self.state.ecs().is_alive(*entity),
|
||||||
None => terrain.get_key(chunk_key).is_none(),
|
None => terrain.get_key_real(chunk_key).is_none(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(|(entity, _, _, _)| entity)
|
.map(|(entity, _, _, _)| entity)
|
||||||
|
@ -108,7 +108,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(chunk_key, meta)| {
|
.filter_map(|(chunk_key, meta)| {
|
||||||
terrain
|
terrain
|
||||||
.get_key_arc(chunk_key)
|
.get_key_arc_real(chunk_key)
|
||||||
.map(|chunk| (Arc::clone(chunk), chunk_key, meta))
|
.map(|chunk| (Arc::clone(chunk), chunk_key, meta))
|
||||||
})
|
})
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -11,7 +11,7 @@ use common::{
|
|||||||
};
|
};
|
||||||
use common_ecs::{Job, Origin, ParMode, Phase, System};
|
use common_ecs::{Job, Origin, ParMode, Phase, System};
|
||||||
use common_net::msg::{ClientGeneral, ServerGeneral};
|
use common_net::msg::{ClientGeneral, ServerGeneral};
|
||||||
use rayon::iter::ParallelIterator;
|
use rayon::prelude::*;
|
||||||
use specs::{Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, Write, WriteStorage};
|
use specs::{Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, Write, WriteStorage};
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
@ -53,7 +53,9 @@ impl<'a> System<'a> for Sys {
|
|||||||
) {
|
) {
|
||||||
job.cpu_stats.measure(ParMode::Rayon);
|
job.cpu_stats.measure(ParMode::Rayon);
|
||||||
let mut new_chunk_requests = (&entities, &mut clients, (&presences).maybe())
|
let mut new_chunk_requests = (&entities, &mut clients, (&presences).maybe())
|
||||||
.par_join()
|
.join()
|
||||||
|
// NOTE: Required because Specs has very poor work splitting for sparse joins.
|
||||||
|
.par_bridge()
|
||||||
.map_init(
|
.map_init(
|
||||||
|| (chunk_send_bus.emitter(), server_event_bus.emitter()),
|
|| (chunk_send_bus.emitter(), server_event_bus.emitter()),
|
||||||
|(chunk_send_emitter, server_emitter), (entity, client, maybe_presence)| {
|
|(chunk_send_emitter, server_emitter), (entity, client, maybe_presence)| {
|
||||||
|
@ -31,11 +31,13 @@ use common::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use common_ecs::{Job, Origin, Phase, System};
|
use common_ecs::{Job, Origin, Phase, System};
|
||||||
use common_net::msg::ServerGeneral;
|
use common_net::msg::{SerializedTerrainChunk, ServerGeneral};
|
||||||
use common_state::TerrainChanges;
|
use common_state::TerrainChanges;
|
||||||
use comp::Behavior;
|
use comp::Behavior;
|
||||||
use rayon::iter::ParallelIterator;
|
use core::cmp::Reverse;
|
||||||
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect, WriteStorage};
|
use itertools::Itertools;
|
||||||
|
use specs::{storage::GenericReadStorage, Entity, Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, Write, WriteExpect, WriteStorage};
|
||||||
|
use rayon::{iter::Either, prelude::*};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
@ -161,7 +163,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
Arc::clone(&world),
|
Arc::clone(&world),
|
||||||
index.clone(),
|
index.clone(),
|
||||||
(*time_of_day/*, calendar.clone()*/),
|
(*time_of_day/*, calendar.clone()*/),
|
||||||
)
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch any generated `TerrainChunk`s and insert them into the terrain.
|
// Fetch any generated `TerrainChunk`s and insert them into the terrain.
|
||||||
@ -256,7 +258,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert a safezone if chunk contains the spawn position
|
// Insert a safezone if chunk contains the spawn position
|
||||||
if server_settings.gameplay.safe_spawn && is_spawn_chunk(key, *spawn_point, &terrain) {
|
if server_settings.gameplay.safe_spawn && is_spawn_chunk(key, *spawn_point) {
|
||||||
server_emitter.emit(ServerEvent::CreateSafezone {
|
server_emitter.emit(ServerEvent::CreateSafezone {
|
||||||
range: Some(SAFE_ZONE_RADIUS),
|
range: Some(SAFE_ZONE_RADIUS),
|
||||||
pos: Pos(spawn_point.0),
|
pos: Pos(spawn_point.0),
|
||||||
@ -264,90 +266,159 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut repositioned = Vec::new();
|
let repositioned = (&entities, &mut positions, reposition_on_load.mask())
|
||||||
for (entity, pos, _) in (&entities, &mut positions, &reposition_on_load).join() {
|
// TODO: Consider using par_bridge() because Rayon has very poor work splitting for
|
||||||
// If an entity is marked as needing repositioning once the chunk loads (e.g.
|
// sparse joins.
|
||||||
// from having just logged in), reposition them.
|
.par_join()
|
||||||
|
.filter_map(|(entity, pos, _)| {
|
||||||
let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32));
|
// NOTE: We use regular as casts rather than as_ because we want to saturate on
|
||||||
if let Some(chunk) = terrain.get_key(chunk_pos) {
|
// overflow.
|
||||||
pos.0 = terrain
|
let entity_pos = pos.0.map(|x| x as i32);
|
||||||
.try_find_space(pos.0.as_::<i32>())
|
// If an entity is marked as needing repositioning once the chunk loads (e.g.
|
||||||
|
// from having just logged in), reposition them.
|
||||||
|
let chunk_pos = TerrainGrid::chunk_key(entity_pos);
|
||||||
|
let chunk = terrain.get_key_real(chunk_pos)?;
|
||||||
|
let new_pos = terrain
|
||||||
|
.try_find_space(entity_pos)
|
||||||
.map(|x| x.as_::<f32>())
|
.map(|x| x.as_::<f32>())
|
||||||
.unwrap_or_else(|| chunk.find_accessible_pos(pos.0.xy().as_::<i32>(), false));
|
.unwrap_or_else(|| chunk.find_accessible_pos(entity_pos.xy(), false));
|
||||||
repositioned.push(entity);
|
pos.0 = new_pos;
|
||||||
let _ = force_update.insert(entity, ForceUpdate);
|
Some((entity, new_pos))
|
||||||
let _ = waypoints.insert(entity, Waypoint::new(pos.0, *time));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for entity in repositioned {
|
|
||||||
reposition_on_load.remove(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the chunk to all nearby players.
|
|
||||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
|
||||||
new_chunks.into_par_iter().for_each_init(
|
|
||||||
|| chunk_send_bus.emitter(),
|
|
||||||
|chunk_send_emitter, (key, _chunk)| {
|
|
||||||
(&entities, &presences, &positions, &clients)
|
|
||||||
.join()
|
|
||||||
.for_each(|(entity, presence, pos, _client)| {
|
|
||||||
let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32));
|
|
||||||
// Subtract 2 from the offset before computing squared magnitude
|
|
||||||
// 1 since chunks need neighbors to be meshed
|
|
||||||
// 1 to act as a buffer if the player moves in that direction
|
|
||||||
let adjusted_dist_sqr = (chunk_pos - key)
|
|
||||||
.map(|e: i32| (e.unsigned_abs()).saturating_sub(2))
|
|
||||||
.magnitude_squared();
|
|
||||||
|
|
||||||
if adjusted_dist_sqr <= presence.view_distance.pow(2) {
|
|
||||||
chunk_send_emitter.emit(ChunkSendEntry {
|
|
||||||
entity,
|
|
||||||
chunk_key: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove chunks that are too far from players.
|
|
||||||
let chunks_to_remove = terrain
|
|
||||||
.par_keys()
|
|
||||||
.copied()
|
|
||||||
// Don't check every chunk every tick (spread over 16 ticks)
|
|
||||||
.filter(|k| k.x.unsigned_abs() % 4 + (k.y.unsigned_abs() % 4) * 4 == (tick.0 % 16) as u32)
|
|
||||||
// There shouldn't be to many pending chunks so we will just check them all
|
|
||||||
.chain(chunk_generator.par_pending_chunks())
|
|
||||||
.filter(|chunk_key| {
|
|
||||||
let mut should_drop = true;
|
|
||||||
|
|
||||||
// For each player with a position, calculate the distance.
|
|
||||||
for (presence, pos) in (&presences, &positions).join() {
|
|
||||||
if chunk_in_vd(pos.0, *chunk_key, &terrain, presence.view_distance) {
|
|
||||||
should_drop = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!should_drop
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for key in chunks_to_remove {
|
for (entity, new_pos) in repositioned {
|
||||||
|
// TODO: Consider putting this in another system since this forces us to take positions
|
||||||
|
// by write rather than read access.
|
||||||
|
let _ = force_update.insert(entity, ForceUpdate);
|
||||||
|
let _ = waypoints.insert(entity, Waypoint::new(new_pos, *time));
|
||||||
|
reposition_on_load.remove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_view_distance = server_settings.max_view_distance.unwrap_or(u32::MAX);
|
||||||
|
let (presences_position_entities, presences_positions) =
|
||||||
|
prepare_player_presences(
|
||||||
|
&world,
|
||||||
|
max_view_distance,
|
||||||
|
&entities,
|
||||||
|
&positions,
|
||||||
|
&presences,
|
||||||
|
&clients,
|
||||||
|
);
|
||||||
|
let real_max_view_distance = convert_to_loaded_vd(u32::MAX, max_view_distance);
|
||||||
|
|
||||||
|
// Send the chunks to all nearby players.
|
||||||
|
new_chunks.par_iter().for_each_init(
|
||||||
|
|| chunk_send_bus.emitter(),
|
||||||
|
|chunk_send_emitter, (chunk_key, _)| {
|
||||||
|
// We only have to check players inside the maximum view distance of the server of
|
||||||
|
// our own position.
|
||||||
|
//
|
||||||
|
// We start by partitioning by X, finding only entities in chunks within the X
|
||||||
|
// range of us. These are guaranteed in bounds due to restrictions on max view
|
||||||
|
// distance (namely: the square of any chunk coordinate plus the max view distance
|
||||||
|
// along both axes must fit in an i32).
|
||||||
|
let min_chunk_x = i32::from(chunk_key.x) - real_max_view_distance;
|
||||||
|
let max_chunk_x = i32::from(chunk_key.x) + real_max_view_distance;
|
||||||
|
let start = presences_position_entities
|
||||||
|
.partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
|
||||||
|
// NOTE: We *could* just scan forward until we hit the end, but this way we save a
|
||||||
|
// comparison in the inner loop, since also needs to check the list length. We
|
||||||
|
// could also save some time by starting from start rather than end, but the hope
|
||||||
|
// is that this way the compiler (and machine) can reorder things so both ends are
|
||||||
|
// fetched in parallel; since the vast majority of the time both fetched elements
|
||||||
|
// should already be in cache, this should not use any extra memory bandwidth.
|
||||||
|
//
|
||||||
|
// TODO: Benchmark and figure out whether this is better in practice than just
|
||||||
|
// scanning forward.
|
||||||
|
let end = presences_position_entities
|
||||||
|
.partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
|
||||||
|
let interior = &presences_position_entities[start..end];
|
||||||
|
interior.into_iter().filter(|((player_chunk_pos, player_vd_sqr), _)| {
|
||||||
|
chunk_in_vd(*player_chunk_pos, *player_vd_sqr, *chunk_key)
|
||||||
|
})
|
||||||
|
.for_each(|(_, entity)| {
|
||||||
|
chunk_send_emitter.emit(ChunkSendEntry {
|
||||||
|
entity: *entity,
|
||||||
|
chunk_key: *chunk_key,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let tick = (tick.0 % 16) as i32;
|
||||||
|
|
||||||
|
// Remove chunks that are too far from players.
|
||||||
|
//
|
||||||
|
// Note that all chunks involved here (both terrain chunks and pending chunks) are
|
||||||
|
// guaranteed in bounds. This simplifies the rest of the logic here.
|
||||||
|
let chunks_to_remove = terrain
|
||||||
|
.par_keys()
|
||||||
|
.copied()
|
||||||
|
// There may be lots of pending chunks, so don't check them all. This should be okay
|
||||||
|
// as long as we're maintaining a reasonable tick rate.
|
||||||
|
.chain(chunk_generator.par_pending_chunks())
|
||||||
|
// Don't check every chunk every tick (spread over 16 ticks)
|
||||||
|
//
|
||||||
|
// TODO: Investigate whether we can add support for performing this filtering directly
|
||||||
|
// within hashbrown (basically, specify we want to iterate through just buckets with
|
||||||
|
// hashes in a particular range). This could provide significiant speedups since we
|
||||||
|
// could avoid having to iterate through a bunch of buckets we don't care about.
|
||||||
|
//
|
||||||
|
// TODO: Make the percentage of the buckets that we go through adjust dynamically
|
||||||
|
// depending on the current number of chunks. In the worst case, we might want to scan
|
||||||
|
// just 1/256 of the chunks each tick, for example.
|
||||||
|
.filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
|
||||||
|
.filter(|&chunk_key| {
|
||||||
|
// We only have to check players inside the maximum view distance of the server of
|
||||||
|
// our own position.
|
||||||
|
//
|
||||||
|
// We start by partitioning by X, finding only entities in chunks within the X
|
||||||
|
// range of us. These are guaranteed in bounds due to restrictions on max view
|
||||||
|
// distance (namely: the square of any chunk coordinate plus the max view distance
|
||||||
|
// along both axes must fit in an i32).
|
||||||
|
let min_chunk_x = i32::from(chunk_key.x) - real_max_view_distance;
|
||||||
|
let max_chunk_x = i32::from(chunk_key.x) + real_max_view_distance;
|
||||||
|
let start = presences_positions
|
||||||
|
.partition_point(|(pos, _)| i32::from(pos.x) < min_chunk_x);
|
||||||
|
// NOTE: We *could* just scan forward until we hit the end, but this way we save a
|
||||||
|
// comparison in the inner loop, since also needs to check the list length. We
|
||||||
|
// could also save some time by starting from start rather than end, but the hope
|
||||||
|
// is that this way the compiler (and machine) can reorder things so both ends are
|
||||||
|
// fetched in parallel; since the vast majority of the time both fetched elements
|
||||||
|
// should already be in cache, this should not use any extra memory bandwidth.
|
||||||
|
//
|
||||||
|
// TODO: Benchmark and figure out whether this is better in practice than just
|
||||||
|
// scanning forward.
|
||||||
|
let end = presences_positions
|
||||||
|
.partition_point(|(pos, _)| i32::from(pos.x) < max_chunk_x);
|
||||||
|
let interior = &presences_positions[start..end];
|
||||||
|
!interior.into_iter().any(|&(player_chunk_pos, player_vd_sqr)| {
|
||||||
|
chunk_in_vd(player_chunk_pos, player_vd_sqr, chunk_key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let chunks_to_remove = chunks_to_remove.into_iter().filter_map(|key| {
|
||||||
// Register the unloading of this chunk from terrain persistence
|
// Register the unloading of this chunk from terrain persistence
|
||||||
#[cfg(feature = "persistent_world")]
|
#[cfg(feature = "persistent_world")]
|
||||||
if let Some(terrain_persistence) = _terrain_persistence.as_mut() {
|
if let Some(terrain_persistence) = _terrain_persistence.as_mut() {
|
||||||
terrain_persistence.unload_chunk(key);
|
terrain_persistence.unload_chunk(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chunk_generator.cancel_if_pending(key);
|
||||||
|
|
||||||
// TODO: code duplication for chunk insertion between here and state.rs
|
// TODO: code duplication for chunk insertion between here and state.rs
|
||||||
if terrain.remove(key).is_some() {
|
terrain.remove(key).map(|chunk| {
|
||||||
terrain_changes.removed_chunks.insert(key);
|
terrain_changes.removed_chunks.insert(key);
|
||||||
rtsim.hook_unload_chunk(key);
|
rtsim.hook_unload_chunk(key);
|
||||||
}
|
chunk
|
||||||
|
})
|
||||||
chunk_generator.cancel_if_pending(key);
|
}).collect::<Vec<_>>();
|
||||||
}
|
// Drop chunks in a background thread.
|
||||||
|
slow_jobs.spawn(&"CHUNK_DROP", async move {
|
||||||
|
drop(chunks_to_remove);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -496,26 +567,157 @@ impl NpcData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chunk_in_vd(
|
pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
|
||||||
player_pos: Vec3<f32>,
|
// Hardcoded max VD to prevent stupid view distances from creating overflows.
|
||||||
chunk_pos: Vec2<i32>,
|
// This must be a value ≤
|
||||||
terrain: &TerrainGrid,
|
// √(i32::MAX - 2 * ((1 << (MAX_WORLD_BLOCKS_LG - TERRAIN_CHUNK_BLOCKS_LG) - 1)² - 1)) / 2
|
||||||
vd: u32,
|
//
|
||||||
) -> bool {
|
// since otherwise we could end up overflowing. Since it is a requirement that each dimension
|
||||||
|
// (in chunks) has to fit in a i16, we can derive √((1<<31)-1 - 2*((1<<15)-1)^2) / 2 ≥ 1 << 7
|
||||||
|
// as the absolute limit.
|
||||||
|
//
|
||||||
|
// TODO: Make this more official and use it elsewhere.
|
||||||
|
const MAX_VD: u32 = 1 << 7;
|
||||||
|
|
||||||
// This fuzzy threshold prevents chunks rapidly unloading and reloading when
|
// This fuzzy threshold prevents chunks rapidly unloading and reloading when
|
||||||
// players move over a chunk border.
|
// players move over a chunk border.
|
||||||
const UNLOAD_THRESHOLD: u32 = 2;
|
const UNLOAD_THRESHOLD: u32 = 2;
|
||||||
|
|
||||||
let player_chunk_pos = terrain.pos_key(player_pos.map(|e| e as i32));
|
// NOTE: This cast is safe for the reasons mentioned above.
|
||||||
|
(vd.max(crate::MIN_VD).min(max_view_distance).saturating_add(UNLOAD_THRESHOLD)).min(MAX_VD) as i32
|
||||||
let adjusted_dist_sqr = (player_chunk_pos - chunk_pos)
|
|
||||||
.map(|e: i32| e.unsigned_abs())
|
|
||||||
.magnitude_squared();
|
|
||||||
|
|
||||||
adjusted_dist_sqr <= (vd.max(crate::MIN_VD) + UNLOAD_THRESHOLD).pow(2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_spawn_chunk(chunk_pos: Vec2<i32>, spawn_pos: SpawnPoint, terrain: &TerrainGrid) -> bool {
|
/// Returns: ((player_chunk_pos, player_vd_squared), entity, is_client)
|
||||||
let spawn_chunk_pos = terrain.pos_key(spawn_pos.0.map(|e| e as i32));
|
pub fn prepare_for_vd_check(
|
||||||
|
world_aabr_in_chunks: &Aabr<i32>,
|
||||||
|
max_view_distance: u32,
|
||||||
|
entity: Entity,
|
||||||
|
presence: &Presence,
|
||||||
|
pos: &Pos,
|
||||||
|
client: Option<u32>,
|
||||||
|
) -> Option<((Vec2<u16>, i32), Entity, bool)> {
|
||||||
|
let is_client = client.is_some();
|
||||||
|
let pos = pos.0;
|
||||||
|
let vd = presence.view_distance;
|
||||||
|
|
||||||
|
// NOTE: We use regular as casts rather than as_ because we want to saturate on
|
||||||
|
// overflow.
|
||||||
|
let player_pos = pos.map(|x| x as i32);
|
||||||
|
let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
|
||||||
|
let player_vd = convert_to_loaded_vd(vd, max_view_distance);
|
||||||
|
|
||||||
|
// We filter out positions that are *clearly* way out of range from consideration.
|
||||||
|
// This is pretty easy to do, and means we don't have to perform expensive overflow
|
||||||
|
// checks elsewhere (otherwise, a player sufficiently far off the map could cause
|
||||||
|
// chunks they were nowhere near to stay loaded, parallel universes style).
|
||||||
|
//
|
||||||
|
// One could also imagine snapping a player to the part of the map nearest to them.
|
||||||
|
// We don't currently do this in case we rely elsewhere on players always being
|
||||||
|
// near the chunks they're keeping loaded, but it would allow us to use u32
|
||||||
|
// exclusively so it's tempting.
|
||||||
|
let player_aabr_in_chunks = Aabr {
|
||||||
|
min: player_chunk_pos - player_vd,
|
||||||
|
max: player_chunk_pos + player_vd,
|
||||||
|
};
|
||||||
|
world_aabr_in_chunks
|
||||||
|
.collides_with_aabr(player_aabr_in_chunks)
|
||||||
|
// The cast to i32 here is definitely safe thanks to MAX_VD limiting us to fit
|
||||||
|
// within i32^2.
|
||||||
|
//
|
||||||
|
// The cast from each coordinate to u16 should also be correct here. This is
|
||||||
|
// because valid world chunk coordinates are no greater than 1 << 14 - 1; since we
|
||||||
|
// verified that the player is within world bounds, safety of the cast follows (we
|
||||||
|
// could even cast to i16, but we actually want it as u16 for some future checks).
|
||||||
|
.then(|| ((player_chunk_pos.as_::<u16>(), player_vd.pow(2) as i32), entity, is_client))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_player_presences<'a, P>(
|
||||||
|
world: &World,
|
||||||
|
max_view_distance: u32,
|
||||||
|
entities: &Entities<'a>,
|
||||||
|
positions: P,
|
||||||
|
presences: &ReadStorage<'a, Presence>,
|
||||||
|
clients: &ReadStorage<'a, Client>,
|
||||||
|
) -> (Vec<((Vec2<u16>, i32), Entity)>, Vec<(Vec2<u16>, i32)>)
|
||||||
|
where P: GenericReadStorage<Component=Pos> + Join<Type=&'a Pos>
|
||||||
|
{
|
||||||
|
// We start by collecting presences and positions from players, because they are very
|
||||||
|
// sparse in the entity list and therefore iterating over them for each chunk can be quite
|
||||||
|
// slow.
|
||||||
|
let world_aabr_in_chunks = Aabr {
|
||||||
|
min: Vec2::zero(),
|
||||||
|
// NOTE: Cast is correct because chunk coordinates must fit in an i32 (actually, i16).
|
||||||
|
max: world.sim().get_size().map(|x| x.saturating_sub(1)).as_::<i32>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
|
||||||
|
(entities, presences, positions, clients.mask().maybe())
|
||||||
|
.join()
|
||||||
|
.filter_map(|(entity, presence, position, client)| {
|
||||||
|
prepare_for_vd_check(
|
||||||
|
&world_aabr_in_chunks,
|
||||||
|
max_view_distance,
|
||||||
|
entity,
|
||||||
|
presence,
|
||||||
|
position,
|
||||||
|
client,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.partition_map(|(player_data, entity, is_client)| {
|
||||||
|
// For chunks with clients, we need to record their entity, because they might be used
|
||||||
|
// for insertion. These elements fit in 8 bytes, so this should be pretty
|
||||||
|
// cache-friendly.
|
||||||
|
if is_client {
|
||||||
|
Either::Left((player_data, entity))
|
||||||
|
} else {
|
||||||
|
// For chunks without clients, we only need to record the position and view
|
||||||
|
// distance. These elements fit in 4 bytes, which is even cache-friendlier.
|
||||||
|
Either::Right(player_data)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// We sort the presence lists by X position, so we can efficiently filter out players
|
||||||
|
// nowhere near the chunk. This is basically a poor substitute for the effects of a proper
|
||||||
|
// KDTree, but a proper KDTree has too much overhead to be worth using for such a short
|
||||||
|
// list (~ 1000 players at most). We also sort by y and reverse position; this will become
|
||||||
|
// important later.
|
||||||
|
presences_positions_entities.sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
|
||||||
|
presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
|
||||||
|
// For the vast majority of chunks (present and pending ones), we'll only ever need the
|
||||||
|
// position and view distance. So we extend it with these from the list of client chunks, and
|
||||||
|
// then do some further work to improve performance (taking advantage of the fact that they
|
||||||
|
// don't require entities).
|
||||||
|
presences_positions
|
||||||
|
.extend(presences_positions_entities.iter().map(|&(player_data, _)| player_data));
|
||||||
|
// Since both lists were previously sorted, we use stable sort over unstable sort, as it's
|
||||||
|
// faster in that case (theoretically a proper merge operation would be ideal, but it's not
|
||||||
|
// worth pulling in a library for).
|
||||||
|
presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
|
||||||
|
// Now that the list is sorted, we deduplicate players in the same chunk (this is why we
|
||||||
|
// need to sort y as well as x; dedup only works if the list is sorted by the element we
|
||||||
|
// use to dedup). Importantly, we can then use only the *first* element as a substitute
|
||||||
|
// for all the players in the chunk, because we *also* sorted from greatest to lowest view
|
||||||
|
// distance, and dedup_by removes all but the first matching element. In the common case
|
||||||
|
// where a few chunks are very crowded, this further reduces the work required per chunk.
|
||||||
|
presences_positions.dedup_by_key(|&mut (pos, _)| pos);
|
||||||
|
|
||||||
|
(presences_positions_entities, presences_positions)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunk_in_vd(
|
||||||
|
player_chunk_pos: Vec2<u16>,
|
||||||
|
player_vd_sqr: i32,
|
||||||
|
chunk_pos: Vec2<i32>,
|
||||||
|
) -> bool {
|
||||||
|
// NOTE: Guaranteed in bounds as long as prepare_player_presences prepared the player_chunk_pos
|
||||||
|
// and player_vd_sqr.
|
||||||
|
let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
|
||||||
|
|
||||||
|
adjusted_dist_sqr <= player_vd_sqr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_spawn_chunk(chunk_pos: Vec2<i32>, spawn_pos: SpawnPoint) -> bool {
|
||||||
|
// FIXME: Ensure spawn_pos doesn't overflow before performing this cast.
|
||||||
|
let spawn_chunk_pos = TerrainGrid::chunk_key(spawn_pos.0.map(|e| e as i32));
|
||||||
chunk_pos == spawn_chunk_pos
|
chunk_pos == spawn_chunk_pos
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
use crate::{chunk_serialize::ChunkSendEntry, client::Client, presence::Presence};
|
use crate::{chunk_serialize::ChunkSendEntry, client::Client, presence::Presence, Settings};
|
||||||
use common::{comp::Pos, event::EventBus, terrain::TerrainGrid};
|
use common::{comp::Pos, event::EventBus};
|
||||||
use common_ecs::{Job, Origin, Phase, System};
|
use common_ecs::{Job, Origin, Phase, System};
|
||||||
use common_net::msg::{CompressedData, ServerGeneral};
|
use common_net::msg::{CompressedData, ServerGeneral};
|
||||||
use common_state::TerrainChanges;
|
use common_state::TerrainChanges;
|
||||||
|
use world::World;
|
||||||
|
use rayon::prelude::*;
|
||||||
use specs::{Entities, Join, Read, ReadExpect, ReadStorage};
|
use specs::{Entities, Join, Read, ReadExpect, ReadStorage};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// This systems sends new chunks to clients as well as changes to existing
|
/// This systems sends new chunks to clients as well as changes to existing
|
||||||
/// chunks
|
/// chunks
|
||||||
@ -12,7 +15,8 @@ pub struct Sys;
|
|||||||
impl<'a> System<'a> for Sys {
|
impl<'a> System<'a> for Sys {
|
||||||
type SystemData = (
|
type SystemData = (
|
||||||
Entities<'a>,
|
Entities<'a>,
|
||||||
ReadExpect<'a, TerrainGrid>,
|
ReadExpect<'a, Arc<World>>,
|
||||||
|
Read<'a, Settings>,
|
||||||
Read<'a, TerrainChanges>,
|
Read<'a, TerrainChanges>,
|
||||||
ReadExpect<'a, EventBus<ChunkSendEntry>>,
|
ReadExpect<'a, EventBus<ChunkSendEntry>>,
|
||||||
ReadStorage<'a, Pos>,
|
ReadStorage<'a, Pos>,
|
||||||
@ -26,22 +30,58 @@ impl<'a> System<'a> for Sys {
|
|||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
_job: &mut Job<Self>,
|
_job: &mut Job<Self>,
|
||||||
(entities, terrain, terrain_changes, chunk_send_bus, positions, presences, clients): Self::SystemData,
|
(entities, world, server_settings, terrain_changes, chunk_send_bus, positions, presences, clients): Self::SystemData,
|
||||||
) {
|
) {
|
||||||
let mut chunk_send_emitter = chunk_send_bus.emitter();
|
let max_view_distance = server_settings.max_view_distance.unwrap_or(u32::MAX);
|
||||||
|
let (presences_position_entities, _) =
|
||||||
|
super::terrain::prepare_player_presences(
|
||||||
|
&world,
|
||||||
|
max_view_distance,
|
||||||
|
&entities,
|
||||||
|
&positions,
|
||||||
|
&presences,
|
||||||
|
&clients,
|
||||||
|
);
|
||||||
|
let real_max_view_distance = super::terrain::convert_to_loaded_vd(u32::MAX, max_view_distance);
|
||||||
|
|
||||||
// Sync changed chunks
|
// Sync changed chunks
|
||||||
for chunk_key in &terrain_changes.modified_chunks {
|
terrain_changes.modified_chunks.par_iter().for_each_init(
|
||||||
for (entity, presence, pos) in (&entities, &presences, &positions).join() {
|
|| chunk_send_bus.emitter(),
|
||||||
if super::terrain::chunk_in_vd(pos.0, *chunk_key, &terrain, presence.view_distance)
|
|chunk_send_emitter, &chunk_key| {
|
||||||
{
|
// We only have to check players inside the maximum view distance of the server of
|
||||||
|
// our own position.
|
||||||
|
//
|
||||||
|
// We start by partitioning by X, finding only entities in chunks within the X
|
||||||
|
// range of us. These are guaranteed in bounds due to restrictions on max view
|
||||||
|
// distance (namely: the square of any chunk coordinate plus the max view distance
|
||||||
|
// along both axes must fit in an i32).
|
||||||
|
let min_chunk_x = i32::from(chunk_key.x) - real_max_view_distance;
|
||||||
|
let max_chunk_x = i32::from(chunk_key.x) + real_max_view_distance;
|
||||||
|
let start = presences_position_entities
|
||||||
|
.partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
|
||||||
|
// NOTE: We *could* just scan forward until we hit the end, but this way we save a
|
||||||
|
// comparison in the inner loop, since also needs to check the list length. We
|
||||||
|
// could also save some time by starting from start rather than end, but the hope
|
||||||
|
// is that this way the compiler (and machine) can reorder things so both ends are
|
||||||
|
// fetched in parallel; since the vast majority of the time both fetched elements
|
||||||
|
// should already be in cache, this should not use any extra memory bandwidth.
|
||||||
|
//
|
||||||
|
// TODO: Benchmark and figure out whether this is better in practice than just
|
||||||
|
// scanning forward.
|
||||||
|
let end = presences_position_entities
|
||||||
|
.partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
|
||||||
|
let interior = &presences_position_entities[start..end];
|
||||||
|
interior.into_iter().filter(|((player_chunk_pos, player_vd_sqr), _)| {
|
||||||
|
super::terrain::chunk_in_vd(*player_chunk_pos, *player_vd_sqr, chunk_key)
|
||||||
|
})
|
||||||
|
.for_each(|(_, entity)| {
|
||||||
chunk_send_emitter.emit(ChunkSendEntry {
|
chunk_send_emitter.emit(ChunkSendEntry {
|
||||||
entity,
|
entity: *entity,
|
||||||
chunk_key: *chunk_key,
|
chunk_key,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
// TODO: Don't send all changed blocks to all clients
|
// TODO: Don't send all changed blocks to all clients
|
||||||
// Sync changed blocks
|
// Sync changed blocks
|
||||||
|
@ -42,6 +42,15 @@ impl World {
|
|||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub const fn map_size_lg(&self) -> MapSizeLg { DEFAULT_WORLD_CHUNKS_LG }
|
pub const fn map_size_lg(&self) -> MapSizeLg { DEFAULT_WORLD_CHUNKS_LG }
|
||||||
|
|
||||||
|
pub fn generate_oob_chunk(&self) -> TerrainChunk {
|
||||||
|
TerrainChunk::new(
|
||||||
|
0,
|
||||||
|
Block::new(BlockKind::Water, Rgb::zero()),
|
||||||
|
Block::air(SpriteKind::Empty),
|
||||||
|
TerrainChunkMeta::void(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_chunk(
|
pub fn generate_chunk(
|
||||||
&self,
|
&self,
|
||||||
_index: IndexRef,
|
_index: IndexRef,
|
||||||
|
@ -14,7 +14,6 @@ const GEN_SIZE: i32 = 4;
|
|||||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||||
let pool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
let pool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
||||||
// Generate chunks here to test
|
// Generate chunks here to test
|
||||||
let mut terrain = TerrainGrid::new().unwrap();
|
|
||||||
let (world, index) = World::generate(
|
let (world, index) = World::generate(
|
||||||
42,
|
42,
|
||||||
sim::WorldOpts {
|
sim::WorldOpts {
|
||||||
@ -27,6 +26,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
&pool,
|
&pool,
|
||||||
);
|
);
|
||||||
|
let mut terrain = TerrainGrid::new(world.map_size_lg().chunks(), Arc::new(world.sim().generate_oob_chunk())).unwrap();
|
||||||
let index = index.as_index_ref();
|
let index = index.as_index_ref();
|
||||||
(0..GEN_SIZE)
|
(0..GEN_SIZE)
|
||||||
.flat_map(|x| (0..GEN_SIZE).map(move |y| Vec2::new(x, y)))
|
.flat_map(|x| (0..GEN_SIZE).map(move |y| Vec2::new(x, y)))
|
||||||
|
@ -1074,7 +1074,7 @@ impl/*<V: RectRasterableVol>*/ Terrain<V> {
|
|||||||
for i in -1..2 {
|
for i in -1..2 {
|
||||||
for j in -1..2 {
|
for j in -1..2 {
|
||||||
neighbours &= terrain
|
neighbours &= terrain
|
||||||
.contains_key(pos + Vec2::new(i, j));
|
.contains_key_real(pos + Vec2::new(i, j));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1170,7 +1170,7 @@ impl/*<V: RectRasterableVol>*/ Terrain<V> {
|
|||||||
for i in -1..2 {
|
for i in -1..2 {
|
||||||
for j in -1..2 {
|
for j in -1..2 {
|
||||||
neighbours &= terrain
|
neighbours &= terrain
|
||||||
.contains_key(neighbour_chunk_pos + Vec2::new(i, j));
|
.contains_key_real(neighbour_chunk_pos + Vec2::new(i, j));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if neighbours {
|
if neighbours {
|
||||||
|
@ -748,7 +748,7 @@ fn main() {
|
|||||||
let mut totals: BTreeMap<&str, f32> = BTreeMap::new();
|
let mut totals: BTreeMap<&str, f32> = BTreeMap::new();
|
||||||
let mut total_timings: BTreeMap<&str, f32> = BTreeMap::new();
|
let mut total_timings: BTreeMap<&str, f32> = BTreeMap::new();
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut volgrid = VolGrid2d::new().unwrap();
|
let mut volgrid = VolGrid2d::new(world.map_size_lg(), Arc::new(world.sim().generate_oob_chunk())).unwrap();
|
||||||
for (i, spiralpos) in Spiral2d::with_radius(RADIUS)
|
for (i, spiralpos) in Spiral2d::with_radius(RADIUS)
|
||||||
.map(|v| v + sitepos.as_())
|
.map(|v| v + sitepos.as_())
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
@ -266,15 +266,9 @@ impl World {
|
|||||||
Some(sampler) => (/*base_z as i32, */sampler.column_gen.sim_chunk, sampler),
|
Some(sampler) => (/*base_z as i32, */sampler.column_gen.sim_chunk, sampler),
|
||||||
// Some((base_z, sim_chunk)) => (base_z as i32, sim_chunk),
|
// Some((base_z, sim_chunk)) => (base_z as i32, sim_chunk),
|
||||||
None => {
|
None => {
|
||||||
return Ok((
|
// NOTE: This is necessary in order to generate a handful of chunks at the edges of
|
||||||
TerrainChunk::new(
|
// the map.
|
||||||
CONFIG.sea_level as i32,
|
return Ok((self.sim().generate_oob_chunk(), ChunkSupplement::default()));
|
||||||
water,
|
|
||||||
air,
|
|
||||||
TerrainChunkMeta::void(),
|
|
||||||
),
|
|
||||||
ChunkSupplement::default(),
|
|
||||||
));
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,8 +46,8 @@ use common::{
|
|||||||
spiral::Spiral2d,
|
spiral::Spiral2d,
|
||||||
store::Id,
|
store::Id,
|
||||||
terrain::{
|
terrain::{
|
||||||
map::MapConfig, uniform_idx_as_vec2, vec2_as_uniform_idx, BiomeKind, MapSizeLg,
|
map::MapConfig, uniform_idx_as_vec2, vec2_as_uniform_idx, BiomeKind, Block, BlockKind,
|
||||||
TerrainChunkSize,
|
MapSizeLg, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize,
|
||||||
},
|
},
|
||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
@ -68,6 +68,7 @@ use std::{
|
|||||||
io::{BufReader, BufWriter},
|
io::{BufReader, BufWriter},
|
||||||
ops::{Add, Div, Mul, Neg, Sub},
|
ops::{Add, Div, Mul, Neg, Sub},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
@ -1602,6 +1603,15 @@ impl WorldSim {
|
|||||||
|
|
||||||
pub fn get_size(&self) -> Vec2<u32> { self.map_size_lg().chunks().map(u32::from) }
|
pub fn get_size(&self) -> Vec2<u32> { self.map_size_lg().chunks().map(u32::from) }
|
||||||
|
|
||||||
|
pub fn generate_oob_chunk(&self) -> TerrainChunk {
|
||||||
|
TerrainChunk::new(
|
||||||
|
CONFIG.sea_level as i32,
|
||||||
|
Block::new(BlockKind::Water, Rgb::zero()),
|
||||||
|
Block::air(SpriteKind::Empty),
|
||||||
|
TerrainChunkMeta::void(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw a map of the world based on chunk information. Returns a buffer of
|
/// Draw a map of the world based on chunk information. Returns a buffer of
|
||||||
/// u32s.
|
/// u32s.
|
||||||
pub fn get_map(&self, index: IndexRef/*, calendar: Option<&Calendar>*/) -> WorldMapMsg {
|
pub fn get_map(&self, index: IndexRef/*, calendar: Option<&Calendar>*/) -> WorldMapMsg {
|
||||||
@ -1701,13 +1711,13 @@ impl WorldSim {
|
|||||||
);
|
);
|
||||||
WorldMapMsg {
|
WorldMapMsg {
|
||||||
dimensions_lg: self.map_size_lg().vec(),
|
dimensions_lg: self.map_size_lg().vec(),
|
||||||
sea_level: CONFIG.sea_level,
|
|
||||||
max_height: self.max_height,
|
max_height: self.max_height,
|
||||||
rgba: Grid::from_raw(self.get_size().map(|e| e as i32), v),
|
rgba: Grid::from_raw(self.get_size().map(|e| e as i32), v),
|
||||||
alt: Grid::from_raw(self.get_size().map(|e| e as i32), alts),
|
alt: Grid::from_raw(self.get_size().map(|e| e as i32), alts),
|
||||||
horizons,
|
horizons,
|
||||||
sites: Vec::new(), // Will be substituted later
|
sites: Vec::new(), // Will be substituted later
|
||||||
pois: Vec::new(), // Will be substituted later
|
pois: Vec::new(), // Will be substituted later
|
||||||
|
default_chunk: Arc::new(self.generate_oob_chunk()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user