mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Clients keep the light the server sends; the server sends light updates
A remote client (a LAN guest, or the game on its server thread) threw away the light in each LevelChunkWithLight and relit the column and its neighbours itself. It now applies the sky and block layers as ClientPacketListener.applyLightData does, and relights only block changes, as vanilla's client does. The hub now sends ClientboundLightUpdatePacket for the light sections that changed in a column to the players for whom it is on the tracked border (ChunkHolder.broadcastChanges), so light from blocks a guest doesn't track reaches it. horizon-ctl gains 'light X1 Y1 Z1 X2 Y2 Z2', and lan_e2e a 'light' check comparing the guest's light with the host's. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d85a3603bd
commit
817a71ffa5
13 files changed
+394
-32
No files matched your search
@@ -187,6 +187,10 @@ pub const COMMANDS: &[Cmd] = &[
|
||||
&[opt("radius", Ty::Num, "search radius in blocks (default 32)"), opt("type", Ty::Str, "entity type filter")], mcp: true, local: false),
|
||||
cmd!("block", Observe, "block X Y Z", "the block state at a position", "",
|
||||
["horizon-ctl block 10 64 -3"], &BLOCK_XYZ, mcp: true, local: false),
|
||||
cmd!("light", Observe, "light X1 Y1 Z1 X2 Y2 Z2", "the stored sky and block light of a box",
|
||||
"At most 32768 blocks per request. One hex digit per block in `sky` and `block`, x fastest, then z, then y; `.` where no chunk is loaded.",
|
||||
["horizon-ctl light 0 64 0 15 70 15"],
|
||||
&[p("x1", Ty::Int, "corner X"), p("y1", Ty::Int, "corner Y"), p("z1", Ty::Int, "corner Z"), p("x2", Ty::Int, "opposite X"), p("y2", Ty::Int, "opposite Y"), p("z2", Ty::Int, "opposite Z")], mcp: true, local: false),
|
||||
cmd!("blocks", Observe, "blocks X1 Y1 Z1 X2 Y2 Z2", "the non-air blocks of a box, with counts per block",
|
||||
"At most 32768 blocks per request.", ["horizon-ctl blocks 0 60 0 15 70 15"],
|
||||
&[p("x1", Ty::Int, "corner X"), p("y1", Ty::Int, "corner Y"), p("z1", Ty::Int, "corner Z"), p("x2", Ty::Int, "opposite X"), p("y2", Ty::Int, "opposite Y"), p("z2", Ty::Int, "opposite Z")], mcp: true, local: false),
|
||||
|
||||
@@ -6634,6 +6634,7 @@ impl Game<'_> {
|
||||
world.weather = self.world.weather;
|
||||
// An open hub keeps hearing the new level's block changes.
|
||||
world.block_log = self.world.block_log.as_ref().map(|_| Vec::new());
|
||||
world.light_log = self.world.light_log.as_ref().map(|_| Default::default());
|
||||
// The sculk tables' watched blocks are built once per game.
|
||||
world.watched = std::mem::take(&mut self.world.watched);
|
||||
self.streaming = streaming;
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
//! take time run as a task advanced every frame and reply when they finish or
|
||||
//! time out, so `horizon-ctl break 10 64 3` returns what the block dropped.
|
||||
use super::{Game, Screen};
|
||||
use horizon_client::{Result, block_entity_anim::Ticker, items::ItemStack, widgets};
|
||||
use horizon_client::{
|
||||
Result,
|
||||
block_entity_anim::Ticker,
|
||||
chunk::{MIN_Y, SECTIONS},
|
||||
items::ItemStack,
|
||||
widgets,
|
||||
};
|
||||
use horizon_core::geometry::Vec3;
|
||||
use horizon_world::registry::BlockStateId;
|
||||
use serde_json::{Value, json};
|
||||
@@ -27,6 +33,7 @@ pub(super) const HELP: &[&str] = &[
|
||||
"entities [RADIUS] [TYPE] mobs, dropped items and orbs near the player, nearest first",
|
||||
"block X Y Z the block state at a position",
|
||||
"blocks X1 Y1 Z1 X2 Y2 Z2 the non-air blocks of a box (at most 32768 blocks)",
|
||||
"light X1 Y1 Z1 X2 Y2 Z2 the stored sky and block light of a box (at most 32768 blocks)",
|
||||
"find-block BLOCK [RADIUS] the nearest loaded blocks of a kind (default radius 32)",
|
||||
"inventory hotbar, main inventory, armor, offhand and carried stack",
|
||||
"screen the open screen or menu, its slots and its buttons",
|
||||
@@ -962,6 +969,35 @@ impl Game<'_> {
|
||||
}
|
||||
ok(json!({ "from": lo, "to": hi, "counts": census, "blocks": list }))
|
||||
}
|
||||
"light" => {
|
||||
self.need_world()?;
|
||||
let (p, q) = (a.block_pos(0)?, a.block_pos(3)?);
|
||||
let lo = [0, 1, 2].map(|i| p[i].min(q[i]));
|
||||
let hi = [0, 1, 2].map(|i| p[i].max(q[i]));
|
||||
let volume: i64 = (0..3).map(|i| i64::from(hi[i] - lo[i] + 1)).product();
|
||||
if volume > 32768 {
|
||||
return Err(format!("{volume} blocks; at most 32768 per request").into());
|
||||
}
|
||||
// One hex digit per block, x fastest, then z, then y; `.`
|
||||
// outside the loaded chunks.
|
||||
let (mut sky, mut block) = (String::new(), String::new());
|
||||
for y in lo[1]..=hi[1] {
|
||||
for z in lo[2]..=hi[2] {
|
||||
for x in lo[0]..=hi[0] {
|
||||
let packed = self
|
||||
.world
|
||||
.chunks
|
||||
.get(&(x >> 4, z >> 4))
|
||||
.filter(|_| (MIN_Y..MIN_Y + SECTIONS as i32 * 16).contains(&y))
|
||||
.map(|c| c.light(x & 15, y, z & 15));
|
||||
let digit = |v: u8| char::from_digit(u32::from(v), 16).unwrap_or('.');
|
||||
sky.push(packed.map_or('.', |v| digit(v >> 4)));
|
||||
block.push(packed.map_or('.', |v| digit(v & 15)));
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(json!({ "from": lo, "to": hi, "sky": sky, "block": block }))
|
||||
}
|
||||
"find-block" => {
|
||||
self.need_world()?;
|
||||
let want = namespaced(a.get(0, "BLOCK")?);
|
||||
|
||||
@@ -504,6 +504,7 @@ impl Game<'_> {
|
||||
let hub = Hub::new(lan, sync, encoder, config);
|
||||
let port = hub.lan.port;
|
||||
self.world.block_log = Some(Vec::new());
|
||||
self.world.light_log = Some(Default::default());
|
||||
self.lan = Some(LanHost {
|
||||
hub,
|
||||
settings: LanSettings { port, ..settings },
|
||||
@@ -553,6 +554,7 @@ impl Game<'_> {
|
||||
// Give the writer threads a moment to flush the disconnect packets.
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
self.world.block_log = None;
|
||||
self.world.light_log = None;
|
||||
self.audio.capture = None;
|
||||
self.lan_events.clear();
|
||||
self.streaming.extra.clear();
|
||||
|
||||
@@ -49,6 +49,9 @@ fn remote_text(tag: &Tag) -> horizon_client::text::Text {
|
||||
comp_text(&tag_to_json(tag))
|
||||
}
|
||||
|
||||
/// A section's sky and block light layers from the wire.
|
||||
type LightLayers = (Option<Vec<u8>>, Option<Vec<u8>>);
|
||||
|
||||
/// Streaming needs a generator; a server world never generates.
|
||||
struct NoTerrain;
|
||||
impl ChunkGenerator for NoTerrain {
|
||||
@@ -771,8 +774,64 @@ impl Game<'_> {
|
||||
PalettedContainer::from_values(BIOME_STRATEGY, &cells);
|
||||
}
|
||||
}
|
||||
// ClientPacketListener.applyLightData: the column keeps the light
|
||||
// the server sent; nothing is relit.
|
||||
let (layers, above) = self.wire_light(&p.light);
|
||||
let sky = r.dimension.has_skylight;
|
||||
horizon_client::light::light_from_layers(&mut chunk, &layers, above, sky);
|
||||
Ok(chunk)
|
||||
}
|
||||
/// `ClientPacketListener.readSectionList`: the sky and block layers of a
|
||||
/// light packet, by section of the client's column, and the sky light
|
||||
/// above the column's top section (from the light section just above
|
||||
/// it, else open sky).
|
||||
fn wire_light(
|
||||
&self,
|
||||
light: &horizon_protocol::chunk::LightData,
|
||||
) -> (Vec<LightLayers>, [u8; 256]) {
|
||||
let mut layers: Vec<LightLayers> = vec![(None, None); SECTIONS];
|
||||
let Some(r) = self.remote.as_ref() else {
|
||||
return (layers, [0; 256]);
|
||||
};
|
||||
let sky = r.dimension.has_skylight;
|
||||
let mut above = [if sky { 15 } else { 0 }; 256];
|
||||
// Light section b is the dimension's section b - 1.
|
||||
let first = (r.dimension.min_y - MIN_Y).div_euclid(16) - 1;
|
||||
for (b, layer) in light
|
||||
.layers(r.dimension.sections() + 2)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let si = first + b as i32;
|
||||
if (0..SECTIONS as i32).contains(&si) {
|
||||
layers[si as usize] = layer;
|
||||
} else if si == SECTIONS as i32
|
||||
&& sky
|
||||
&& let Some(l) = &layer.0
|
||||
{
|
||||
for (i, a) in above.iter_mut().enumerate() {
|
||||
*a = (l[i >> 1] >> ((i & 1) * 4)) & 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
(layers, above)
|
||||
}
|
||||
/// `handleLightUpdatePacket`: the sections it names take its light.
|
||||
fn remote_light_update(&mut self, p: &play::LightUpdate) {
|
||||
let Some(sky) = self.remote.as_ref().map(|r| r.dimension.has_skylight) else {
|
||||
return;
|
||||
};
|
||||
if !self.world.chunks.contains_key(&(p.x, p.z)) {
|
||||
return;
|
||||
}
|
||||
let (layers, _) = self.wire_light(&p.light);
|
||||
let Some(chunk) = self.world.chunks.get_mut(&(p.x, p.z)) else {
|
||||
return;
|
||||
};
|
||||
let changed =
|
||||
horizon_client::light::update_light_layers(Arc::make_mut(chunk), &layers, sky);
|
||||
Streaming::light_updated(&mut self.world, p.x, p.z, &changed);
|
||||
}
|
||||
/// `ClientChunkCache.Storage.inRange`: within the storage around the
|
||||
/// view centre, `calculateStorageRange` (the server's radius, at least
|
||||
/// two, plus three) chunks each way.
|
||||
@@ -1219,6 +1278,7 @@ impl Game<'_> {
|
||||
}
|
||||
Err(e) => log::warn!("Horizon: chunk {} {}: {e}", p.x, p.z),
|
||||
},
|
||||
C::LightUpdate(p) => self.remote_light_update(&p),
|
||||
C::ForgetLevelChunk(p) => {
|
||||
let [x, z] = p.pos;
|
||||
self.streaming.forget(&mut self.world, x, z);
|
||||
|
||||
@@ -480,6 +480,33 @@ impl ChunkEncoder {
|
||||
w.data
|
||||
}
|
||||
|
||||
/// ClientboundLightUpdatePacket for the light sections over `changed` (a
|
||||
/// bit per section of the column, `sectionLightChanged`), as vanilla's
|
||||
/// `ClientboundLightUpdatePacketData` writes them, with both kinds of
|
||||
/// each section (Horizon's light engine doesn't tell them apart; the
|
||||
/// client keeps the same light either way). A section changed as its
|
||||
/// last block went (light dimming around a removed block) is sent too:
|
||||
/// a client not tracking the block would otherwise keep its old light.
|
||||
pub fn light_update(&self, chunk: &Chunk, changed: u32) -> Vec<u8> {
|
||||
let first = self.sections.start;
|
||||
let lights: Vec<(usize, SectionLight)> = self
|
||||
.light_sections(chunk, u64::MAX)
|
||||
.into_iter()
|
||||
.filter(|&(b, _)| {
|
||||
// Light section b holds the column's section first + b - 1.
|
||||
(1..=self.sections.len()).contains(&b) && changed >> (first + b - 1) & 1 != 0
|
||||
})
|
||||
.collect();
|
||||
horizon_protocol::packets::play::Clientbound::LightUpdate(
|
||||
horizon_protocol::packets::play::LightUpdate {
|
||||
x: chunk.x,
|
||||
z: chunk.z,
|
||||
light: light_data_of(&lights, self.sky),
|
||||
},
|
||||
)
|
||||
.encode()
|
||||
}
|
||||
|
||||
pub fn encode(
|
||||
&self,
|
||||
chunk: &Chunk,
|
||||
|
||||
@@ -988,18 +988,23 @@ impl Hub {
|
||||
.any(|g| g.dimension == self.config.dimension)
|
||||
{
|
||||
let _ = ctx.world.block_log.take();
|
||||
let _ = ctx.world.light_log.take();
|
||||
}
|
||||
return events;
|
||||
}
|
||||
if ctx.world.block_log.is_none() {
|
||||
ctx.world.block_log = Some(Vec::new());
|
||||
}
|
||||
if ctx.world.light_log.is_none() {
|
||||
ctx.world.light_log = Some(Default::default());
|
||||
}
|
||||
self.sync_world(ctx);
|
||||
for i in 0..self.guests.len() {
|
||||
if self.here(&self.guests[i]) {
|
||||
self.update_chunks(i, ctx);
|
||||
}
|
||||
}
|
||||
self.broadcast_light_changes(ctx.world);
|
||||
self.broadcast_block_changes(ctx.world);
|
||||
// ServerGamePacketListenerImpl.tick: the actions handled since the last tick are
|
||||
// acknowledged after the block changes they made were sent, so a client's predicted
|
||||
@@ -1234,6 +1239,42 @@ impl Hub {
|
||||
}
|
||||
}
|
||||
|
||||
/// `ChunkHolder.broadcastChanges`' light part: the light sections that
|
||||
/// changed in a column go to the players for whom it is on the border of
|
||||
/// what they track (`ChunkMap.isChunkOnTrackedBorder`); the others light
|
||||
/// the blocks they are sent themselves.
|
||||
fn broadcast_light_changes(&mut self, world: &mut World) {
|
||||
let Some(log) = world.light_log.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if log.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut changes: Vec<((i32, i32), u32)> = log.drain().collect();
|
||||
changes.sort_unstable();
|
||||
for ((x, z), sections) in changes {
|
||||
let tracked = |g: &Guest, x: i32, z: i32| g.sent_chunks.contains(&(x, z));
|
||||
let viewers: Vec<usize> = (0..self.guests.len())
|
||||
.filter(|&i| {
|
||||
let g = &self.guests[i];
|
||||
self.here(g)
|
||||
&& tracked(g, x, z)
|
||||
&& (-1..=1).any(|dz| (-1..=1).any(|dx| !tracked(g, x + dx, z + dz)))
|
||||
})
|
||||
.collect();
|
||||
if viewers.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some(chunk) = world.chunks.get(&(x, z)) else {
|
||||
continue;
|
||||
};
|
||||
let body = self.encoder.light_update(chunk, sections);
|
||||
for &i in &viewers {
|
||||
self.guests[i].sender.send(body.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn broadcast_block_changes(&mut self, world: &mut World) {
|
||||
let Some(log) = world.block_log.as_mut() else {
|
||||
return;
|
||||
|
||||
@@ -381,6 +381,81 @@ pub fn apply_relighting(chunk: &mut Chunk, lights: Vec<Light>) -> Vec<(i32, i32,
|
||||
changed
|
||||
}
|
||||
|
||||
/// `DataLayer.get`: the nibble at index `i` (`y << 8 | z << 4 | x`), low first.
|
||||
#[inline]
|
||||
fn nibble(layer: &[u8], i: usize) -> u8 {
|
||||
(layer[i >> 1] >> ((i & 1) * 4)) & 15
|
||||
}
|
||||
|
||||
/// A column's light as vanilla stores it, two DataLayers per section (sky,
|
||||
/// block; `layers[i]` for section `i`, `None` where no layer is stored), into
|
||||
/// the sections' packed light, which then needs no light job. A missing sky
|
||||
/// layer repeats the bottom layer of the nearest stored one above it, and
|
||||
/// `above` (per `z << 4 | x`) above the highest
|
||||
/// (`SkyLightSectionStorage.getLightValue`); a missing block layer is dark.
|
||||
pub fn light_from_layers<L: AsRef<[u8]>>(
|
||||
chunk: &mut Chunk,
|
||||
layers: &[(Option<L>, Option<L>)],
|
||||
mut above: [u8; 256],
|
||||
sky_light: bool,
|
||||
) {
|
||||
for (si, (sky, block)) in layers.iter().enumerate().take(SECTIONS).rev() {
|
||||
let sky = sky.as_ref().filter(|_| sky_light);
|
||||
let section = Arc::make_mut(&mut chunk.sections[si]);
|
||||
if sky.is_none() && block.is_none() && above.iter().all(|&v| v == above[0]) {
|
||||
section.light = Light::Uniform(above[0] << 4);
|
||||
section.compact();
|
||||
continue;
|
||||
}
|
||||
let mut data = Box::new([0u8; 4096]);
|
||||
for (i, packed) in data.iter_mut().enumerate() {
|
||||
let s = sky.map_or(above[i & 255], |a| nibble(a.as_ref(), i));
|
||||
let b = block.as_ref().map_or(0, |a| nibble(a.as_ref(), i));
|
||||
*packed = s << 4 | b;
|
||||
}
|
||||
for (i, a) in above.iter_mut().enumerate() {
|
||||
*a = data[i] >> 4;
|
||||
}
|
||||
section.light = Light::Full(data);
|
||||
section.compact();
|
||||
}
|
||||
chunk.lit = true;
|
||||
chunk.light_loaded = true;
|
||||
}
|
||||
|
||||
/// A light update's layers (`LevelLightEngine.queueSectionData`): each one
|
||||
/// given replaces that kind of light in its section, the other kind stays.
|
||||
/// Returns the sections whose light changed.
|
||||
pub fn update_light_layers<L: AsRef<[u8]>>(
|
||||
chunk: &mut Chunk,
|
||||
layers: &[(Option<L>, Option<L>)],
|
||||
sky_light: bool,
|
||||
) -> Vec<usize> {
|
||||
let mut changed = Vec::new();
|
||||
for (si, (sky, block)) in layers.iter().enumerate().take(SECTIONS) {
|
||||
let sky = sky.as_ref().filter(|_| sky_light);
|
||||
if sky.is_none() && block.is_none() {
|
||||
continue;
|
||||
}
|
||||
let old = &chunk.sections[si].light;
|
||||
let mut data = Box::new([0u8; 4096]);
|
||||
for (i, packed) in data.iter_mut().enumerate() {
|
||||
let was = old.get(i);
|
||||
let s = sky.map_or(was >> 4, |a| nibble(a.as_ref(), i));
|
||||
let b = block.as_ref().map_or(was & 15, |a| nibble(a.as_ref(), i));
|
||||
*packed = s << 4 | b;
|
||||
}
|
||||
if (0..4096).all(|i| old.get(i) == data[i]) {
|
||||
continue;
|
||||
}
|
||||
let section = Arc::make_mut(&mut chunk.sections[si]);
|
||||
section.light = Light::Full(data);
|
||||
section.compact();
|
||||
changed.push(si);
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// Resident chunks the incremental relighter reads and writes.
|
||||
pub trait LightWorld {
|
||||
fn state(&self, x: i32, y: i32, z: i32) -> Option<u32>;
|
||||
@@ -809,6 +884,44 @@ mod tests {
|
||||
assert!(copied <= 3, "{copied} sections copied");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_layers_fill_the_column_as_vanilla_reads_them() {
|
||||
// Section 10's sky layer is 7 on its bottom row and 3 above; no
|
||||
// layers anywhere else; a block layer of 5 in section 2.
|
||||
let mut sky = vec![0x33u8; 2048];
|
||||
sky[..128].fill(0x77);
|
||||
let mut layers: Vec<(Option<Vec<u8>>, Option<Vec<u8>>)> = vec![(None, None); SECTIONS];
|
||||
layers[10].0 = Some(sky);
|
||||
layers[2].1 = Some(vec![0x55; 2048]);
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
light_from_layers(&mut chunk, &layers, [15; 256], true);
|
||||
let at = |c: &Chunk, si: usize, i: usize| c.sections[si].light(i);
|
||||
assert!(chunk.lit && chunk.light_loaded);
|
||||
assert!(
|
||||
matches!(chunk.sections[11].light, Light::Uniform(0xF0)),
|
||||
"above: open sky"
|
||||
);
|
||||
assert_eq!((at(&chunk, 10, 0), at(&chunk, 10, 4095)), (0x70, 0x30));
|
||||
// Below, each section repeats the bottom row of the stored one above.
|
||||
assert!(matches!(chunk.sections[9].light, Light::Uniform(0x70)));
|
||||
assert_eq!(at(&chunk, 2, 100), 0x75, "block light only where stored");
|
||||
assert!(matches!(chunk.sections[0].light, Light::Uniform(0x70)));
|
||||
|
||||
// A light update replaces one kind of light in the sections it names.
|
||||
let mut update: Vec<(Option<Vec<u8>>, Option<Vec<u8>>)> = vec![(None, None); SECTIONS];
|
||||
update[2].1 = Some(vec![0; 2048]);
|
||||
update[3].0 = Some(vec![0x77; 2048]);
|
||||
let changed = update_light_layers(&mut chunk, &update, true);
|
||||
assert_eq!(changed, vec![2]);
|
||||
assert!(matches!(chunk.sections[2].light, Light::Uniform(0x70)));
|
||||
update[3].0 = Some(vec![0xFF; 2048]);
|
||||
assert_eq!(
|
||||
update_light_layers(&mut chunk, &update, false),
|
||||
Vec::<usize>::new(),
|
||||
"no sky storage"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_chunk_missing_neighbours_and_no_sky() {
|
||||
let mut chunk = Chunk::empty(0, 0);
|
||||
|
||||
@@ -1278,31 +1278,8 @@ pub fn load_light(tag: &Tag, chunk: &mut Chunk, sky_light: bool) {
|
||||
};
|
||||
layers[si as usize] = (layer("SkyLight"), layer("BlockLight"));
|
||||
}
|
||||
let sky_default = if sky_light { 15u8 } else { 0 };
|
||||
// Sky nibbles of the section above's bottom layer, going down.
|
||||
let mut above: [u8; 256] = [sky_default; 256];
|
||||
for si in (0..SECTIONS).rev() {
|
||||
let (sky, block) = layers[si];
|
||||
let mut data = Box::new([0u8; 4096]);
|
||||
let nibble = |a: &[u8], i: usize| (a[i >> 1] >> ((i & 1) * 4)) & 15;
|
||||
for (i, packed) in data.iter_mut().enumerate() {
|
||||
let s = match sky {
|
||||
Some(a) if sky_light => nibble(a, i),
|
||||
_ => above[i & 255],
|
||||
};
|
||||
let b = block.map_or(0, |a| nibble(a, i));
|
||||
*packed = s << 4 | b;
|
||||
}
|
||||
for (i, a) in above.iter_mut().enumerate() {
|
||||
*a = data[i] >> 4;
|
||||
}
|
||||
let mut section = (*chunk.sections[si]).clone();
|
||||
section.light = Light::Full(data);
|
||||
section.compact();
|
||||
chunk.sections[si] = Arc::new(section);
|
||||
}
|
||||
chunk.lit = true;
|
||||
chunk.light_loaded = true;
|
||||
let above = [if sky_light { 15 } else { 0 }; 256];
|
||||
crate::light::light_from_layers(chunk, &layers, above, sky_light);
|
||||
}
|
||||
|
||||
type RegionCache = Mutex<HashMap<(i32, i32), Option<Arc<Mutex<RegionFile>>>>>;
|
||||
|
||||
@@ -502,16 +502,38 @@ impl Streaming {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A column received from a server: store it and relight it and its neighbours.
|
||||
/// A column received from a server, with the light it sent
|
||||
/// (`ClientPacketListener.handleLevelChunkWithLight`): nothing is
|
||||
/// relit, and it and its neighbours remesh (`enableChunkLight`).
|
||||
pub fn insert_remote(&mut self, world: &mut World, chunk: Chunk) {
|
||||
let key = (chunk.x, chunk.z);
|
||||
self.lit.remove(&key);
|
||||
self.lighting.remove(&key);
|
||||
self.light_snapshots.remove(&key);
|
||||
world.chunks.insert(key, Arc::new(chunk));
|
||||
self.lit.insert(key);
|
||||
self.generated_total += 1;
|
||||
for dz in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
world.dirty_light.insert((key.0 + dx, key.1 + dz));
|
||||
for sy in 0..SECTIONS as i32 {
|
||||
world.dirty_sections.insert((key.0 + dx, sy, key.1 + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A server's light update for a resident column: the sections whose
|
||||
/// light changed and their neighbours remesh
|
||||
/// (`setSectionDirtyWithNeighbors`).
|
||||
pub fn light_updated(world: &mut World, x: i32, z: i32, sections: &[usize]) {
|
||||
for &sy in sections {
|
||||
for dz in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
let s = sy as i32 + dy;
|
||||
if (0..SECTIONS as i32).contains(&s) {
|
||||
world.dirty_sections.insert((x + dx, s, z + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1148,6 +1170,11 @@ impl Streaming {
|
||||
/// Sections whose meshes sample a changed light value (faces and corners
|
||||
/// read light from neighbouring blocks).
|
||||
fn mark_light_changes(world: &mut World, changed: &[(i32, i32, i32)]) {
|
||||
if let Some(log) = &mut world.light_log {
|
||||
for &(x, y, z) in changed {
|
||||
*log.entry((x >> 4, z >> 4)).or_default() |= 1 << ((y - crate::chunk::MIN_Y) >> 4);
|
||||
}
|
||||
}
|
||||
let mut last = None;
|
||||
for &(x, y, z) in changed {
|
||||
for dz in -1..=1 {
|
||||
|
||||
@@ -47,6 +47,10 @@ pub struct World {
|
||||
/// Every block position written since the log was last drained, kept
|
||||
/// only while the world is open to the network (see `lan`).
|
||||
pub block_log: Option<Vec<(i32, i32, i32)>>,
|
||||
/// The sections whose light changed since the log was last drained, as
|
||||
/// a bit per section of each column, kept while `block_log` is
|
||||
/// (`ChunkHolder.sectionLightChanged`).
|
||||
pub light_log: Option<crate::fast_hash::FxHashMap<(i32, i32), u32>>,
|
||||
/// The villager POIs of each section (`PoiManager`'s sections).
|
||||
pub pois: crate::poi_sections::PoiSections<crate::mobs::villager_brain::Poi>,
|
||||
}
|
||||
@@ -133,6 +137,7 @@ impl World {
|
||||
day_time: 1000,
|
||||
weather: Weather::default(),
|
||||
block_log: None,
|
||||
light_log: None,
|
||||
pois: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ game accepts, test hooks included.
|
||||
- **Observe:** `state` (position, rotation, velocity, health, food, saturation,
|
||||
XP, effects, game mode, dimension, time, weather, the screen and what the
|
||||
crosshair points at), `looking-at`, `entities [RADIUS] [TYPE]` (mobs with
|
||||
ids and health, dropped items, orbs), `block X Y Z`, `blocks X1 Y1 Z1 X2 Y2 Z2`,
|
||||
ids and health, dropped items, orbs), `block X Y Z`, `blocks X1 Y1 Z1 X2 Y2 Z2`, `light X1 Y1 Z1 X2 Y2 Z2`,
|
||||
`find-block BLOCK [RADIUS]`, `inventory` (stacks with their components as
|
||||
SNBT), `screen` (the open screen or title menu, its buttons and slots) and
|
||||
`messages` (chat, title, subtitle and action bar).
|
||||
|
||||
+71
-2
@@ -49,6 +49,9 @@ address. The script then checks, from both sides, what a guest's game used to ge
|
||||
guest as the host has them (kind, place, rotation, what they hold or wear, the variant).
|
||||
- projectiles: primed TNT (one falling), an end crystal and an arrow that lands reach the guest where
|
||||
the host has them; a falling block lands on both sides.
|
||||
- light: the guest's light matches the host's: around a torch placed near it, under a roof and
|
||||
once a block of the roof is removed under the sky, and in the last chunk it tracks next to
|
||||
glowstone in a chunk it doesn't (only the server's light updates bring that one).
|
||||
|
||||
Each check is reported on its own (a failing one doesn't stop the others) and the screenshots of
|
||||
both sides go to --out (host-*.png, guest-*.png).
|
||||
@@ -81,7 +84,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
GUEST_NAME = 'Guest'
|
||||
# The host plays single player on its server thread (--server-thread).
|
||||
SERVER_THREAD = False
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'containers', 'feedback', 'titles', 'commands', 'recipes', 'trials', 'clicks', 'sleep', 'push', 'eat', 'xp', 'objects', 'projectiles', 'effects', 'portals', 'distance']
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'containers', 'feedback', 'titles', 'commands', 'recipes', 'trials', 'clicks', 'sleep', 'push', 'eat', 'xp', 'objects', 'projectiles', 'light', 'effects', 'portals', 'distance']
|
||||
|
||||
|
||||
class Game:
|
||||
@@ -1201,6 +1204,71 @@ def check_projectiles(host, guest, out):
|
||||
return f'{len(theirs)} identical, fuses (host, guest) {fuses}'
|
||||
|
||||
|
||||
def light_differences(host, guest, lo, hi):
|
||||
"""Positions of the box loaded on both sides, and those whose sky or block light differ."""
|
||||
h, g = host.raw('light', *lo, *hi), guest.raw('light', *lo, *hi)
|
||||
both = [i for i in range(len(h['sky'])) if h['sky'][i] != '.' and g['sky'][i] != '.']
|
||||
differ = [i for i in both if (h['sky'][i], h['block'][i]) != (g['sky'][i], g['block'][i])]
|
||||
return h, g, both, differ
|
||||
|
||||
|
||||
def same_light(host, guest, lo, hi, what, ticks=100):
|
||||
"""Wait until the guest's light over the box is the host's; the number of positions compared."""
|
||||
for _ in range(ticks // 10):
|
||||
h, g, both, differ = light_differences(host, guest, lo, hi)
|
||||
if not differ:
|
||||
assert both, f'{what}: no position of the box is loaded on both sides'
|
||||
return len(both)
|
||||
host.wait(10)
|
||||
i = differ[0]
|
||||
nx, nz = hi[0] - lo[0] + 1, hi[2] - lo[2] + 1
|
||||
pos = (lo[0] + i % nx, lo[1] + i // (nx * nz), lo[2] + i // nx % nz)
|
||||
raise AssertionError(f'{what}: {len(differ)} of {len(both)} positions differ, first {pos}: host sky '
|
||||
f'{h["sky"][i]} block {h["block"][i]}, guest sky {g["sky"][i]} block {g["block"][i]}')
|
||||
|
||||
|
||||
def check_light(host, guest, out):
|
||||
gx, gy, gz = (int(v // 1) for v in pos_of(guest))
|
||||
results = {}
|
||||
# A torch near the guest: the guest lights it itself, as vanilla's client does.
|
||||
torch = (gx + 3, gy, gz + 3)
|
||||
host('command', command=f'setblock {torch[0]} {torch[1]} {torch[2]} minecraft:torch')
|
||||
try:
|
||||
results['torch'] = same_light(host, guest, (gx - 12, gy - 1, gz - 12), (gx + 16, gy + 3, gz + 16), 'torch')
|
||||
finally:
|
||||
host('command', command=f'setblock {torch[0]} {torch[1]} {torch[2]} minecraft:air')
|
||||
# A roof over the guest, then a hole in it: sky light falls through.
|
||||
roof = f'{gx - 3} {gy + 4} {gz - 3} {gx + 3} {gy + 4} {gz + 3}'
|
||||
box = (gx - 6, gy - 1, gz - 6), (gx + 6, gy + 5, gz + 6)
|
||||
host('command', command=f'fill {roof} minecraft:stone')
|
||||
try:
|
||||
results['roof'] = same_light(host, guest, *box, 'roof')
|
||||
host('command', command=f'setblock {gx} {gy + 4} {gz} minecraft:air')
|
||||
results['hole'] = same_light(host, guest, *box, 'hole in the roof')
|
||||
finally:
|
||||
host('command', command=f'fill {roof} minecraft:air')
|
||||
# The last chunk the guest tracks eastwards, and glowstone high up just past it in a chunk it
|
||||
# doesn't track: the guest never sees that block, only the light updates of its border chunk.
|
||||
y = 200
|
||||
row = guest.raw('light', gx, y, gz, gx + 16 * 24, y, gz)['sky']
|
||||
assert '.' in row, 'the guest has every chunk 24 chunks east'
|
||||
untracked = (gx + row.index('.')) >> 4
|
||||
near = untracked * 16 + 1
|
||||
# On its server thread the host's own game is a client too, sent the same light updates.
|
||||
if not SERVER_THREAD:
|
||||
assert host.raw('light', near, y, gz, near, y, gz)['sky'] != '.', 'the host lacks the chunk past the guest'
|
||||
host('command', command=f'setblock {near} {y} {gz} minecraft:glowstone')
|
||||
try:
|
||||
lo, hi = (untracked * 16 - 16, y - 3, gz - 8), (untracked * 16 - 1, y + 3, gz + 8)
|
||||
results['border'] = same_light(host, guest, lo, hi, 'border chunk')
|
||||
lit = guest.raw('light', near - 2, y, gz, near - 2, y, gz)['block']
|
||||
assert lit == 'd', f'the border chunk by the glowstone has block light {lit}, not 13'
|
||||
finally:
|
||||
host('command', command=f'setblock {near} {y} {gz} minecraft:air')
|
||||
results['border_cleared'] = same_light(host, guest, lo, hi, 'border chunk without the glowstone')
|
||||
return results
|
||||
|
||||
|
||||
CHECK_FNS = {'tab': check_tab, 'self': check_self, 'armour': check_armour, 'nametags': check_nametags,
|
||||
'chest': check_chest, 'crafting': check_crafting, 'sign': check_sign, 'damage': check_damage,
|
||||
'mobs': check_mobs, 'hazards': check_hazards, 'pvp': check_pvp,
|
||||
@@ -1209,7 +1277,8 @@ CHECK_FNS = {'tab': check_tab, 'self': check_self, 'armour': check_armour, 'name
|
||||
'recipes': check_recipes, 'trials': check_trials, 'walls': check_walls,
|
||||
'effects': check_effects, 'portals': check_portals, 'distance': check_distance, 'clicks': check_clicks,
|
||||
'sleep': check_sleep, 'push': check_push, 'eat': check_eat,
|
||||
'xp': check_xp, 'objects': check_objects, 'projectiles': check_projectiles}
|
||||
'xp': check_xp, 'objects': check_objects, 'projectiles': check_projectiles,
|
||||
'light': check_light}
|
||||
|
||||
|
||||
def run(ctl, out, resources, checks):
|
||||
|
||||
Reference in new issue
Block a user