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 }