1 /* 2 * Copyright (c) 2017-2020 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: Copyright (c) 2017-2020 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/sel-level/sel/level/format/anvil.d, sel/level/format/anvil.d) 28 */ 29 module sel.level.format.anvil; 30 31 import std.algorithm : sort; 32 import std.bitmanip : peek; 33 import std.conv : to, ConvException; 34 import std.file : exists, isFile, read, write, dirEntries, SpanMode, FileException; 35 import std.path : dirSeparator; 36 import std..string : endsWith, split; 37 import std.system : Endian; 38 import std.typetuple : TypeTuple; 39 import std.zlib : Compress, UnCompress, HeaderFormat, ZlibException; 40 41 import sel.level.data; 42 import sel.level.exception; 43 import sel.level.level; 44 45 import sel.math : Vector2; 46 47 import sel.nbt.file : JavaLevelFormat; 48 import sel.nbt.stream : Stream, ClassicStream; 49 import sel.nbt.tags; 50 51 import xbuffer : Buffer; 52 53 import std.stdio : writeln; // debug 54 55 private alias LevelInfoValues = TypeTuple!( 56 String, "name", "LevelName", 57 Long, "seed", "RandomSeed", 58 Int, "gamemode", "GameType", 59 Int, "difficulty", "Difficulty", 60 Byte, "hardcore", "hardcore", 61 Long, "time", "Time", 62 Long, "dayTime", "DayTime", 63 Int, "spawn.x", "SpawnX", 64 Int, "spawn.y", "SpawnY", 65 Int, "spawn.z", "SpawnZ", 66 Byte, "raining", "raining", 67 Int, "rainTime", "rainTime", 68 Byte, "thundering", "thundering", 69 Int, "thunderTime", "thunderTime", 70 Byte, "commandsAllowed", "allowCommands", 71 ); 72 73 abstract class AbstractAnvil : Level { 74 75 private JavaLevelFormat infoReader; 76 77 private ubyte[][Vector2!int] regions; 78 79 public this(string path) { 80 super(path); 81 this.infoReader = new JavaLevelFormat(this.path ~ "level.dat"); 82 } 83 84 protected override LevelInfo readLevelInfo() { 85 Compound compound; 86 try { 87 compound = this.infoReader.load(); 88 } catch(FileException) { 89 throw new LevelInfoException(LevelInfoException.NOT_FOUND, "Level info was not found"); 90 } catch(ZlibException) { 91 throw new LevelInfoException(LevelInfoException.BADLY_COMPRESSED, "Level info was badly compressed"); 92 } 93 enforceLevelInfoException(compound !is null, LevelInfoException.WRONG_FORMAT, "Root tag is not a compound"); 94 enforceLevelInfoException(compound.has!Compound("Data"), LevelInfoException.WRONG_FORMAT, "Compound has no data tag"); 95 LevelInfo ret = readLevelInfoCompound!LevelInfoValues(cast(Compound)compound["Data"]); 96 foreach(gamerule ; compound.getValue!Compound("GameRules", [])) { 97 if(cast(String)gamerule) { 98 immutable value = (cast(String)gamerule).value; 99 if(value == "true") { 100 ret.gamerules[gamerule.name] = LevelInfo.GameRule(true); 101 } else if(value == "false") { 102 ret.gamerules[gamerule.name] = LevelInfo.GameRule(false); 103 } else { 104 try { 105 ret.gamerules[gamerule.name] = LevelInfo.GameRule(to!int(value)); 106 } catch(ConvException) { 107 throw new LevelInfoException(LevelInfoException.WRONG_VALUE, "Gamerule " ~ gamerule.name ~ " cannot be converted to integer"); 108 } 109 } 110 } 111 } 112 return ret; 113 } 114 115 protected override void writeLevelInfo(LevelInfo levelInfo) { 116 auto data = writeLevelInfoCompound!LevelInfoValues(levelInfo); 117 if(levelInfo.gamerules.length) { 118 auto compound = new Named!Compound("GameRules"); 119 foreach(name, gamerule; levelInfo.gamerules) { 120 compound[] = new Named!String(name, gamerule.isBool ? to!string(gamerule.bool_) : to!string(gamerule.int_)); 121 } 122 data[] = compound; 123 } 124 this.infoReader.tag = new Compound(data.rename("Data")); 125 this.infoReader.save(); 126 } 127 128 protected override Chunk readChunkImpl(Dimension dimension, Vector2!int position) { 129 auto savedChunk = position in chunks; 130 if(savedChunk) return *savedChunk; 131 Vector2!int regionPosition = position >> 5; 132 immutable file = this.path ~ dimensionPath(dimension) ~ dirSeparator ~ "r." ~ regionPosition.x.to!string ~ "." ~ regionPosition.z.to!string ~ ".mca"; 133 if(exists(file)) { 134 // region exists 135 void enforce(bool condition, uint code, lazy string msg, string file=__FILE__, size_t line=__LINE__) { 136 enforceChunkException(condition, position, code, msg, file, line); 137 } 138 auto cached = regionPosition in this.regions; 139 ubyte[] data = cached ? *cached : cast(ubyte[])read(file); 140 enforce(data.length > 8192, ChunkException.WRONG_FORMAT, "Data is too short"); 141 // region may be valid 142 immutable infoOffset = ((position.x & 31) + (position.z & 31) * 32) * 4; 143 immutable info = peek!uint(data, infoOffset); 144 immutable timestamp = peek!uint(data, infoOffset + 4096); 145 if(info == 0) return null; 146 // chunk exists 147 immutable offset = (info >> 8) * 4096; 148 Buffer buffer = new Buffer(data[offset..offset+(info & 255)*4096]); //TODO validate and avoid range errors 149 immutable length = buffer.read!(Endian.bigEndian, uint)(); 150 enforce(buffer.read!ubyte() == 2, ChunkException.UNKNOWN_COMPRESSION_METHOD, "Chunk has an unknown compression method"); 151 UnCompress uncompress = new UnCompress(); 152 const(void)[] ucd = uncompress.uncompress(buffer.readData(length-1)); 153 ucd ~= uncompress.flush(); 154 buffer.data = ucd; 155 Compound compound = cast(Compound)new ClassicStream!(Endian.bigEndian)(buffer).readTag(); 156 enforce(compound !is null, ChunkException.WRONG_FORMAT, "Root tag is not a compound"); 157 Chunk chunk = new Chunk(position, timestamp); 158 Compound level = compound.get!Compound("Level", null); 159 enforce(level !is null, ChunkException.WRONG_FORMAT, "Level tag does not exist or is not a compound"); 160 if(level.has!IntArray("Biomes")) { 161 // read biomes 162 int[] biomes = cast(IntArray)level["Biomes"]; 163 if(biomes.length == 256) chunk.biomes = biomes; 164 } 165 if(level.has!List("Sections")) { 166 // read sections 167 List sections = cast(List)level["Sections"]; 168 enforce(sections.childType == NBT_TYPE.COMPOUND, ChunkException.WRONG_FORMAT, "Sections are not of type compound"); 169 foreach(sectionList ; sections) { 170 Compound sectionCompound = cast(Compound)sectionList; 171 enforce(sectionCompound.has!Byte("Y"), ChunkException.WRONG_FORMAT, "The Y coordinate tag is missing or has a wrong format"); 172 immutable byte y = cast(Byte)sectionCompound["Y"]; 173 enforce(y >= 0 && y <= 15, ChunkException.WRONG_FORMAT, "The Y coordinate is not is a valid range (" ~ y.to!string ~ ")"); 174 enforce(y !in chunk.sections, ChunkException.WRONG_FORMAT, "Duplicate section"); 175 Chunk.Block[] palette; 176 if(sectionCompound.has!List("Palette")) { 177 List paletteList = cast(List)sectionCompound["Palette"]; 178 enforce(paletteList.childType == NBT_TYPE.COMPOUND, ChunkException.WRONG_FORMAT, "Palette's children are not of type compound"); 179 foreach(paletteValue ; cast(List)sectionCompound["Palette"]) { 180 Compound paletteCompound = cast(Compound)paletteValue; 181 enforce(paletteCompound.has!String("Name"), ChunkException.WRONG_FORMAT, "Palette value has no name"); 182 Chunk.Block block = Chunk.Block(cast(String)paletteCompound["Name"]); 183 if(paletteCompound.has!Compound("Properties")) { 184 foreach(v ; (cast(Compound)paletteCompound["Properties"])[]) { 185 String value = cast(String)v; 186 if(value !is null) block.properties[value.name] = value; 187 } 188 } 189 palette ~= block; 190 } 191 if(sectionCompound.has!LongArray("BlockStates")) { 192 buffer.data = []; 193 foreach(value ; cast(LongArray)sectionCompound["BlockStates"]) { 194 buffer.write!(Endian.bigEndian)(value); 195 } 196 } 197 } 198 } 199 } 200 this.chunks[position] = chunk; 201 return chunk; 202 } else { 203 return null; 204 } 205 } 206 207 protected override ReadChunksResult readChunksImpl(Dimension dimension) { 208 ReadChunksResult ret; 209 immutable path = this.path ~ dimensionPath(dimension) ~ dirSeparator; 210 foreach(string file ; dirEntries(path, SpanMode.shallow)) { 211 if(file.isFile && file.endsWith(".mca")) { 212 string[] splitted = file[path.length..$-4].split("."); 213 if(splitted.length == 3 && splitted[0] == "r") { 214 try { 215 Vector2!int region = Vector2!int(to!int(splitted[1]) << 5, to!int(splitted[2]) << 5); 216 foreach(x ; region.x..region.x+32) { 217 foreach(z ; region.z..region.z+32) { 218 Vector2!int position = Vector2!int(x, z); 219 try { 220 ret.chunks[position] = this.readChunk(dimension, position); 221 } catch(ChunkException e) { 222 ret.exceptions ~= e; 223 } 224 } 225 } 226 this.regions.remove(region); 227 } catch(ConvException) {} 228 } 229 } 230 } 231 return ret; 232 } 233 234 private static string dimensionPath(Dimension dimension) { 235 if(dimension.java == 0) return "region"; 236 else return "DIM" ~ dimension.java.to!string ~ dirSeparator ~ "data"; 237 } 238 239 } 240 241 class AnvilImpl(string order) : AbstractAnvil if(order.split("").sort.release == ["x", "y", "z"]) { 242 243 public this(string path) { 244 super(path); 245 } 246 247 } 248 249 alias Anvil = AnvilImpl!"yzx"; 250 251 unittest { 252 253 Level anvil = new Anvil("test/Anvil"); 254 255 with(anvil.levelInfo) { 256 assert(name == "New World"); 257 assert(seed == 608293555344486561L); 258 assert(gamemode == 1); 259 assert(hardcore == false); 260 assert(time == 15388); 261 assert(dayTime == 15388); 262 assert(spawn.x == 8); 263 assert(spawn.y == 64); 264 assert(spawn.z == 224); 265 assert(commandsAllowed == true); 266 } 267 268 Chunk chunk = anvil.readChunk(0, 0); 269 270 assert(chunk !is null); 271 272 assert(chunk.biomes[0] == 0); 273 assert(chunk.biomes[$-1] == 7); 274 275 }