/
McMMOSimpleRegionFile.java
260 lines (215 loc) · 9.43 KB
/
McMMOSimpleRegionFile.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/*
* This file is part of SpoutPlugin.
*
* Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
* SpoutPlugin is licensed under the GNU Lesser General Public License.
*
* SpoutPlugin is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SpoutPlugin is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.gmail.nossr50.util.blockmeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.BitSet;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
/**
* File format:
* bytes 0-4096 contain 1024 integer values representing the segment index of each chunk
* bytes 4096-8192 contain 1024 integer values representing the byte length of each chunk
* bytes 8192-8196 is the integer value of the segment exponent
* bytes 8196-12288 are reserved for future use
* bytes 12288+ contain the data segments, by default 1024 byte segments.
* Chunk data is compressed and stored in 1 or more segments as needed.
*/
public class McMMOSimpleRegionFile {
private static final int DEFAULT_SEGMENT_EXPONENT = 10; // TODO, analyze real world usage and determine if a smaller segment(512) is worth it or not. (need to know average chunkstore bytesize)
private static final int DEFAULT_SEGMENT_SIZE = (int)Math.pow(2, DEFAULT_SEGMENT_EXPONENT); // 1024
private static final int RESERVED_HEADER_BYTES = 12288; // This needs to be divisible by segment size
private static final int NUM_CHUNKS = 1024; // 32x32
private static final int SEEK_CHUNK_SEGMENT_INDICES = 0;
private static final int SEEK_CHUNK_BYTE_LENGTHS = 4096;
private static final int SEEK_FILE_INFO = 8192;
// Chunk info
private final int[] chunkSegmentIndex = new int[NUM_CHUNKS];
private final int[] chunkNumBytes = new int[NUM_CHUNKS];
private final int[] chunkNumSegments = new int[NUM_CHUNKS];
// Segments
private final BitSet segments = new BitSet(); // Used to denote which segments are in use or not
// Segment size/mask
private final int segmentExponent;
private final int segmentMask;
// File location
private final @NotNull File parent;
// File access
private final RandomAccessFile file;
// Region index
private final int rx;
private final int rz;
public McMMOSimpleRegionFile(@NotNull File f, int rx, int rz) {
this.rx = rx;
this.rz = rz;
this.parent = f;
try {
this.file = new RandomAccessFile(parent, "rw");
// New file, write out header bytes
if (file.length() < RESERVED_HEADER_BYTES) {
file.write(new byte[RESERVED_HEADER_BYTES]);
file.seek(SEEK_FILE_INFO);
file.writeInt(DEFAULT_SEGMENT_EXPONENT);
}
file.seek(SEEK_FILE_INFO);
this.segmentExponent = file.readInt();
this.segmentMask = (1 << segmentExponent) - 1;
// Mark reserved segments reserved
int reservedSegments = this.bytesToSegments(RESERVED_HEADER_BYTES);
segments.set(0, reservedSegments, true);
// Read chunk header data
file.seek(SEEK_CHUNK_SEGMENT_INDICES);
for (int i = 0; i < NUM_CHUNKS; i++)
chunkSegmentIndex[i] = file.readInt();
file.seek(SEEK_CHUNK_BYTE_LENGTHS);
for (int i = 0; i < NUM_CHUNKS; i++) {
chunkNumBytes[i] = file.readInt();
chunkNumSegments[i] = bytesToSegments(chunkNumBytes[i]);
markChunkSegments(i, true);
}
fixFileLength();
}
catch (IOException fnfe) {
throw new RuntimeException(fnfe);
}
}
public synchronized @NotNull DataOutputStream getOutputStream(int x, int z) {
int index = getChunkIndex(x, z); // Get chunk index
return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
}
private static class McMMOSimpleChunkBuffer extends ByteArrayOutputStream {
final McMMOSimpleRegionFile rf;
final int index;
McMMOSimpleChunkBuffer(McMMOSimpleRegionFile rf, int index) {
super(DEFAULT_SEGMENT_SIZE);
this.rf = rf;
this.index = index;
}
@Override
public void close() throws IOException {
rf.write(index, buf, count);
}
}
private synchronized void write(int index, byte[] buffer, int size) throws IOException {
int oldSegmentIndex = chunkSegmentIndex[index]; // Get current segment index
markChunkSegments(index, false); // Clear our old segments
int newSegmentIndex = findContiguousSegments(oldSegmentIndex, size); // Find contiguous segments to save to
file.seek((long) newSegmentIndex << segmentExponent); // Seek to file location
file.write(buffer, 0, size); // Write data
// update in memory info
chunkSegmentIndex[index] = newSegmentIndex;
chunkNumBytes[index] = size;
chunkNumSegments[index] = bytesToSegments(size);
// Mark segments in use
markChunkSegments(index, true);
// Update header info
file.seek(SEEK_CHUNK_SEGMENT_INDICES + (4L * index));
file.writeInt(chunkSegmentIndex[index]);
file.seek(SEEK_CHUNK_BYTE_LENGTHS + (4L * index));
file.writeInt(chunkNumBytes[index]);
}
public synchronized @Nullable DataInputStream getInputStream(int x, int z) throws IOException {
int index = getChunkIndex(x, z); // Get chunk index
int byteLength = chunkNumBytes[index]; // Get byte length of data
// No bytes
if (byteLength == 0)
return null;
byte[] data = new byte[byteLength];
file.seek((long) chunkSegmentIndex[index] << segmentExponent); // Seek to file location
file.readFully(data); // Read in the data
return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
}
public synchronized void close() {
try {
file.close();
segments.clear();
}
catch (IOException ioe) {
throw new RuntimeException("Unable to close file", ioe);
}
}
private synchronized void markChunkSegments(int index, boolean inUse) {
// No bytes used
if (chunkNumBytes[index] == 0)
return;
int start = chunkSegmentIndex[index];
int end = start + chunkNumSegments[index];
// If we are writing, assert we don't write over any in-use segments
if (inUse)
{
int nextSetBit = segments.nextSetBit(start);
if (nextSetBit != -1 && nextSetBit < end)
throw new IllegalStateException("Attempting to overwrite an in-use segment");
}
segments.set(start, end, inUse);
}
private synchronized void fixFileLength() throws IOException {
int fileLength = (int)file.length();
int extend = -fileLength & segmentMask; // how many bytes do we need to be divisible by segment size
// Go to end of file
file.seek(fileLength);
// Append bytes
file.write(new byte[extend], 0, extend);
}
private synchronized int findContiguousSegments(int hint, int size) {
if (size == 0)
return 0; // Zero byte data will not claim any chunks anyways
int segments = bytesToSegments(size); // Number of segments we need
// Check the hinted location (previous location of chunk) most of the time we can fit where we were.
boolean oldFree = true;
for (int i = hint; i < this.segments.size() && i < hint + segments; i++) {
if (this.segments.get(i)) {
oldFree = false;
break;
}
}
// We fit!
if (oldFree)
return hint;
// Find somewhere to put us
int start = 0;
int current = 0;
while (current < this.segments.size()) {
boolean segmentInUse = this.segments.get(current); // check if segment is in use
current++; // Move up a segment
// Move up start if the segment was in use
if (segmentInUse)
start = current;
// If we have enough segments now, return
if (current - start >= segments)
return start;
}
// Return the end of the segments (will expand to fit them)
return start;
}
private synchronized int bytesToSegments(int bytes) {
if (bytes <= 0)
return 1;
return ((bytes - 1) >> segmentExponent) + 1; // ((bytes - 1) / segmentSize) + 1
}
private synchronized int getChunkIndex(int x, int z) {
if (rx != (x >> 5) || rz != (z >> 5))
throw new IndexOutOfBoundsException();
x = x & 0x1F; // 5 bits (mod 32)
z = z & 0x1F; // 5 bits (mod 32)
return (x << 5) + z; // x in the upper 5 bits, z in the lower 5 bits
}
}