1 /*
2  * Copyright (c) 2017
3  * 
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12  * See the GNU Lesser General Public License for more details.
13  * 
14  */
15 module sel.nbt.file;
16 
17 // test
18 import std.conv;
19 import std.stdio : writeln;
20 
21 static import std.bitmanip;
22 static import std.file;
23 import std.system : Endian;
24 import std.traits : isAbstractClass;
25 import std.typetuple : TypeTuple;
26 import std.zlib;
27 
28 import sel.nbt.stream;
29 import sel.nbt.tags;
30 
31 enum Compression {
32 
33 	deflate,
34 	gzip,
35 	none
36 
37 }
38 
39 /**
40  * Example:
41  * ---
42  * alias Level = Format!(Named!Compound, ClassicStream!(Endian.bigEndian), Compression.gzip);
43  * auto level = Level.read("level.dat");
44  * level.get!Compound("data")["LevelName"] = "Edited from code!";
45  * Level.write(level, "level.dat");
46  * ---
47  */
48 class Format(T : Tag, S : Stream, Compression c, int level=6) if(!isAbstractClass!T && !isAbstractClass!S) {
49 
50 	private static S stream;
51 
52 	public static this() {
53 		stream = new S();
54 	}
55 
56 	public string location;
57 	public T tag;
58 
59 	public this(string location, T tag=null) {
60 		this.location = location;
61 		this.tag = tag;
62 	}
63 
64 	public this(T tag, string location="") {
65 		this(location, tag);
66 	}
67 	
68 	public T load() {
69 		
70 		ubyte[] data = cast(ubyte[])std.file.read(this.location);
71 
72 		this.loadHeader(data);
73 		
74 		static if(c != Compression.none) {
75 			UnCompress uc = new UnCompress(cast(HeaderFormat)c);
76 			data = cast(ubyte[])uc.uncompress(data);
77 			data ~= cast(ubyte[])uc.flush();
78 		}
79 		
80 		stream.buffer = data;
81 
82 		//TODO nameless tag
83 		this.tag = cast(T)stream.readTag();
84 
85 		return this.tag;
86 		
87 	}
88 
89 	protected void loadHeader(ref ubyte[] data) {}
90 
91 	public T save() {
92 
93 		stream.buffer.length = 0;
94 		//TODO nameless tag
95 		stream.writeTag(this.tag);
96 		ubyte[] data = stream.buffer;
97 
98 		static if(c != Compression.none) {
99 			Compress compress = new Compress(level, cast(HeaderFormat)c);
100 			data = cast(ubyte[])compress.compress(data);
101 			data ~= cast(ubyte[])compress.flush();
102 		}
103 
104 		this.saveHeader(data);
105 
106 		std.file.write(this.location, data);
107 
108 		return this.tag;
109 
110 	}
111 
112 	protected void saveHeader(ref ubyte[] data) {}
113 
114 	alias tag this;
115 
116 }
117 
118 alias MinecraftLevelFormat = Format!(Compound, ClassicStream!(Endian.bigEndian), Compression.gzip);
119 
120 class PocketLevelFormat : Format!(Compound, ClassicStream!(Endian.littleEndian), Compression.none) {
121 
122 	private uint v = 5;
123 
124 	public this(string location, Compound tag=null) {
125 		super(location, tag);
126 	}
127 
128 	public this(Compound tag, string location="") {
129 		super(tag, location);
130 	}
131 
132 	protected override void loadHeader(ref ubyte[] data) {
133 		if(data.length >= 8) {
134 			this.v = std.bitmanip.read!(uint, Endian.littleEndian)(data);
135 			size_t size = std.bitmanip.read!(uint, Endian.littleEndian)(data);
136 			assert(data.length == size);
137 		}
138 	}
139 
140 	protected override void saveHeader(ref ubyte[] data) {
141 		data = std.bitmanip.nativeToLittleEndian(this.v).dup ~ std.bitmanip.nativeToLittleEndian(cast(uint)data.length).dup ~ data;
142 	}
143 
144 	alias tag this;
145 
146 }
147 
148 unittest {
149 
150 	auto minecraft = new MinecraftLevelFormat("test/minecraft.dat");
151 	minecraft.load();
152 	auto data = minecraft.get!Compound("Data", null);
153 	assert(data !is null);
154 	assert(data.has!String("LevelName") && data.get!String("LevelName", null) == "New World");
155 	assert(data.has!Int("version") && data.get!Int("version", null) == 19133);
156 
157 	minecraft = new MinecraftLevelFormat(new Compound(new Named!Int("Test", 42)), "test.dat");
158 	minecraft.save();
159 	auto u = new UnCompress(HeaderFormat.gzip);
160 	auto b = cast(ubyte[])u.uncompress(std.file.read("test.dat"));
161 	b ~= cast(ubyte[])u.flush();
162 	assert(b == [10, 0, 0, 3, 0, 4, 84, 101, 115, 116, 0, 0, 0, 42, 0]);
163 
164 	auto pocket = new PocketLevelFormat("test/pocket.dat");
165 	pocket.load();
166 	assert(pocket.has!String("LevelName") && pocket.get!String("LevelName", null) == "AAAAAAAAAA");
167 	assert(pocket.has!Int("Difficulty") && pocket.get!Int("Difficulty", null) == 2);
168 
169 	pocket = new PocketLevelFormat(new Compound(new Named!Int("Test", 42)), "test.dat");
170 	pocket.save();
171 	assert(cast(ubyte[])std.file.read("test.dat") == [5, 0, 0, 0, 15, 0, 0, 0, 10, 0, 0, 3, 4, 0, 84, 101, 115, 116, 42, 0, 0, 0, 0]);
172 
173 }