mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-11 07:11:30 +11:00
Anvil file support (blocks and biomes) (#145)
Adds the `valence_anvil` crate for loading anvil worlds. It can only read blocks and biomes currently. Support for saving data is to be added later. Co-authored-by: Ryan <ryanj00a@gmail.com>
This commit is contained in:
parent
8a7782e16f
commit
6de5de57a5
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,3 +13,4 @@ Cargo.lock
|
||||||
flamegraph.svg
|
flamegraph.svg
|
||||||
perf.data
|
perf.data
|
||||||
perf.data.old
|
perf.data.old
|
||||||
|
/valence_anvil/.asset_cache/
|
||||||
|
|
|
@ -41,7 +41,7 @@ valence_protocol = { version = "0.1.0", path = "valence_protocol", features = ["
|
||||||
vek = "0.15.8"
|
vek = "0.15.8"
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.21.1"
|
version = "1.21.2"
|
||||||
features = ["macros", "rt-multi-thread", "net", "io-util", "sync", "time"]
|
features = ["macros", "rt-multi-thread", "net", "io-util", "sync", "time"]
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
|
@ -70,6 +70,7 @@ num = "0.4.0"
|
||||||
members = [
|
members = [
|
||||||
"valence_derive",
|
"valence_derive",
|
||||||
"valence_nbt",
|
"valence_nbt",
|
||||||
|
"valence_anvil",
|
||||||
"valence_protocol",
|
"valence_protocol",
|
||||||
"valence_spatial_index",
|
"valence_spatial_index",
|
||||||
"packet_inspector",
|
"packet_inspector",
|
||||||
|
|
|
@ -171,7 +171,7 @@ impl Config for Game {
|
||||||
let mut in_terrain = false;
|
let mut in_terrain = false;
|
||||||
let mut depth = 0;
|
let mut depth = 0;
|
||||||
|
|
||||||
for y in (0..chunk.height()).rev() {
|
for y in (0..chunk.section_count() * 16).rev() {
|
||||||
let b = terrain_column(
|
let b = terrain_column(
|
||||||
self,
|
self,
|
||||||
block_x,
|
block_x,
|
||||||
|
@ -184,7 +184,7 @@ impl Config for Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add grass
|
// Add grass
|
||||||
for y in (0..chunk.height()).rev() {
|
for y in (0..chunk.section_count() * 16).rev() {
|
||||||
if chunk.block_state(x, y, z).is_air()
|
if chunk.block_state(x, y, z).is_air()
|
||||||
&& chunk.block_state(x, y - 1, z) == BlockState::GRASS_BLOCK
|
&& chunk.block_state(x, y - 1, z) == BlockState::GRASS_BLOCK
|
||||||
{
|
{
|
||||||
|
|
6994
extracted/biomes.json
Normal file
6994
extracted/biomes.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -38,6 +38,7 @@ public class Main implements ModInitializer {
|
||||||
LOGGER.info("Starting extractors...");
|
LOGGER.info("Starting extractors...");
|
||||||
|
|
||||||
var extractors = new Extractor[]{
|
var extractors = new Extractor[]{
|
||||||
|
new Biomes(),
|
||||||
new Blocks(),
|
new Blocks(),
|
||||||
new Enchants(),
|
new Enchants(),
|
||||||
new Entities(),
|
new Entities(),
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
package rs.valence.extractor.extractors;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import net.minecraft.entity.SpawnGroup;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.collection.Weighted;
|
||||||
|
import net.minecraft.util.registry.BuiltinRegistries;
|
||||||
|
import net.minecraft.util.registry.Registry;
|
||||||
|
import net.minecraft.world.biome.BiomeParticleConfig;
|
||||||
|
import rs.valence.extractor.Main;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
public class Biomes implements Main.Extractor {
|
||||||
|
public Biomes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String fileName() {
|
||||||
|
return "biomes.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement extract() {
|
||||||
|
// The biome particle probability field is private.
|
||||||
|
// We have to resort to reflection, unfortunately.
|
||||||
|
Field particleConfigProbabilityField;
|
||||||
|
try {
|
||||||
|
particleConfigProbabilityField = BiomeParticleConfig.class.getDeclaredField("probability");
|
||||||
|
particleConfigProbabilityField.setAccessible(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
var biomesJson = new JsonArray();
|
||||||
|
|
||||||
|
for (var biome : BuiltinRegistries.BIOME) {
|
||||||
|
var biomeIdent = BuiltinRegistries.BIOME.getId(biome);
|
||||||
|
assert biomeIdent != null;
|
||||||
|
|
||||||
|
var biomeJson = new JsonObject();
|
||||||
|
biomeJson.addProperty("precipitation", biome.getPrecipitation().getName());
|
||||||
|
biomeJson.addProperty("temperature", biome.getTemperature());
|
||||||
|
biomeJson.addProperty("downfall", biome.getDownfall());
|
||||||
|
|
||||||
|
var effectJson = new JsonObject();
|
||||||
|
var biomeEffects = biome.getEffects();
|
||||||
|
|
||||||
|
effectJson.addProperty("sky_color", biomeEffects.getSkyColor());
|
||||||
|
effectJson.addProperty("water_fog_color", biomeEffects.getWaterFogColor());
|
||||||
|
effectJson.addProperty("fog_color", biomeEffects.getFogColor());
|
||||||
|
effectJson.addProperty("water_color", biomeEffects.getWaterColor());
|
||||||
|
biomeEffects.getFoliageColor().ifPresent(color -> effectJson.addProperty("foliage_color", color));
|
||||||
|
biomeEffects.getGrassColor().ifPresent(color -> effectJson.addProperty("grass_color", color));
|
||||||
|
effectJson.addProperty("grass_color_modifier", biomeEffects.getGrassColorModifier().getName());
|
||||||
|
biomeEffects.getMusic().ifPresent(biome_music -> {
|
||||||
|
var music = new JsonObject();
|
||||||
|
music.addProperty("replace_current_music", biome_music.shouldReplaceCurrentMusic());
|
||||||
|
music.addProperty("sound", biome_music.getSound().getId().getPath());
|
||||||
|
music.addProperty("max_delay", biome_music.getMaxDelay());
|
||||||
|
music.addProperty("min_delay", biome_music.getMinDelay());
|
||||||
|
effectJson.add("music", music);
|
||||||
|
});
|
||||||
|
|
||||||
|
biomeEffects.getLoopSound().ifPresent(soundEvent -> effectJson.addProperty("ambient_sound", soundEvent.getId().getPath()));
|
||||||
|
biomeEffects.getAdditionsSound().ifPresent(soundEvent -> {
|
||||||
|
var sound = new JsonObject();
|
||||||
|
sound.addProperty("sound", soundEvent.getSound().getId().getPath());
|
||||||
|
sound.addProperty("tick_chance", soundEvent.getChance());
|
||||||
|
effectJson.add("additions_sound", sound);
|
||||||
|
});
|
||||||
|
biomeEffects.getMoodSound().ifPresent(soundEvent -> {
|
||||||
|
var sound = new JsonObject();
|
||||||
|
sound.addProperty("sound", soundEvent.getSound().getId().getPath());
|
||||||
|
sound.addProperty("tick_delay", soundEvent.getCultivationTicks());
|
||||||
|
sound.addProperty("offset", soundEvent.getExtraDistance());
|
||||||
|
sound.addProperty("block_search_extent", soundEvent.getSpawnRange());
|
||||||
|
|
||||||
|
effectJson.add("mood_sound", sound);
|
||||||
|
});
|
||||||
|
|
||||||
|
biome.getParticleConfig().ifPresent(biomeParticleConfig -> {
|
||||||
|
try {
|
||||||
|
var particleConfig = new JsonObject();
|
||||||
|
// We must first convert it into an identifier, because asString() returns a resource identifier as string.
|
||||||
|
Identifier id = new Identifier(biomeParticleConfig.getParticle().asString());
|
||||||
|
particleConfig.addProperty("kind", id.getPath());
|
||||||
|
particleConfig.addProperty("probability" ,particleConfigProbabilityField.getFloat(biomeParticleConfig));
|
||||||
|
biomeJson.add("particle", particleConfig);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var spawnSettingsJson = new JsonObject();
|
||||||
|
var spawnSettings = biome.getSpawnSettings();
|
||||||
|
spawnSettingsJson.addProperty("probability", spawnSettings.getCreatureSpawnProbability());
|
||||||
|
|
||||||
|
var spawnGroupsJson = new JsonObject();
|
||||||
|
for (var spawnGroup : SpawnGroup.values()) {
|
||||||
|
var spawnGroupJson = new JsonArray();
|
||||||
|
for (var entry : spawnSettings.getSpawnEntries(spawnGroup).getEntries()) {
|
||||||
|
var groupEntryJson = new JsonObject();
|
||||||
|
groupEntryJson.addProperty("name", Registry.ENTITY_TYPE.getId(entry.type).getPath());
|
||||||
|
groupEntryJson.addProperty("min_group_size", entry.minGroupSize);
|
||||||
|
groupEntryJson.addProperty("max_group_size", entry.maxGroupSize);
|
||||||
|
groupEntryJson.addProperty("weight", ((Weighted) entry).getWeight().getValue());
|
||||||
|
spawnGroupJson.add(groupEntryJson);
|
||||||
|
}
|
||||||
|
spawnGroupsJson.add(spawnGroup.getName(), spawnGroupJson);
|
||||||
|
}
|
||||||
|
spawnSettingsJson.add("groups", spawnGroupsJson);
|
||||||
|
|
||||||
|
biomeJson.add("effects", effectJson);
|
||||||
|
biomeJson.add("spawn_settings", spawnSettingsJson);
|
||||||
|
|
||||||
|
var entryJson = new JsonObject();
|
||||||
|
entryJson.addProperty("name", biomeIdent.getPath());
|
||||||
|
entryJson.addProperty("id", BuiltinRegistries.BIOME.getRawId(biome));
|
||||||
|
entryJson.add("element", biomeJson);
|
||||||
|
biomesJson.add(entryJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
return biomesJson;
|
||||||
|
}
|
||||||
|
}
|
192
src/chunk.rs
192
src/chunk.rs
|
@ -110,7 +110,8 @@ impl<C: Config> Chunks<C> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the height of all loaded chunks in the world. This returns the
|
/// Returns the height of all loaded chunks in the world. This returns the
|
||||||
/// same value as [`Chunk::height`] for all loaded chunks.
|
/// same value as [`Chunk::section_count`] multiplied by 16 for all loaded
|
||||||
|
/// chunks.
|
||||||
pub fn height(&self) -> usize {
|
pub fn height(&self) -> usize {
|
||||||
self.dimension_height as usize
|
self.dimension_height as usize
|
||||||
}
|
}
|
||||||
|
@ -204,7 +205,7 @@ impl<C: Config> Chunks<C> {
|
||||||
|
|
||||||
let y = pos.y.checked_sub(self.dimension_min_y)?.try_into().ok()?;
|
let y = pos.y.checked_sub(self.dimension_min_y)?.try_into().ok()?;
|
||||||
|
|
||||||
if y < chunk.height() {
|
if y < chunk.section_count() * 16 {
|
||||||
Some(chunk.block_state(
|
Some(chunk.block_state(
|
||||||
pos.x.rem_euclid(16) as usize,
|
pos.x.rem_euclid(16) as usize,
|
||||||
y,
|
y,
|
||||||
|
@ -437,42 +438,46 @@ impl<C: Config, P: Into<ChunkPos>> IndexMut<P> for Chunks<C> {
|
||||||
/// Operations that can be performed on a chunk. [`LoadedChunk`] and
|
/// Operations that can be performed on a chunk. [`LoadedChunk`] and
|
||||||
/// [`UnloadedChunk`] implement this trait.
|
/// [`UnloadedChunk`] implement this trait.
|
||||||
pub trait Chunk {
|
pub trait Chunk {
|
||||||
/// Returns the height of this chunk in blocks. The result is always a
|
/// Returns the number of sections in this chunk. To get the height of the
|
||||||
/// multiple of 16.
|
/// chunk in meters, multiply the result by 16.
|
||||||
fn height(&self) -> usize;
|
fn section_count(&self) -> usize;
|
||||||
|
|
||||||
/// Gets the block state at the provided offsets in the chunk.
|
/// Gets the block state at the provided offsets in the chunk.
|
||||||
///
|
///
|
||||||
/// **Note**: The arguments to this function are offsets from the minimum
|
/// **Note**: The arguments to this function are offsets from the minimum
|
||||||
/// corner of the chunk in _chunk space_ rather than _world space_. You
|
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||||
/// might be looking for [`Chunks::block_state`] instead.
|
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if the offsets are outside the bounds of the chunk.
|
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||||
|
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||||
fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState;
|
fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState;
|
||||||
|
|
||||||
/// Sets the block state at the provided offsets in the chunk. The previous
|
/// Sets the block state at the provided offsets in the chunk. The previous
|
||||||
/// block state at the position is returned.
|
/// block state at the position is returned.
|
||||||
///
|
///
|
||||||
/// **Note**: The arguments to this function are offsets from the minimum
|
/// **Note**: The arguments to this function are offsets from the minimum
|
||||||
/// corner of the chunk in _chunk space_ rather than _world space_. You
|
/// corner of the chunk in _chunk space_ rather than _world space_.
|
||||||
/// might be looking for [`Chunks::set_block_state`] instead.
|
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if the offsets are outside the bounds of the chunk.
|
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||||
|
/// must be less than 16 while `y` must be less than `section_count() * 16`.
|
||||||
fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState;
|
fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState;
|
||||||
|
|
||||||
/// Sets every block state in this chunk to the given block state.
|
/// Sets every block in a section to the given block state.
|
||||||
///
|
///
|
||||||
/// This is semantically equivalent to calling [`set_block_state`] on every
|
/// This is semantically equivalent to setting every block in the section
|
||||||
/// block in the chunk followed by a call to [`optimize`] at the end.
|
/// with [`set_block_state`]. However, this function may be implemented more
|
||||||
/// However, this function may be implemented more efficiently.
|
/// efficiently.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `sect_y` is out of bounds. `sect_y` must be less than the
|
||||||
|
/// section count.
|
||||||
///
|
///
|
||||||
/// [`set_block_state`]: Self::set_block_state
|
/// [`set_block_state`]: Self::set_block_state
|
||||||
/// [`optimize`]: Self::optimize
|
fn fill_block_states(&mut self, sect_y: usize, block: BlockState);
|
||||||
fn fill_block_states(&mut self, block: BlockState);
|
|
||||||
|
|
||||||
/// Gets the biome at the provided biome offsets in the chunk.
|
/// Gets the biome at the provided biome offsets in the chunk.
|
||||||
///
|
///
|
||||||
|
@ -481,7 +486,8 @@ pub trait Chunk {
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if the offsets are outside the bounds of the chunk.
|
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||||
|
/// must be less than 4 while `y` must be less than `section_count() * 4`.
|
||||||
fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId;
|
fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId;
|
||||||
|
|
||||||
/// Sets the biome at the provided offsets in the chunk. The previous
|
/// Sets the biome at the provided offsets in the chunk. The previous
|
||||||
|
@ -492,18 +498,23 @@ pub trait Chunk {
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if the offsets are outside the bounds of the chunk.
|
/// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
|
||||||
|
/// must be less than 4 while `y` must be less than `section_count() * 4`.
|
||||||
fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId;
|
fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId;
|
||||||
|
|
||||||
/// Sets every biome in this chunk to the given biome.
|
/// Sets every biome in a section to the given block state.
|
||||||
///
|
///
|
||||||
/// This is semantically equivalent to calling [`set_biome`] on every
|
/// This is semantically equivalent to setting every biome in the section
|
||||||
/// biome in the chunk followed by a call to [`optimize`] at the end.
|
/// with [`set_biome`]. However, this function may be implemented more
|
||||||
/// However, this function may be implemented more efficiently.
|
/// efficiently.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `sect_y` is out of bounds. `sect_y` must be less than the
|
||||||
|
/// section count.
|
||||||
///
|
///
|
||||||
/// [`set_biome`]: Self::set_biome
|
/// [`set_biome`]: Self::set_biome
|
||||||
/// [`optimize`]: Self::optimize
|
fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId);
|
||||||
fn fill_biomes(&mut self, biome: BiomeId);
|
|
||||||
|
|
||||||
/// Optimizes this chunk to use the minimum amount of memory possible. It
|
/// Optimizes this chunk to use the minimum amount of memory possible. It
|
||||||
/// should have no observable effect on the contents of the chunk.
|
/// should have no observable effect on the contents of the chunk.
|
||||||
|
@ -521,44 +532,30 @@ pub struct UnloadedChunk {
|
||||||
|
|
||||||
impl UnloadedChunk {
|
impl UnloadedChunk {
|
||||||
/// Constructs a new unloaded chunk containing only [`BlockState::AIR`] and
|
/// Constructs a new unloaded chunk containing only [`BlockState::AIR`] and
|
||||||
/// [`BiomeId::default()`] with the given height in blocks.
|
/// [`BiomeId::default()`] with the given number of sections. A section is a
|
||||||
///
|
/// 16x16x16 meter volume.
|
||||||
/// # Panics
|
pub fn new(section_count: usize) -> Self {
|
||||||
///
|
|
||||||
/// Panics if the value of `height` does not meet the following criteria:
|
|
||||||
/// `height % 16 == 0 && height <= 4064`.
|
|
||||||
pub fn new(height: usize) -> Self {
|
|
||||||
let mut chunk = Self { sections: vec![] };
|
let mut chunk = Self { sections: vec![] };
|
||||||
|
chunk.resize(section_count);
|
||||||
chunk.resize(height);
|
|
||||||
chunk
|
chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes the height of the chunk to `new_height`. This is a potentially
|
/// Changes the section count of the chunk to `new_section_count`. This is a
|
||||||
/// expensive operation that may involve copying.
|
/// potentially expensive operation that may involve copying.
|
||||||
///
|
///
|
||||||
/// The chunk is extended and truncated from the top. New blocks are always
|
/// The chunk is extended and truncated from the top. New blocks are always
|
||||||
/// [`BlockState::AIR`] and biomes are [`BiomeId::default()`].
|
/// [`BlockState::AIR`] and biomes are [`BiomeId::default()`].
|
||||||
///
|
pub fn resize(&mut self, new_section_count: usize) {
|
||||||
/// # Panics
|
let old_section_count = self.section_count();
|
||||||
///
|
|
||||||
/// The constraints on `new_height` are the same as [`UnloadedChunk::new`].
|
|
||||||
pub fn resize(&mut self, new_height: usize) {
|
|
||||||
assert!(
|
|
||||||
new_height % 16 == 0 && new_height <= 4064,
|
|
||||||
"invalid chunk height of {new_height}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let old_height = self.sections.len() * 16;
|
if new_section_count > old_section_count {
|
||||||
|
|
||||||
if new_height > old_height {
|
|
||||||
let additional = (new_height - old_height) / 16;
|
|
||||||
self.sections.reserve_exact(additional);
|
|
||||||
self.sections
|
self.sections
|
||||||
.resize_with(new_height / 16, ChunkSection::default);
|
.reserve_exact(new_section_count - old_section_count);
|
||||||
|
self.sections
|
||||||
|
.resize_with(new_section_count, ChunkSection::default);
|
||||||
debug_assert_eq!(self.sections.capacity(), self.sections.len());
|
debug_assert_eq!(self.sections.capacity(), self.sections.len());
|
||||||
} else if new_height < old_height {
|
} else {
|
||||||
self.sections.truncate(new_height / 16);
|
self.sections.truncate(new_section_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -571,13 +568,13 @@ impl Default for UnloadedChunk {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chunk for UnloadedChunk {
|
impl Chunk for UnloadedChunk {
|
||||||
fn height(&self) -> usize {
|
fn section_count(&self) -> usize {
|
||||||
self.sections.len() * 16
|
self.sections.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
|
fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
|
||||||
assert!(
|
assert!(
|
||||||
x < 16 && y < self.height() && z < 16,
|
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -588,7 +585,7 @@ impl Chunk for UnloadedChunk {
|
||||||
|
|
||||||
fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState {
|
fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState {
|
||||||
assert!(
|
assert!(
|
||||||
x < 16 && y < self.height() && z < 16,
|
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -605,9 +602,13 @@ impl Chunk for UnloadedChunk {
|
||||||
old_block
|
old_block
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_block_states(&mut self, block: BlockState) {
|
fn fill_block_states(&mut self, sect_y: usize, block: BlockState) {
|
||||||
for sect in self.sections.iter_mut() {
|
let Some(sect) = self.sections.get_mut(sect_y) else {
|
||||||
// TODO: adjust motion blocking here.
|
panic!(
|
||||||
|
"section index {sect_y} out of bounds for chunk with {} sections",
|
||||||
|
self.section_count()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
if block.is_air() {
|
if block.is_air() {
|
||||||
sect.non_air_count = 0;
|
sect.non_air_count = 0;
|
||||||
|
@ -617,11 +618,10 @@ impl Chunk for UnloadedChunk {
|
||||||
|
|
||||||
sect.block_states.fill(block);
|
sect.block_states.fill(block);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId {
|
fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId {
|
||||||
assert!(
|
assert!(
|
||||||
x < 4 && y < self.height() / 4 && z < 4,
|
x < 4 && y < self.section_count() * 4 && z < 4,
|
||||||
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -630,7 +630,7 @@ impl Chunk for UnloadedChunk {
|
||||||
|
|
||||||
fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId {
|
fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId {
|
||||||
assert!(
|
assert!(
|
||||||
x < 4 && y < self.height() / 4 && z < 4,
|
x < 4 && y < self.section_count() * 4 && z < 4,
|
||||||
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -639,11 +639,16 @@ impl Chunk for UnloadedChunk {
|
||||||
.set(x + z * 4 + y % 4 * 4 * 4, biome)
|
.set(x + z * 4 + y % 4 * 4 * 4, biome)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_biomes(&mut self, biome: BiomeId) {
|
fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId) {
|
||||||
for sect in self.sections.iter_mut() {
|
let Some(sect) = self.sections.get_mut(sect_y) else {
|
||||||
|
panic!(
|
||||||
|
"section index {sect_y} out of bounds for chunk with {} sections",
|
||||||
|
self.section_count()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
sect.biomes.fill(biome);
|
sect.biomes.fill(biome);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn optimize(&mut self) {
|
fn optimize(&mut self) {
|
||||||
for sect in self.sections.iter_mut() {
|
for sect in self.sections.iter_mut() {
|
||||||
|
@ -727,7 +732,7 @@ impl ChunkSection {
|
||||||
|
|
||||||
impl<C: Config> LoadedChunk<C> {
|
impl<C: Config> LoadedChunk<C> {
|
||||||
fn new(mut chunk: UnloadedChunk, dimension_section_count: usize, state: C::ChunkState) -> Self {
|
fn new(mut chunk: UnloadedChunk, dimension_section_count: usize, state: C::ChunkState) -> Self {
|
||||||
chunk.resize(dimension_section_count * 16);
|
chunk.resize(dimension_section_count);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
|
@ -870,13 +875,13 @@ impl<C: Config> LoadedChunk<C> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Config> Chunk for LoadedChunk<C> {
|
impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
fn height(&self) -> usize {
|
fn section_count(&self) -> usize {
|
||||||
self.sections.len() * 16
|
self.sections.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
|
fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
|
||||||
assert!(
|
assert!(
|
||||||
x < 16 && y < self.height() && z < 16,
|
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -887,7 +892,7 @@ impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
|
|
||||||
fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState {
|
fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState {
|
||||||
assert!(
|
assert!(
|
||||||
x < 16 && y < self.height() && z < 16,
|
x < 16 && y < self.section_count() * 16 && z < 16,
|
||||||
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk block offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -909,8 +914,14 @@ impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
old_block
|
old_block
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_block_states(&mut self, block: BlockState) {
|
fn fill_block_states(&mut self, sect_y: usize, block: BlockState) {
|
||||||
for sect in self.sections.iter_mut() {
|
let Some(sect) = self.sections.get_mut(sect_y) else {
|
||||||
|
panic!(
|
||||||
|
"section index {sect_y} out of bounds for chunk with {} sections",
|
||||||
|
self.section_count()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Mark the appropriate blocks as modified.
|
// Mark the appropriate blocks as modified.
|
||||||
// No need to iterate through all the blocks if we know they're all the same.
|
// No need to iterate through all the blocks if we know they're all the same.
|
||||||
if let PalettedContainer::Single(single) = §.block_states {
|
if let PalettedContainer::Single(single) = §.block_states {
|
||||||
|
@ -925,8 +936,6 @@ impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: adjust motion blocking here.
|
|
||||||
|
|
||||||
if block.is_air() {
|
if block.is_air() {
|
||||||
sect.non_air_count = 0;
|
sect.non_air_count = 0;
|
||||||
} else {
|
} else {
|
||||||
|
@ -935,11 +944,10 @@ impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
|
|
||||||
sect.block_states.fill(block);
|
sect.block_states.fill(block);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId {
|
fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId {
|
||||||
assert!(
|
assert!(
|
||||||
x < 4 && y < self.height() / 4 && z < 4,
|
x < 4 && y < self.section_count() * 4 && z < 4,
|
||||||
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -948,7 +956,7 @@ impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
|
|
||||||
fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId {
|
fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId {
|
||||||
assert!(
|
assert!(
|
||||||
x < 4 && y < self.height() / 4 && z < 4,
|
x < 4 && y < self.section_count() * 4 && z < 4,
|
||||||
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
"chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -963,10 +971,15 @@ impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
old_biome
|
old_biome
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_biomes(&mut self, biome: BiomeId) {
|
fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId) {
|
||||||
for sect in self.sections.iter_mut() {
|
let Some(sect) = self.sections.get_mut(sect_y) else {
|
||||||
|
panic!(
|
||||||
|
"section index {sect_y} out of bounds for chunk with {} sections",
|
||||||
|
self.section_count()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
sect.biomes.fill(biome);
|
sect.biomes.fill(biome);
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is set to true unconditionally, but it doesn't have to be.
|
// TODO: this is set to true unconditionally, but it doesn't have to be.
|
||||||
self.any_biomes_modified = true;
|
self.any_biomes_modified = true;
|
||||||
|
@ -983,13 +996,6 @@ impl<C: Config> Chunk for LoadedChunk<C> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
fn is_motion_blocking(b: BlockState) -> bool {
|
|
||||||
// TODO: use is_solid || is_fluid ?
|
|
||||||
!b.is_air()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
fn compact_u64s_len(vals_count: usize, bits_per_val: usize) -> usize {
|
fn compact_u64s_len(vals_count: usize, bits_per_val: usize) -> usize {
|
||||||
let vals_per_u64 = 64 / bits_per_val;
|
let vals_per_u64 = 64 / bits_per_val;
|
||||||
num::Integer::div_ceil(&vals_count, &vals_per_u64)
|
num::Integer::div_ceil(&vals_count, &vals_per_u64)
|
||||||
|
@ -1080,8 +1086,14 @@ mod tests {
|
||||||
check_invariants(&loaded.sections);
|
check_invariants(&loaded.sections);
|
||||||
check_invariants(&unloaded.sections);
|
check_invariants(&unloaded.sections);
|
||||||
|
|
||||||
loaded.fill_block_states(rand_block_state(&mut rng));
|
loaded.fill_block_states(
|
||||||
unloaded.fill_block_states(rand_block_state(&mut rng));
|
rng.gen_range(0..loaded.section_count()),
|
||||||
|
rand_block_state(&mut rng),
|
||||||
|
);
|
||||||
|
unloaded.fill_block_states(
|
||||||
|
rng.gen_range(0..loaded.section_count()),
|
||||||
|
rand_block_state(&mut rng),
|
||||||
|
);
|
||||||
|
|
||||||
check_invariants(&loaded.sections);
|
check_invariants(&loaded.sections);
|
||||||
check_invariants(&unloaded.sections);
|
check_invariants(&unloaded.sections);
|
||||||
|
|
38
valence_anvil/Cargo.toml
Normal file
38
valence_anvil/Cargo.toml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
[package]
|
||||||
|
name = "valence_anvil"
|
||||||
|
description = "A library for Minecraft's Anvil world format."
|
||||||
|
documentation = "https://docs.rs/valence_anvil/"
|
||||||
|
repository = "https://github.com/valence_anvil/valence/tree/main/valence_anvil"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
|
keywords = ["anvil", "minecraft", "deserialization"]
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan Johnson <ryanj00a@gmail.com>", "TerminatorNL <TerminatorNL@users.noreply.github.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = "1.4.3"
|
||||||
|
flate2 = "1.0.25"
|
||||||
|
thiserror = "1.0.37"
|
||||||
|
num-integer = "0.1.45" # TODO: remove when div_ceil is stabilized.
|
||||||
|
valence = { version = "0.1.0", path = "..", optional = true }
|
||||||
|
valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
anyhow = "1.0.68"
|
||||||
|
criterion = "0.4.0"
|
||||||
|
fs_extra = "1.2.0"
|
||||||
|
tempfile = "3.3.0"
|
||||||
|
valence = { version = "0.1.0", path = ".." }
|
||||||
|
valence_anvil = { version = "0.1.0", path = ".", features = ["valence"] }
|
||||||
|
zip = "0.6.3"
|
||||||
|
|
||||||
|
[dev-dependencies.reqwest]
|
||||||
|
version = "0.11.12"
|
||||||
|
default-features = false
|
||||||
|
# Avoid OpenSSL dependency on Linux.
|
||||||
|
features = ["rustls-tls", "blocking", "stream"]
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "world_parsing"
|
||||||
|
harness = false
|
137
valence_anvil/benches/world_parsing.rs
Normal file
137
valence_anvil/benches/world_parsing.rs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
use std::fs::create_dir_all;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{ensure, Context};
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use fs_extra::dir::CopyOptions;
|
||||||
|
use reqwest::IntoUrl;
|
||||||
|
use valence::chunk::UnloadedChunk;
|
||||||
|
use valence_anvil::AnvilWorld;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
criterion_group!(benches, criterion_benchmark);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
let world_dir = get_world_asset(
|
||||||
|
"https://github.com/valence-rs/valence-test-data/archive/refs/heads/asset/sp_world_1.19.2.zip",
|
||||||
|
"1.19.2 benchmark world",
|
||||||
|
true
|
||||||
|
).expect("failed to get world asset");
|
||||||
|
|
||||||
|
let mut world = AnvilWorld::new(world_dir);
|
||||||
|
|
||||||
|
c.bench_function("Load square 10x10", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let world = black_box(&mut world);
|
||||||
|
|
||||||
|
for z in -5..5 {
|
||||||
|
for x in -5..5 {
|
||||||
|
let nbt = world
|
||||||
|
.read_chunk(x, z)
|
||||||
|
.expect("failed to read chunk")
|
||||||
|
.expect("missing chunk at position")
|
||||||
|
.data;
|
||||||
|
|
||||||
|
let mut chunk = UnloadedChunk::new(24);
|
||||||
|
|
||||||
|
valence_anvil::to_valence(&nbt, &mut chunk, 4, |_| Default::default()).unwrap();
|
||||||
|
|
||||||
|
black_box(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the asset. If the asset is already present on the system due to a
|
||||||
|
/// prior run, the cached asset is used instead. If the asset is not
|
||||||
|
/// cached yet, this function downloads the asset using the current thread.
|
||||||
|
/// This will block until the download is complete.
|
||||||
|
///
|
||||||
|
/// returns: `PathBuf` The reference to the asset on the file system
|
||||||
|
fn get_world_asset(
|
||||||
|
url: impl IntoUrl,
|
||||||
|
dest_path: impl AsRef<Path>,
|
||||||
|
remove_top_level_dir: bool,
|
||||||
|
) -> anyhow::Result<PathBuf> {
|
||||||
|
let url = url.into_url()?;
|
||||||
|
let dest_path = dest_path.as_ref();
|
||||||
|
|
||||||
|
let asset_cache_dir = Path::new(".asset_cache");
|
||||||
|
|
||||||
|
create_dir_all(asset_cache_dir).context("unable to create `.asset_cache` directory")?;
|
||||||
|
|
||||||
|
let final_path = asset_cache_dir.join(dest_path);
|
||||||
|
|
||||||
|
if final_path.exists() {
|
||||||
|
return Ok(final_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response = reqwest::blocking::get(url.clone())?;
|
||||||
|
|
||||||
|
let cache_download_directory = asset_cache_dir.join("downloads");
|
||||||
|
|
||||||
|
create_dir_all(&cache_download_directory)
|
||||||
|
.context("unable to create `.asset_cache/downloads` directory")?;
|
||||||
|
|
||||||
|
let mut downloaded_zip_file =
|
||||||
|
tempfile::tempfile_in(&cache_download_directory).context("Could not create temp file")?;
|
||||||
|
|
||||||
|
println!("Downloading {dest_path:?} from {url}");
|
||||||
|
|
||||||
|
response
|
||||||
|
.copy_to(&mut downloaded_zip_file)
|
||||||
|
.context("could not write web contents to the temporary file")?;
|
||||||
|
|
||||||
|
let mut zip_archive = ZipArchive::new(downloaded_zip_file)
|
||||||
|
.context("unable to create zip archive from downloaded content")?;
|
||||||
|
|
||||||
|
if !remove_top_level_dir {
|
||||||
|
zip_archive
|
||||||
|
.extract(&final_path)
|
||||||
|
.context("unable to unzip downloaded contents")?;
|
||||||
|
|
||||||
|
return Ok(final_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp_dir = tempfile::tempdir_in(&cache_download_directory)
|
||||||
|
.context("unable to create temporary directory in `.asset_cache`")?;
|
||||||
|
|
||||||
|
zip_archive
|
||||||
|
.extract(&temp_dir)
|
||||||
|
.context("unable to unzip downloaded contents")?;
|
||||||
|
|
||||||
|
let mut entries = temp_dir.path().read_dir()?;
|
||||||
|
|
||||||
|
let top_level_dir = entries
|
||||||
|
.next()
|
||||||
|
.context("the downloaded zip file was empty")??;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
entries.next().is_none(),
|
||||||
|
"found more than one entry in the top level directory of the Zip file"
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
top_level_dir.path().is_dir(),
|
||||||
|
"the only content in the zip archive is a file"
|
||||||
|
);
|
||||||
|
|
||||||
|
create_dir_all(&final_path).context("could not create a directory inside the asset cache")?;
|
||||||
|
|
||||||
|
let dir_entries = top_level_dir
|
||||||
|
.path()
|
||||||
|
.read_dir()?
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
let items_to_move: Vec<_> = dir_entries.into_iter().map(|d| d.path()).collect();
|
||||||
|
|
||||||
|
fs_extra::move_items(&items_to_move, &final_path, &CopyOptions::new())?;
|
||||||
|
|
||||||
|
// We keep the temporary directory around until we're done moving files out
|
||||||
|
// of it.
|
||||||
|
drop(temp_dir);
|
||||||
|
|
||||||
|
Ok(final_path)
|
||||||
|
}
|
201
valence_anvil/examples/valence_loading.rs
Normal file
201
valence_anvil/examples/valence_loading.rs
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
//! # IMPORTANT
|
||||||
|
//!
|
||||||
|
//! Run this example with one argument containing the path of the the following
|
||||||
|
//! to the world directory you wish to load. Inside this directory you can
|
||||||
|
//! commonly see `advancements`, `DIM1`, `DIM-1` and most importantly `region`
|
||||||
|
//! subdirectories. Only the `region` directory is accessed.
|
||||||
|
|
||||||
|
extern crate valence;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use valence::prelude::*;
|
||||||
|
use valence_anvil::AnvilWorld;
|
||||||
|
|
||||||
|
pub fn main() -> ShutdownResult {
|
||||||
|
let Some(world_dir) = env::args().nth(1) else {
|
||||||
|
return Err("please add the world directory as program argument.".into())
|
||||||
|
};
|
||||||
|
|
||||||
|
let world_dir = PathBuf::from(world_dir);
|
||||||
|
|
||||||
|
if !world_dir.exists() || !world_dir.is_dir() {
|
||||||
|
return Err("world argument must be a directory that exists".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !world_dir.join("region").exists() {
|
||||||
|
return Err("could not find the \"region\" directory in the given world directory".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
valence::start_server(
|
||||||
|
Game {
|
||||||
|
world_dir,
|
||||||
|
player_count: AtomicUsize::new(0),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ClientData {
|
||||||
|
id: EntityId,
|
||||||
|
//block: valence::block::BlockKind
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Game {
|
||||||
|
world_dir: PathBuf,
|
||||||
|
player_count: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_PLAYERS: usize = 10;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Config for Game {
|
||||||
|
type ServerState = Option<PlayerListId>;
|
||||||
|
type ClientState = ClientData;
|
||||||
|
type EntityState = ();
|
||||||
|
type WorldState = AnvilWorld;
|
||||||
|
/// If the chunk should stay loaded at the end of the tick.
|
||||||
|
type ChunkState = bool;
|
||||||
|
type PlayerListState = ();
|
||||||
|
type InventoryState = ();
|
||||||
|
|
||||||
|
async fn server_list_ping(
|
||||||
|
&self,
|
||||||
|
_server: &SharedServer<Self>,
|
||||||
|
_remote_addr: SocketAddr,
|
||||||
|
_protocol_version: i32,
|
||||||
|
) -> ServerListPing {
|
||||||
|
ServerListPing::Respond {
|
||||||
|
online_players: self.player_count.load(Ordering::SeqCst) as i32,
|
||||||
|
max_players: MAX_PLAYERS as i32,
|
||||||
|
player_sample: Default::default(),
|
||||||
|
description: "Hello Valence!".color(Color::AQUA),
|
||||||
|
favicon_png: Some(
|
||||||
|
include_bytes!("../../assets/logo-64x64.png")
|
||||||
|
.as_slice()
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&self, server: &mut Server<Self>) {
|
||||||
|
for (id, _) in server.shared.dimensions() {
|
||||||
|
server.worlds.insert(id, AnvilWorld::new(&self.world_dir));
|
||||||
|
}
|
||||||
|
server.state = Some(server.player_lists.insert(()).0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&self, server: &mut Server<Self>) {
|
||||||
|
let (world_id, world) = server.worlds.iter_mut().next().unwrap();
|
||||||
|
|
||||||
|
server.clients.retain(|_, client| {
|
||||||
|
if client.created_this_tick() {
|
||||||
|
if self
|
||||||
|
.player_count
|
||||||
|
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
|
||||||
|
(count < MAX_PLAYERS).then_some(count + 1)
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
client.disconnect("The server is full!".color(Color::RED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match server
|
||||||
|
.entities
|
||||||
|
.insert_with_uuid(EntityKind::Player, client.uuid(), ())
|
||||||
|
{
|
||||||
|
Some((id, _)) => client.state.id = id,
|
||||||
|
None => {
|
||||||
|
client.disconnect("Conflicting UUID");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.respawn(world_id);
|
||||||
|
client.set_flat(true);
|
||||||
|
client.set_game_mode(GameMode::Spectator);
|
||||||
|
client.teleport([0.0, 125.0, 0.0], 0.0, 0.0);
|
||||||
|
client.set_player_list(server.state.clone());
|
||||||
|
|
||||||
|
if let Some(id) = &server.state {
|
||||||
|
server.player_lists.get_mut(id).insert(
|
||||||
|
client.uuid(),
|
||||||
|
client.username(),
|
||||||
|
client.textures().cloned(),
|
||||||
|
client.game_mode(),
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.send_message("Welcome to the java chunk parsing example!");
|
||||||
|
client.send_message(
|
||||||
|
"Chunks with a single lava source block indicates that the chunk is not \
|
||||||
|
(fully) generated."
|
||||||
|
.italic(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.is_disconnected() {
|
||||||
|
self.player_count.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
if let Some(id) = &server.state {
|
||||||
|
server.player_lists.get_mut(id).remove(client.uuid());
|
||||||
|
}
|
||||||
|
server.entities.delete(client.id);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(entity) = server.entities.get_mut(client.state.id) {
|
||||||
|
while let Some(event) = client.next_event() {
|
||||||
|
event.handle_default(client, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dist = client.view_distance();
|
||||||
|
let p = client.position();
|
||||||
|
|
||||||
|
for pos in ChunkPos::at(p.x, p.z).in_view(dist) {
|
||||||
|
if let Some(existing) = world.chunks.get_mut(pos) {
|
||||||
|
existing.state = true;
|
||||||
|
} else {
|
||||||
|
match world.state.read_chunk(pos.x, pos.z) {
|
||||||
|
Ok(Some(anvil_chunk)) => {
|
||||||
|
let mut chunk = UnloadedChunk::new(24);
|
||||||
|
|
||||||
|
if let Err(e) =
|
||||||
|
valence_anvil::to_valence(&anvil_chunk.data, &mut chunk, 4, |_| {
|
||||||
|
BiomeId::default()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
eprintln!("Failed to convert chunk at ({}, {}): {e}", pos.x, pos.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
world.chunks.insert(pos, chunk, true);
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// No chunk at this position.
|
||||||
|
world.chunks.insert(pos, UnloadedChunk::default(), true);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to read chunk at ({}, {}): {e}", pos.x, pos.z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
for (_, chunk) in world.chunks.iter_mut() {
|
||||||
|
if !chunk.state {
|
||||||
|
chunk.set_deleted(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
171
valence_anvil/src/lib.rs
Normal file
171
valence_anvil/src/lib.rs
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
use std::collections::btree_map::Entry;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::io::{ErrorKind, Read, Seek, SeekFrom};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
|
use flate2::bufread::{GzDecoder, ZlibDecoder};
|
||||||
|
use thiserror::Error;
|
||||||
|
#[cfg(feature = "valence")]
|
||||||
|
pub use to_valence::*;
|
||||||
|
use valence_nbt::Compound;
|
||||||
|
|
||||||
|
#[cfg(feature = "valence")]
|
||||||
|
mod to_valence;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AnvilWorld {
|
||||||
|
/// Path to the "region" subdirectory in the world root.
|
||||||
|
region_root: PathBuf,
|
||||||
|
/// Maps region (x, z) positions to region files.
|
||||||
|
regions: BTreeMap<(i32, i32), Region>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub struct AnvilChunk {
|
||||||
|
/// This chunk's NBT data.
|
||||||
|
pub data: Compound,
|
||||||
|
/// The time this chunk was last modified measured in seconds since the
|
||||||
|
/// epoch.
|
||||||
|
pub timestamp: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ReadChunkError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Nbt(#[from] valence_nbt::Error),
|
||||||
|
#[error("invalid chunk sector offset")]
|
||||||
|
BadSectorOffset,
|
||||||
|
#[error("invalid chunk size")]
|
||||||
|
BadChunkSize,
|
||||||
|
#[error("unknown compression scheme number of {0}")]
|
||||||
|
UnknownCompressionScheme(u8),
|
||||||
|
#[error("not all chunk NBT data was read")]
|
||||||
|
IncompleteNbtRead,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Region {
|
||||||
|
file: File,
|
||||||
|
/// The first 8 KiB in the file.
|
||||||
|
header: [u8; SECTOR_SIZE * 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTOR_SIZE: usize = 4096;
|
||||||
|
|
||||||
|
impl AnvilWorld {
|
||||||
|
pub fn new(world_root: impl Into<PathBuf>) -> Self {
|
||||||
|
let mut region_root = world_root.into();
|
||||||
|
region_root.push("region");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
region_root,
|
||||||
|
regions: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a chunk from the file system with the given chunk coordinates. If
|
||||||
|
/// no chunk exists at the position, then `None` is returned.
|
||||||
|
pub fn read_chunk(
|
||||||
|
&mut self,
|
||||||
|
chunk_x: i32,
|
||||||
|
chunk_z: i32,
|
||||||
|
) -> Result<Option<AnvilChunk>, ReadChunkError> {
|
||||||
|
let region_x = chunk_x.div_euclid(32);
|
||||||
|
let region_z = chunk_z.div_euclid(32);
|
||||||
|
|
||||||
|
let region = match self.regions.entry((region_x, region_z)) {
|
||||||
|
Entry::Vacant(ve) => {
|
||||||
|
// Load the region file if it exists. Otherwise, the chunk is considered absent.
|
||||||
|
|
||||||
|
let path = self
|
||||||
|
.region_root
|
||||||
|
.join(format!("r.{region_x}.{region_z}.mca"));
|
||||||
|
|
||||||
|
let mut file = match File::options().read(true).write(true).open(path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut header = [0; SECTOR_SIZE * 2];
|
||||||
|
|
||||||
|
file.read_exact(&mut header)?;
|
||||||
|
|
||||||
|
ve.insert(Region { file, header })
|
||||||
|
}
|
||||||
|
Entry::Occupied(oe) => oe.into_mut(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let chunk_idx = (chunk_x.rem_euclid(32) + chunk_z.rem_euclid(32) * 32) as usize;
|
||||||
|
|
||||||
|
let location_bytes = (®ion.header[chunk_idx * 4..]).read_u32::<BigEndian>()?;
|
||||||
|
let timestamp = (®ion.header[chunk_idx * 4 + SECTOR_SIZE..]).read_u32::<BigEndian>()?;
|
||||||
|
|
||||||
|
if location_bytes == 0 {
|
||||||
|
// No chunk exists at this position.
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sector_offset = (location_bytes >> 8) as u64;
|
||||||
|
let sector_count = (location_bytes & 0xff) as usize;
|
||||||
|
|
||||||
|
if sector_offset < 2 {
|
||||||
|
// If the sector offset was <2, then the chunk data would be inside the region
|
||||||
|
// header. That doesn't make any sense.
|
||||||
|
return Err(ReadChunkError::BadSectorOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to the beginning of the chunk's data.
|
||||||
|
region
|
||||||
|
.file
|
||||||
|
.seek(SeekFrom::Start(sector_offset * SECTOR_SIZE as u64))?;
|
||||||
|
|
||||||
|
let exact_chunk_size = region.file.read_u32::<BigEndian>()? as usize;
|
||||||
|
|
||||||
|
if exact_chunk_size > sector_count * SECTOR_SIZE {
|
||||||
|
// Sector size of this chunk must always be >= the exact size.
|
||||||
|
return Err(ReadChunkError::BadChunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data_buf = vec![0; exact_chunk_size].into_boxed_slice();
|
||||||
|
region.file.read_exact(&mut data_buf)?;
|
||||||
|
|
||||||
|
let mut r = data_buf.as_ref();
|
||||||
|
|
||||||
|
let mut decompress_buf = vec![];
|
||||||
|
|
||||||
|
// What compression does the chunk use?
|
||||||
|
let mut nbt_slice = match r.read_u8()? {
|
||||||
|
// GZip
|
||||||
|
1 => {
|
||||||
|
let mut z = GzDecoder::new(r);
|
||||||
|
z.read_to_end(&mut decompress_buf)?;
|
||||||
|
decompress_buf.as_slice()
|
||||||
|
}
|
||||||
|
// Zlib
|
||||||
|
2 => {
|
||||||
|
let mut z = ZlibDecoder::new(r);
|
||||||
|
z.read_to_end(&mut decompress_buf)?;
|
||||||
|
decompress_buf.as_slice()
|
||||||
|
}
|
||||||
|
// Uncompressed
|
||||||
|
3 => r,
|
||||||
|
// Unknown
|
||||||
|
b => return Err(ReadChunkError::UnknownCompressionScheme(b)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (data, _) = valence_nbt::from_binary_slice(&mut nbt_slice)?;
|
||||||
|
|
||||||
|
if !nbt_slice.is_empty() {
|
||||||
|
return Err(ReadChunkError::IncompleteNbtRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(AnvilChunk { data, timestamp }))
|
||||||
|
}
|
||||||
|
}
|
275
valence_anvil/src/to_valence.rs
Normal file
275
valence_anvil/src/to_valence.rs
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
use num_integer::div_ceil;
|
||||||
|
use thiserror::Error;
|
||||||
|
use valence::biome::BiomeId;
|
||||||
|
use valence::chunk::Chunk;
|
||||||
|
use valence::protocol::block::{BlockKind, PropName, PropValue};
|
||||||
|
use valence::protocol::Ident;
|
||||||
|
use valence_nbt::{Compound, List, Value};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ToValenceError {
|
||||||
|
#[error("missing chunk sections")]
|
||||||
|
MissingSections,
|
||||||
|
#[error("missing chunk section Y")]
|
||||||
|
MissingSectionY,
|
||||||
|
#[error("missing block states")]
|
||||||
|
MissingBlockStates,
|
||||||
|
#[error("missing block palette")]
|
||||||
|
MissingBlockPalette,
|
||||||
|
#[error("invalid block palette length")]
|
||||||
|
BadBlockPaletteLen,
|
||||||
|
#[error("missing block name in palette")]
|
||||||
|
MissingBlockName,
|
||||||
|
#[error("unknown block name of \"{0}\"")]
|
||||||
|
UnknownBlockName(String),
|
||||||
|
#[error("unknown property name of \"{0}\"")]
|
||||||
|
UnknownPropName(String),
|
||||||
|
#[error("property value of block is not a string")]
|
||||||
|
BadPropValueType,
|
||||||
|
#[error("unknown property value of \"{0}\"")]
|
||||||
|
UnknownPropValue(String),
|
||||||
|
#[error("missing packed block state data in section")]
|
||||||
|
MissingBlockStateData,
|
||||||
|
#[error("unexpected number of longs in block state data")]
|
||||||
|
BadBlockLongCount,
|
||||||
|
#[error("invalid block palette index")]
|
||||||
|
BadBlockPaletteIndex,
|
||||||
|
#[error("missing biomes")]
|
||||||
|
MissingBiomes,
|
||||||
|
#[error("missing biome palette")]
|
||||||
|
MissingBiomePalette,
|
||||||
|
#[error("invalid biome palette length")]
|
||||||
|
BadBiomePaletteLen,
|
||||||
|
#[error("biome name is not a valid resource identifier")]
|
||||||
|
BadBiomeName,
|
||||||
|
#[error("missing biome name")]
|
||||||
|
MissingBiomeName,
|
||||||
|
#[error("missing packed biome data in section")]
|
||||||
|
MissingBiomeData,
|
||||||
|
#[error("unexpected number of longs in biome data")]
|
||||||
|
BadBiomeLongCount,
|
||||||
|
#[error("invalid biome palette index")]
|
||||||
|
BadBiomePaletteIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads an Anvil chunk in NBT form and writes its data to a Valence [`Chunk`].
|
||||||
|
/// An error is returned if the NBT data does not match the expected structure
|
||||||
|
/// for an Anvil chunk.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `nbt`: The Anvil chunk to read from. This is usually the value returned by
|
||||||
|
/// [`read_chunk`].
|
||||||
|
/// - `chunk`: The Valence chunk to write to.
|
||||||
|
/// - `sect_offset`: A constant to add to all sector Y positions in `nbt`. After
|
||||||
|
/// applying the offset, only the sectors in the range
|
||||||
|
/// `0..chunk.sector_count()` are written.
|
||||||
|
/// - `map_biome`: A function to map biome resource identifiers in the NBT data
|
||||||
|
/// to Valence [`BiomeId`]s.
|
||||||
|
///
|
||||||
|
/// [`read_chunk`]: crate::AnvilWorld::read_chunk
|
||||||
|
pub fn to_valence<C, F>(
|
||||||
|
nbt: &Compound,
|
||||||
|
chunk: &mut C,
|
||||||
|
sect_offset: i32,
|
||||||
|
mut map_biome: F,
|
||||||
|
) -> Result<(), ToValenceError>
|
||||||
|
where
|
||||||
|
C: Chunk,
|
||||||
|
F: FnMut(Ident<&str>) -> BiomeId,
|
||||||
|
{
|
||||||
|
let Some(Value::List(List::Compound(sections))) = nbt.get("sections") else {
|
||||||
|
return Err(ToValenceError::MissingSections)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut converted_block_palette = vec![];
|
||||||
|
let mut converted_biome_palette = vec![];
|
||||||
|
|
||||||
|
for section in sections {
|
||||||
|
let Some(Value::Byte(sect_y)) = section.get("Y") else {
|
||||||
|
return Err(ToValenceError::MissingSectionY)
|
||||||
|
};
|
||||||
|
|
||||||
|
let adjusted_sect_y = *sect_y as i32 + sect_offset;
|
||||||
|
|
||||||
|
if adjusted_sect_y < 0 || adjusted_sect_y as usize >= chunk.section_count() {
|
||||||
|
// Section is out of bounds. Skip it.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(Value::Compound(block_states)) = section.get("block_states") else {
|
||||||
|
return Err(ToValenceError::MissingBlockStates)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(Value::List(List::Compound(palette))) = block_states.get("palette") else {
|
||||||
|
return Err(ToValenceError::MissingBlockPalette)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(1..BLOCKS_PER_SECTION).contains(&palette.len()) {
|
||||||
|
return Err(ToValenceError::BadBlockPaletteLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
converted_block_palette.clear();
|
||||||
|
|
||||||
|
for block in palette {
|
||||||
|
let Some(Value::String(name)) = block.get("Name") else {
|
||||||
|
return Err(ToValenceError::MissingBlockName)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(block_kind) = BlockKind::from_str(ident_path(name)) else {
|
||||||
|
return Err(ToValenceError::UnknownBlockName(name.into()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = block_kind.to_state();
|
||||||
|
|
||||||
|
if let Some(Value::Compound(properties)) = block.get("Properties") {
|
||||||
|
for (key, value) in properties {
|
||||||
|
let Value::String(value) = value else {
|
||||||
|
return Err(ToValenceError::BadPropValueType)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(prop_name) = PropName::from_str(key) else {
|
||||||
|
return Err(ToValenceError::UnknownPropName(key.into()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(prop_value) = PropValue::from_str(value) else {
|
||||||
|
return Err(ToValenceError::UnknownPropValue(value.into()))
|
||||||
|
};
|
||||||
|
|
||||||
|
state = state.set(prop_name, prop_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
converted_block_palette.push(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if converted_block_palette.len() == 1 {
|
||||||
|
chunk.fill_block_states(adjusted_sect_y as usize, converted_block_palette[0]);
|
||||||
|
} else {
|
||||||
|
debug_assert!(converted_block_palette.len() > 1);
|
||||||
|
|
||||||
|
let Some(Value::LongArray(data)) = block_states.get("data") else {
|
||||||
|
return Err(ToValenceError::MissingBlockStateData)
|
||||||
|
};
|
||||||
|
|
||||||
|
let bits_per_idx = bit_width(converted_block_palette.len() - 1).max(4);
|
||||||
|
let idxs_per_long = 64 / bits_per_idx;
|
||||||
|
let long_count = div_ceil(BLOCKS_PER_SECTION, idxs_per_long);
|
||||||
|
let mask = 2_u64.pow(bits_per_idx as u32) - 1;
|
||||||
|
|
||||||
|
if long_count != data.len() {
|
||||||
|
return Err(ToValenceError::BadBlockLongCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
for &long in data.iter() {
|
||||||
|
let u64 = long as u64;
|
||||||
|
|
||||||
|
for j in 0..idxs_per_long {
|
||||||
|
if i >= BLOCKS_PER_SECTION {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = (u64 >> (bits_per_idx * j)) & mask;
|
||||||
|
|
||||||
|
let Some(block) = converted_block_palette.get(idx as usize).cloned() else {
|
||||||
|
return Err(ToValenceError::BadBlockPaletteIndex)
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = i % 16;
|
||||||
|
let z = i / 16 % 16;
|
||||||
|
let y = i / (16 * 16);
|
||||||
|
|
||||||
|
chunk.set_block_state(x, adjusted_sect_y as usize * 16 + y, z, block);
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(Value::Compound(biomes)) = section.get("biomes") else {
|
||||||
|
return Err(ToValenceError::MissingBiomes)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(Value::List(List::String(palette))) = biomes.get("palette") else {
|
||||||
|
return Err(ToValenceError::MissingBiomePalette)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(1..BIOMES_PER_SECTION).contains(&palette.len()) {
|
||||||
|
return Err(ToValenceError::BadBiomePaletteLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
converted_biome_palette.clear();
|
||||||
|
|
||||||
|
for biome_name in palette {
|
||||||
|
let Ok(ident) = Ident::new(biome_name.as_str()) else {
|
||||||
|
return Err(ToValenceError::BadBiomeName)
|
||||||
|
};
|
||||||
|
|
||||||
|
converted_biome_palette.push(map_biome(ident));
|
||||||
|
}
|
||||||
|
|
||||||
|
if converted_biome_palette.len() == 1 {
|
||||||
|
chunk.fill_biomes(adjusted_sect_y as usize, converted_biome_palette[0]);
|
||||||
|
} else {
|
||||||
|
debug_assert!(converted_biome_palette.len() > 1);
|
||||||
|
|
||||||
|
let Some(Value::LongArray(data)) = biomes.get("data") else {
|
||||||
|
return Err(ToValenceError::MissingBiomeData)
|
||||||
|
};
|
||||||
|
|
||||||
|
let bits_per_idx = bit_width(converted_biome_palette.len() - 1);
|
||||||
|
let idxs_per_long = 64 / bits_per_idx;
|
||||||
|
let long_count = div_ceil(BIOMES_PER_SECTION, idxs_per_long);
|
||||||
|
let mask = 2_u64.pow(bits_per_idx as u32) - 1;
|
||||||
|
|
||||||
|
if long_count != data.len() {
|
||||||
|
return Err(ToValenceError::BadBiomeLongCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
for &long in data.iter() {
|
||||||
|
let u64 = long as u64;
|
||||||
|
|
||||||
|
for j in 0..idxs_per_long {
|
||||||
|
if i >= BIOMES_PER_SECTION {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = (u64 >> (bits_per_idx * j)) & mask;
|
||||||
|
|
||||||
|
let Some(biome) = converted_biome_palette.get(idx as usize).cloned() else {
|
||||||
|
return Err(ToValenceError::BadBiomePaletteIndex)
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = i % 4;
|
||||||
|
let z = i / 4 % 4;
|
||||||
|
let y = i / (4 * 4);
|
||||||
|
|
||||||
|
chunk.set_biome(x, adjusted_sect_y as usize * 4 + y, z, biome);
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLOCKS_PER_SECTION: usize = 16 * 16 * 16;
|
||||||
|
const BIOMES_PER_SECTION: usize = 4 * 4 * 4;
|
||||||
|
|
||||||
|
/// Gets the path part of a resource identifier.
|
||||||
|
fn ident_path(ident: &str) -> &str {
|
||||||
|
match ident.rsplit_once(':') {
|
||||||
|
Some((_, after)) => after,
|
||||||
|
None => ident,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum number of bits needed to represent the integer `n`.
|
||||||
|
const fn bit_width(n: usize) -> usize {
|
||||||
|
(usize::BITS - n.leading_zeros()) as _
|
||||||
|
}
|
Loading…
Reference in a new issue