Synchronize world chunk index updates

This commit is contained in:
Tanner
2026-11-08 17:14:22 +00:00
parent c79ee932a3
commit ea887c7019
3 changed files with 154 additions and 100 deletions
+123 -96
View File
@@ -1,4 +1,5 @@
import json import json
import threading
import time import time
from math import floor from math import floor
from munch import Munch from munch import Munch
@@ -218,6 +219,7 @@ class WorldState:
def __init__(self, min_y=-64): def __init__(self, min_y=-64):
self.min_y = min_y self.min_y = min_y
self.min_section = min_y // 16 self.min_section = min_y // 16
self.lock = threading.RLock()
self.chunks = {} self.chunks = {}
self.index = {} self.index = {}
self.block_entities = {} self.block_entities = {}
@@ -229,68 +231,84 @@ class WorldState:
self.player_id = None self.player_id = None
def set_dimension(self, dimension, dimension_type=None): def set_dimension(self, dimension, dimension_type=None):
if dimension: with self.lock:
self.dimension = dimension if dimension:
if dimension_type: self.dimension = dimension
min_y = dimension_type.get('min_y', self.min_y) if dimension_type:
if isinstance(min_y, int): min_y = dimension_type.get('min_y', self.min_y)
self.min_y = min_y if isinstance(min_y, int):
self.min_section = min_y // 16 self.min_y = min_y
self.min_section = min_y // 16
def load_chunk(self, packet): def load_chunk(self, packet):
# Refresh selectors in case block metadata was reloaded before login. with self.lock:
from mosfet.info import blocks # Refresh selectors in case block metadata was reloaded before login.
self.indexed_ids = blocks.INDEXED_IDS from mosfet.info import blocks
self.unload_chunk(packet.x, packet.z) self.indexed_ids = blocks.INDEXED_IDS
chunk = ChunkColumn(packet.x, packet.z, packet.sections, self.unload_chunk(packet.x, packet.z)
self.min_section) chunk = ChunkColumn(packet.x, packet.z, packet.sections,
self.chunks[(packet.x, packet.z)] = chunk self.min_section)
for entity in getattr(packet, 'block_entities', []): self.chunks[(packet.x, packet.z)] = chunk
pos = (entity['x'], entity['y'], entity['z']) for entity in getattr(packet, 'block_entities', []):
self.set_block_entity( pos = (entity['x'], entity['y'], entity['z'])
pos, entity['type'], entity.get('data')) self.set_block_entity(
for section_y, section in chunk.sections.items(): pos, entity['type'], entity.get('data'))
palette = section['blocks'] for section_y, section in chunk.sections.items():
if not self.indexed_ids.intersection( palette = section['blocks']
palette.palette or [palette.get(i) for i in range(palette.size)]): if not self.indexed_ids.intersection(
continue palette.palette or [palette.get(i)
for index in range(palette.size): for i in range(palette.size)]):
state_id = palette.get(index)
if state_id not in self.indexed_ids:
continue continue
local_x = index & 0xF for index in range(palette.size):
local_z = (index >> 4) & 0xF state_id = palette.get(index)
local_y = (index >> 8) & 0xF if state_id not in self.indexed_ids:
pos = (packet.x * 16 + local_x, continue
section_y * 16 + local_y, local_x = index & 0xF
packet.z * 16 + local_z) local_z = (index >> 4) & 0xF
self.index.setdefault(state_id, set()).add(pos) local_y = (index >> 8) & 0xF
pos = (packet.x * 16 + local_x,
section_y * 16 + local_y,
packet.z * 16 + local_z)
self.index.setdefault(state_id, set()).add(pos)
def handle_block_update(self, packet): def handle_block_update(self, packet):
self.set_block_at(packet.x, packet.y, packet.z, packet.block_state_id) self.set_block_at(packet.x, packet.y, packet.z, packet.block_state_id)
def handle_section_update(self, packet): def handle_section_update(self, packet):
for x, y, z, state_id in packet.changes: with self.lock:
self.set_block_at(x, y, z, state_id) for x, y, z, state_id in packet.changes:
self.set_block_at(x, y, z, state_id)
def get_chunk(self, x, z): def get_chunk(self, x, z):
return self.chunks.get((x, z)) with self.lock:
return self.chunks.get((x, z))
def get_block_at(self, x, y, z): def get_block_at(self, x, y, z):
chunk = self.get_chunk(floor(x / 16), floor(z / 16)) with self.lock:
if chunk is None: chunk = self.chunks.get((floor(x / 16), floor(z / 16)))
return None if chunk is None:
return chunk.get_block_at(floor(x), floor(y), floor(z)) return None
return chunk.get_block_at(floor(x), floor(y), floor(z))
def indexed_positions(self, block_ids):
with self.lock:
return [position for block_id in block_ids
for position in tuple(self.index.get(block_id, ()))]
def block_entities_snapshot(self):
with self.lock:
return dict(self.block_entities)
def set_block_entity(self, position, entity_type, data): def set_block_entity(self, position, entity_type, data):
position = tuple(position) position = tuple(position)
if data is None: with self.lock:
self.block_entities.pop(position, None) if data is None:
return self.block_entities.pop(position, None)
self.block_entities[position] = { return
'type': entity_type, self.block_entities[position] = {
'data': nbt_plain(data), 'type': entity_type,
} 'data': nbt_plain(data),
}
@staticmethod @staticmethod
def _is_sign_state(block_state_id): def _is_sign_state(block_state_id):
@@ -300,67 +318,76 @@ class WorldState:
def set_block_at(self, x, y, z, block_state_id): def set_block_at(self, x, y, z, block_state_id):
x, y, z = floor(x), floor(y), floor(z) x, y, z = floor(x), floor(y), floor(z)
chunk = self.get_chunk(floor(x / 16), floor(z / 16)) with self.lock:
if chunk is None: chunk = self.chunks.get((floor(x / 16), floor(z / 16)))
return False if chunk is None:
previous = chunk.set_block_at(x, y, z, block_state_id) return False
pos = (x, y, z) previous = chunk.set_block_at(x, y, z, block_state_id)
if previous in self.indexed_ids: pos = (x, y, z)
self.index.get(previous, set()).discard(pos) if previous in self.indexed_ids:
if (self._is_sign_state(previous) and self.index.get(previous, set()).discard(pos)
not self._is_sign_state(block_state_id)): if (self._is_sign_state(previous) and
self.block_entities.pop(pos, None) not self._is_sign_state(block_state_id)):
if block_state_id in self.indexed_ids: self.block_entities.pop(pos, None)
self.index.setdefault(block_state_id, set()).add(pos) if block_state_id in self.indexed_ids:
return True self.index.setdefault(block_state_id, set()).add(pos)
return True
def unload_chunk(self, x, z): def unload_chunk(self, x, z):
chunk = self.chunks.pop((x, z), None) with self.lock:
if chunk is None: chunk = self.chunks.pop((x, z), None)
return if chunk is None:
x0, z0 = x * 16, z * 16 return
for state_id, locations in list(self.index.items()): x0, z0 = x * 16, z * 16
self.index[state_id] = { for state_id, locations in list(self.index.items()):
pos for pos in locations remaining = {
if not (x0 <= pos[0] < x0 + 16 and z0 <= pos[2] < z0 + 16) pos for pos in locations.copy()
if not (x0 <= pos[0] < x0 + 16 and
z0 <= pos[2] < z0 + 16)
}
if remaining:
self.index[state_id] = remaining
else:
self.index.pop(state_id, None)
self.block_entities = {
pos: entity for pos, entity in self.block_entities.items()
if not (x0 <= pos[0] < x0 + 16 and
z0 <= pos[2] < z0 + 16)
} }
if not self.index[state_id]:
del self.index[state_id]
self.block_entities = {
pos: entity for pos, entity in self.block_entities.items()
if not (x0 <= pos[0] < x0 + 16 and
z0 <= pos[2] < z0 + 16)
}
def unload_all_chunks(self): def unload_all_chunks(self):
self.chunks.clear() with self.lock:
self.index.clear() self.chunks.clear()
self.block_entities.clear() self.index.clear()
self.block_entities.clear()
def get_loaded_area(self, ignore_empty=False): def get_loaded_area(self, ignore_empty=False):
if not self.chunks: with self.lock:
return None if not self.chunks:
xs = [pos[0] for pos in self.chunks] return None
zs = [pos[1] for pos in self.chunks] xs = [pos[0] for pos in self.chunks]
ys = [section_y for chunk in self.chunks.values() zs = [pos[1] for pos in self.chunks]
for section_y in chunk.sections] ys = [section_y for chunk in self.chunks.values()
return ((min(xs), min(ys), min(zs)), for section_y in chunk.sections]
(max(xs), max(ys), max(zs))) return ((min(xs), min(ys), min(zs)),
(max(xs), max(ys), max(zs)))
def check_loaded(self, chunk_distance=None): def check_loaded(self, chunk_distance=None):
if self.center is None: with self.lock:
return bool(self.chunks) if self.center is None:
radius = self.radius if chunk_distance is None else chunk_distance return bool(self.chunks)
if radius is None: radius = self.radius if chunk_distance is None else chunk_distance
return bool(self.chunks) if radius is None:
cx, cz = self.center return bool(self.chunks)
return all((x, z) in self.chunks cx, cz = self.center
for x in range(cx - radius, cx + radius + 1) return all((x, z) in self.chunks
for z in range(cz - radius, cz + radius + 1)) for x in range(cx - radius, cx + radius + 1)
for z in range(cz - radius, cz + radius + 1))
@property @property
def loaded_chunk_count(self): def loaded_chunk_count(self):
return len(self.chunks) with self.lock:
return len(self.chunks)
def __repr__(self): def __repr__(self):
return ('WorldState(dimension={!r}, chunks={}, min_y={}, center={!r}, ' return ('WorldState(dimension={!r}, chunks={}, min_y={}, center={!r}, '
+12 -4
View File
@@ -60,7 +60,11 @@ class World:
def find_signs(self, center, distance=16): def find_signs(self, center, distance=16):
"""Return nearby loaded signs as position/side/four-lines records.""" """Return nearby loaded signs as position/side/four-lines records."""
result = [] result = []
for position, entity in self.g.chunks.block_entities.items(): chunks = self.g.chunks
entities = (chunks.block_entities_snapshot()
if hasattr(chunks, 'block_entities_snapshot') else
dict(chunks.block_entities))
for position, entity in entities.items():
if utils.phyp(center, position) > distance: if utils.phyp(center, position) > distance:
continue continue
get_block_at = getattr(self.g.chunks, 'get_block_at', None) get_block_at = getattr(self.g.chunks, 'get_block_at', None)
@@ -348,9 +352,13 @@ class World:
yield check yield check
def find_blocks_indexed(self, center, block_ids, distance=0): def find_blocks_indexed(self, center, block_ids, distance=0):
index = [] chunks = self.g.chunks
for bid in block_ids: if hasattr(chunks, 'indexed_positions'):
index.extend(self.g.chunks.index.get(bid, [])) index = chunks.indexed_positions(block_ids)
else:
index = []
for bid in block_ids:
index.extend(chunks.index.get(bid, []))
result = [] result = []
for block in index: for block in index:
+19
View File
@@ -39,6 +39,25 @@ class TestSectionBlockUpdates(unittest.TestCase):
self.assertEqual(packet.id, 0x56) self.assertEqual(packet.id, 0x56)
self.assertEqual(packet.changes, [(-173, 70, -311, bed_id)]) self.assertEqual(packet.changes, [(-173, 70, -311, bed_id)])
def test_unload_chunk_prunes_index_and_block_entities_safely(self):
bed_id = next(iter(blocks.BED_IDS))
inside = (1, 64, 1)
outside = (17, 64, 1)
world = WorldState()
world.chunks[(0, 0)] = ChunkColumn(0, 0, [], world.min_section)
world.index[bed_id] = {inside, outside}
world.block_entities = {
inside: {'type': 1, 'data': {}},
outside: {'type': 1, 'data': {}},
}
snapshot = world.indexed_positions({bed_id})
world.unload_chunk(0, 0)
self.assertEqual(set(snapshot), {inside, outside})
self.assertEqual(world.index[bed_id], {outside})
self.assertEqual(set(world.block_entities), {outside})
def test_section_update_replaces_old_block_and_index_entry(self): def test_section_update_replaces_old_block_and_index_entry(self):
section_x, section_y, section_z = -11, 4, -20 section_x, section_y, section_z = -11, 4, -20
x, y, z = -173, 70, -311 x, y, z = -173, 70, -311