using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; namespace MinecraftClient.Mapping { /// /// Represents a Minecraft World /// public class World { /// /// The chunks contained into the Minecraft world /// Tuple: Tuple /// private ConcurrentDictionary, ChunkColumn> chunks = new(); /// /// The dimension info of the world /// private static Dimension curDimension= new(); private static readonly Dictionary dimensionList = new(); /// /// VarInt ID → dimension name mapping, populated from RegistryData in 1.20.6+ /// private static Dictionary dimensionIdMap = new(); /// /// VarInt ID → attribute name mapping, populated from RegistryData (minecraft:attribute) in 1.20.6+ /// private static Dictionary attributeIdMap = new(); /// /// Chunk data parsing progress /// public int chunkCnt = 0; public int chunkLoadNotCompleted = 0; /// /// Read, set or unload the specified chunk column /// /// ChunkColumn X /// ChunkColumn Z /// chunk at the given location public ChunkColumn? this[int chunkX, int chunkZ] { get { chunks.TryGetValue(new(chunkX, chunkZ), out ChunkColumn? chunkColumn); return chunkColumn; } set { Tuple chunkCoord = new(chunkX, chunkZ); if (value is null) chunks.TryRemove(chunkCoord, out _); else chunks.AddOrUpdate(chunkCoord, value, (_, _) => value); } } /// /// Storage of all dimensional data - 1.19.1 and above /// /// Registry Codec nbt data public static void StoreDimensionList(Dictionary registryCodec) { var dimensionListNbt = (object[])(((Dictionary)registryCodec["minecraft:dimension_type"])["value"]); foreach (var (dimensionName, dimensionType) in from Dictionary dimensionNbt in dimensionListNbt let dimensionName = (string)dimensionNbt["name"] let dimensionType = (Dictionary)dimensionNbt["element"] select (dimensionName, dimensionType)) { StoreOneDimension(dimensionName, dimensionType); } } public static void LoadDefaultDimensions1206Plus() { // TODO: Move this to a JSON file. var defaultRegistryCodec = new Dictionary { { "minecraft:dimension_type", new Dictionary { { "value", new object[] { new Dictionary { { "name", "minecraft:overworld" }, { "id", 0 }, { "element", new Dictionary { { "piglin_safe", (byte)0 }, { "natural", 1 }, { "ambient_light", 0.0 }, { "monster_spawn_block_light_limit", 0 }, { "infiniburn", "#minecraft:infiniburn_overworld" }, { "respawn_anchor_works", 0 }, { "has_skylight", 1 }, { "bed_works", 1 }, { "effects", "minecraft:overworld" }, { "has_raids", 1 }, { "logical_height", 384 }, { "coordinate_scale", 1.0 }, { "monster_spawn_light_level", new Dictionary { { "min_inclusive", 0 }, { "max_inclusive", 7 }, { "type", "minecraft:uniform" } } }, { "min_y", -64 }, { "ultrawarm", 0 }, { "has_ceiling", 0 }, { "height", 384 } } } }, new Dictionary { { "name", "minecraft:overworld_caves" }, { "id", 1 }, { "element", new Dictionary { { "piglin_safe", (byte)0 }, { "natural", 1 }, { "ambient_light", 0.0 }, { "monster_spawn_block_light_limit", 0 }, { "infiniburn", "#minecraft:infiniburn_overworld" }, { "respawn_anchor_works", 0 }, { "has_skylight", 1 }, { "bed_works", 1 }, { "effects", "minecraft:overworld" }, { "has_raids", 1 }, { "logical_height", 384 }, { "coordinate_scale", 1.0 }, { "monster_spawn_light_level", new Dictionary { { "min_inclusive", 0 }, { "max_inclusive", 7 }, { "type", "minecraft:uniform" } } }, { "min_y", -64 }, { "ultrawarm", 0 }, { "has_ceiling", 1 }, { "height", 384 } } } }, new Dictionary { { "name", "minecraft:the_end" }, { "id", 2 }, { "element", new Dictionary { { "piglin_safe", (byte)0 }, { "natural", 0 }, { "ambient_light", 0.0 }, { "monster_spawn_block_light_limit", 0 }, { "infiniburn", "#minecraft:infiniburn_end" }, { "respawn_anchor_works", 0 }, { "has_skylight", 0 }, { "bed_works", 0 }, { "effects", "minecraft:the_end" }, { "fixed_time", 6000 }, { "has_raids", 1 }, { "logical_height", 256 }, { "coordinate_scale", 1.0 }, { "monster_spawn_light_level", new Dictionary { { "min_inclusive", 0 }, { "max_inclusive", 7 }, { "type", "minecraft:uniform" } } }, { "min_y", 0 }, { "ultrawarm", 0 }, { "has_ceiling", 0 }, { "height", 256 } } } }, new Dictionary { { "name", "minecraft:the_nether" }, { "id", 3 }, { "element", new Dictionary { { "piglin_safe", (byte)1 }, { "natural", 0 }, { "ambient_light", 0.1 }, { "monster_spawn_block_light_limit", 15 }, { "infiniburn", "#minecraft:infiniburn_nether" }, { "respawn_anchor_works", 1 }, { "has_skylight", 0 }, { "bed_works", 0 }, { "effects", "minecraft:the_nether" }, { "fixed_time", 18000 }, { "has_raids", 0 }, { "logical_height", 128 }, { "coordinate_scale", 8.0 }, { "monster_spawn_light_level", 7 }, { "min_y", 0 }, { "ultrawarm", 1 }, { "has_ceiling", 1 }, { "height", 256 } } } } } } } } }; StoreDimensionList(defaultRegistryCodec); } public static void SetDimensionIdMap(Dictionary idMap) { dimensionIdMap = idMap; } public static string GetDimensionNameById(int id) { return dimensionIdMap.TryGetValue(id, out var name) ? name : "minecraft:overworld"; } public static bool HasAnyDimension() { return dimensionList.Count > 0; } public static void SetAttributeIdMap(Dictionary idMap) { attributeIdMap = idMap; } /// /// Get attribute name by its registry VarInt ID. Returns null if the ID is unknown. /// When KnownDataPacks negotiation tells the server we already have vanilla data, /// the server skips sending the attribute registry. In that case we fall back to /// the built-in vanilla 1.20.6 attribute order (22 entries). /// public static string? GetAttributeNameById(int id) { if (attributeIdMap.Count == 0) LoadDefaultAttributes(); return attributeIdMap.TryGetValue(id, out var name) ? name : null; } private static void LoadDefaultAttributes() { // Fallback for when the server doesn't send attribute registry via RegistryData. // Matches 1.21.1 Attributes.java registration order. // For 1.20.6+ servers, SetAttributeIdMap() overrides this with the actual registry. attributeIdMap = new Dictionary { { 0, "generic.armor" }, { 1, "generic.armor_toughness" }, { 2, "generic.attack_damage" }, { 3, "generic.attack_knockback" }, { 4, "generic.attack_speed" }, { 5, "player.block_break_speed" }, { 6, "player.block_interaction_range" }, { 7, "generic.burning_time" }, { 8, "generic.explosion_knockback_resistance" }, { 9, "player.entity_interaction_range" }, { 10, "generic.fall_damage_multiplier" }, { 11, "generic.flying_speed" }, { 12, "generic.follow_range" }, { 13, "generic.gravity" }, { 14, "generic.jump_strength" }, { 15, "generic.knockback_resistance" }, { 16, "generic.luck" }, { 17, "generic.max_absorption" }, { 18, "generic.max_health" }, { 19, "player.mining_efficiency" }, { 20, "generic.movement_efficiency" }, { 21, "generic.movement_speed" }, { 22, "generic.oxygen_bonus" }, { 23, "generic.safe_fall_distance" }, { 24, "generic.scale" }, { 25, "player.sneaking_speed" }, { 26, "zombie.spawn_reinforcements" }, { 27, "generic.step_height" }, { 28, "player.submerged_mining_speed" }, { 29, "player.sweeping_damage_ratio" }, { 30, "generic.water_movement_efficiency" } }; } /// /// Store one dimension - Directly used in 1.16.2 to 1.18.2 /// /// Dimension name /// Dimension Type nbt data public static void StoreOneDimension(string dimensionName, Dictionary dimensionType) { if (dimensionList.ContainsKey(dimensionName)) dimensionList.Remove(dimensionName); dimensionList.Add(dimensionName, new Dimension(dimensionName, dimensionType)); } /// /// Set current dimension - 1.16 and above /// /// The name of the dimension type /// The dimension type (NBT Tag Compound) public static void SetDimension(string name) { // Try to get the dimension using the name as is if (dimensionList.TryGetValue(name, out Dimension? dimension)) { curDimension = dimension; return; // Dimension found } // If not found, check if name lacks 'minecraft:' prefix and try again if (!name.StartsWith("minecraft:")) { string prefixedName = "minecraft:" + name; if (dimensionList.TryGetValue(prefixedName, out dimension)) { curDimension = dimension; return; // Dimension found with prefixed name } } // If still not found, dimension does not exist throw new KeyNotFoundException($"Dimension '{name}' not found in dimensions dictionary."); } /// /// Get current dimension /// /// Current dimension public static Dimension GetDimension() { return curDimension; } /// /// Set chunk column at the specified location /// /// ChunkColumn X /// ChunkColumn Y /// ChunkColumn Z /// ChunkColumn size /// Chunk data /// Whether the ChunkColumn has been fully loaded public void StoreChunk(int chunkX, int chunkY, int chunkZ, int chunkColumnSize, Chunk? chunk, bool loadCompleted) { ChunkColumn chunkColumn = chunks.GetOrAdd(new(chunkX, chunkZ), (_) => new(chunkColumnSize)); chunkColumn[chunkY] = chunk; if (loadCompleted) chunkColumn.FullyLoaded = true; } /// /// Get chunk column at the specified location /// /// Location to retrieve chunk column /// The chunk column public ChunkColumn? GetChunkColumn(Location location) { return this[location.ChunkX, location.ChunkZ]; } /// /// Get block at the specified location /// /// Location to retrieve block from /// Block at specified location or Air if the location is not loaded public Block GetBlock(Location location) { ChunkColumn? column = GetChunkColumn(location); if (column is not null) { Chunk? chunk = column.GetChunk(location); if (chunk is not null) return chunk.GetBlock(location); } return Block.Air; } /// /// Look for a block around the specified location /// /// Start location /// Block type /// Search radius - larger is slower: O^3 complexity /// Block matching the specified block type public List FindBlock(Location from, Material block, double radius) { return FindBlock(from, block, radius, radius, radius); } /// /// Look for a block around the specified location /// /// Start location /// Block type /// Search radius on the X axis /// Search radius on the Y axis /// Search radius on the Z axis /// Block matching the specified block type public List FindBlock(Location from, Material block, double radiusx, double radiusy, double radiusz) { Location minPoint = new Location(from.X - radiusx, from.Y - radiusy, from.Z - radiusz); Location maxPoint = new Location(from.X + radiusx, from.Y + radiusy, from.Z + radiusz); List xRange = Enumerable.Range(Convert.ToInt32(Math.Floor(minPoint.X)), Convert.ToInt32(Math.Floor(maxPoint.X - minPoint.X)) + 1).ToList(); List yRange = Enumerable.Range(Convert.ToInt32(Math.Floor(minPoint.Y)), Convert.ToInt32(Math.Floor(maxPoint.Y - minPoint.Y)) + 1).ToList(); List zRange = Enumerable.Range(Convert.ToInt32(Math.Floor(minPoint.Z)), Convert.ToInt32(Math.Floor(maxPoint.Z - minPoint.Z)) + 1).ToList(); List listOfBlocks = xRange.SelectMany(x => yRange.SelectMany(y => zRange.Select(z => new Location(x, y, z)))).ToList(); return listOfBlocks.Where(loc => GetBlock(loc).Type == block).ToList(); } /// /// Set block at the specified location /// /// Location to set block to /// Block to set public void SetBlock(Location location, Block block) { ChunkColumn? column = this[location.ChunkX, location.ChunkZ]; if (column is not null && column.ColumnSize >= location.ChunkY) { Chunk? chunk = column.GetChunk(location); if (chunk is null) column[location.ChunkY] = chunk = new Chunk(); chunk[location.ChunkBlockX, location.ChunkBlockY, location.ChunkBlockZ] = block; } } /// /// Clear all terrain data from the world /// public void Clear() { chunks = new(); chunkCnt = 0; chunkLoadNotCompleted = 0; } public static string GetChunkLoadingStatus(World world) { double chunkLoadedRatio; if (world.chunkCnt == 0) chunkLoadedRatio = 0; else chunkLoadedRatio = (world.chunkCnt - world.chunkLoadNotCompleted) / (double)world.chunkCnt; string status = string.Format(Translations.cmd_move_chunk_loading_status, chunkLoadedRatio, world.chunkCnt - world.chunkLoadNotCompleted, world.chunkCnt); return status; } } }