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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
|
package gregtech.api.util;
import static gregtech.api.enums.Mods.GregTech;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.ParametersAreNonnullByDefault;
import net.minecraft.world.ChunkCoordIntPair;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.world.WorldEvent;
import org.apache.commons.io.FileUtils;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import gregtech.api.enums.GTValues;
import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
/**
* A utility to save all kinds of data that is a function of any chunk.
* <p>
* GregTech takes care of saving and loading the data from disk, and an efficient mechanism to locate it. Subclass only
* need to define the exact scheme of each element data (by overriding the three protected abstract method)
* <p>
* Oh, there is no limit on how large your data is, though you'd not have the familiar NBT interface, but DataOutput
* should be reasonably common anyway.
* <p>
* It should be noted this class is NOT thread safe.
* <p>
* Element cannot be null.
* <p>
* TODO: Implement automatic region unloading.
*
* @param <T> data element type
* @author glease
*/
@ParametersAreNonnullByDefault
public abstract class GTChunkAssociatedData<T extends GTChunkAssociatedData.IData> {
private static final Map<String, GTChunkAssociatedData<?>> instances = new ConcurrentHashMap<>();
private static final int IO_PARALLELISM = Math.min(
8,
Math.max(
1,
Runtime.getRuntime()
.availableProcessors() * 2
/ 3));
private static final ExecutorService IO_WORKERS = Executors.newWorkStealingPool(IO_PARALLELISM);
private static final Pattern FILE_PATTERN = Pattern.compile("(.+)\\.(-?\\d+)\\.(-?\\d+)\\.dat");
static {
// register event handler
new EventHandler();
}
protected final String mId;
protected final Class<T> elementtype;
private final int regionLength;
private final int version;
private final boolean saveDefaults;
/**
* Data is stored as a `(world id -> (super region id -> super region data))` hash map. where super region's size is
* determined by regionSize. Here it is called super region, to not confuse with vanilla's regions.
*/
private final Map<Integer, Map<ChunkCoordIntPair, SuperRegion>> masterMap = new ConcurrentHashMap<>();
/**
* Initialize this instance.
*
* @param aId An arbitrary, but globally unique identifier for what this data is
* @param elementType The class of this element type. Used to create arrays.
* @param regionLength The length of one super region. Each super region will contain
* {@code regionLength * regionLength} chunks
* @param version An integer marking the version of this data. Useful later when the data's serialized form
* changed.
*/
protected GTChunkAssociatedData(String aId, Class<T> elementType, int regionLength, byte version,
boolean saveDefaults) {
if (regionLength * regionLength > Short.MAX_VALUE || regionLength <= 0)
throw new IllegalArgumentException("Region invalid: " + regionLength);
if (!IData.class.isAssignableFrom(elementType)) throw new IllegalArgumentException("Data type invalid");
if (aId.contains(".")) throw new IllegalArgumentException("ID cannot contains dot");
this.mId = aId;
this.elementtype = elementType;
this.regionLength = regionLength;
this.version = version;
this.saveDefaults = saveDefaults;
if (instances.putIfAbsent(aId, this) != null)
throw new IllegalArgumentException("Duplicate GTChunkAssociatedData: " + aId);
}
private ChunkCoordIntPair getRegionID(int aChunkX, int aChunkZ) {
return new ChunkCoordIntPair(Math.floorDiv(aChunkX, regionLength), Math.floorDiv(aChunkZ, regionLength));
}
/**
* Get a reference to data of the chunk that tile entity is in. The returned reference should be mutable.
*/
public final T get(IGregTechTileEntity tileEntity) {
return get(tileEntity.getWorld(), tileEntity.getXCoord() >> 4, tileEntity.getZCoord() >> 4);
}
public final T get(Chunk chunk) {
return get(chunk.worldObj, chunk.xPosition, chunk.zPosition);
}
public final T get(World world, ChunkCoordIntPair coord) {
return get(world, coord.chunkXPos, coord.chunkZPos);
}
public final T get(World world, int chunkX, int chunkZ) {
SuperRegion region = masterMap.computeIfAbsent(world.provider.dimensionId, ignored -> new ConcurrentHashMap<>())
.computeIfAbsent(getRegionID(chunkX, chunkZ), c -> new SuperRegion(world, c));
return region.get(Math.floorMod(chunkX, regionLength), Math.floorMod(chunkZ, regionLength));
}
protected final void set(World world, int chunkX, int chunkZ, T data) {
SuperRegion region = masterMap.computeIfAbsent(world.provider.dimensionId, ignored -> new ConcurrentHashMap<>())
.computeIfAbsent(getRegionID(chunkX, chunkZ), c -> new SuperRegion(world, c));
region.set(Math.floorMod(chunkX, regionLength), Math.floorMod(chunkZ, regionLength), data);
}
protected final boolean isCreated(int dimId, int chunkX, int chunkZ) {
Map<ChunkCoordIntPair, SuperRegion> dimData = masterMap.getOrDefault(dimId, null);
if (dimData == null) return false;
SuperRegion region = dimData.getOrDefault(getRegionID(chunkX, chunkZ), null);
if (region == null) return false;
return region.isCreated(Math.floorMod(chunkX, regionLength), Math.floorMod(chunkZ, regionLength));
}
public void clear() {
if (GTValues.debugWorldData) {
long dirtyRegionCount = masterMap.values()
.stream()
.flatMap(
m -> m.values()
.stream())
.filter(SuperRegion::isDirty)
.count();
if (dirtyRegionCount > 0) GTLog.out.println(
"Clearing ChunkAssociatedData with " + dirtyRegionCount + " regions dirty. Data might have been lost!");
}
masterMap.clear();
}
public void save() {
saveRegions(
masterMap.values()
.stream()
.flatMap(
m -> m.values()
.stream()));
}
public void save(World world) {
Map<ChunkCoordIntPair, SuperRegion> map = masterMap.get(world.provider.dimensionId);
if (map != null) saveRegions(
map.values()
.stream());
}
private void saveRegions(Stream<SuperRegion> stream) {
stream.filter(SuperRegion::isDirty)
.map(c -> (Runnable) c::save)
.map(r -> CompletableFuture.runAsync(r, IO_WORKERS))
.reduce(CompletableFuture::allOf)
.ifPresent(f -> {
try {
f.get();
} catch (Exception e) {
GTLog.err.println("Data save error: " + mId);
e.printStackTrace(GTLog.err);
}
});
}
protected abstract void writeElement(DataOutput output, T element, World world, int chunkX, int chunkZ)
throws IOException;
protected abstract T readElement(DataInput input, int version, World world, int chunkX, int chunkZ)
throws IOException;
protected abstract T createElement(World world, int chunkX, int chunkZ);
/**
* Clear all mappings, regardless of whether they are dirty
*/
public static void clearAll() {
for (GTChunkAssociatedData<?> d : instances.values()) d.clear();
}
/**
* Save all mappings
*/
public static void saveAll() {
for (GTChunkAssociatedData<?> d : instances.values()) d.save();
}
/**
* Load data for all chunks for a given world. Current data for that world will be discarded. If this is what you
* intended, call {@link #save(World)} beforehand.
* <p>
* Be aware of the memory consumption though.
*/
protected void loadAll(World w) {
if (GTValues.debugWorldData && masterMap.containsKey(w.provider.dimensionId)) GTLog.err.println(
"Reloading ChunkAssociatedData " + mId + " for world " + w.provider.dimensionId + " discards old data!");
if (!getSaveDirectory(w).isDirectory())
// nothing to load...
return;
try (Stream<Path> stream = Files.list(getSaveDirectory(w).toPath())) {
Map<ChunkCoordIntPair, SuperRegion> worldData = stream.map(f -> {
Matcher matcher = FILE_PATTERN.matcher(
f.getFileName()
.toString());
return matcher.matches() ? matcher : null;
})
.filter(Objects::nonNull)
.filter(m -> mId.equals(m.group(1)))
.map(
m -> CompletableFuture.supplyAsync(
() -> new SuperRegion(w, Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3))),
IO_WORKERS))
.map(f -> {
try {
return f.get();
} catch (Exception e) {
GTLog.err.println("Error loading region");
e.printStackTrace(GTLog.err);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(SuperRegion::getCoord, Function.identity()));
masterMap.put(w.provider.dimensionId, worldData);
} catch (IOException | UncheckedIOException e) {
GTLog.err.println("Error loading all region");
e.printStackTrace(GTLog.err);
}
}
protected File getSaveDirectory(World w) {
File base;
if (w.provider.getSaveFolder() == null) base = w.getSaveHandler()
.getWorldDirectory();
else base = new File(
w.getSaveHandler()
.getWorldDirectory(),
w.provider.getSaveFolder());
return new File(base, GregTech.ID);
}
public interface IData {
/**
* @return Whether the data is different from chunk default
*/
boolean isSameAsDefault();
}
protected final class SuperRegion {
private final T[] data = createData();
private final File backingStorage;
private final WeakReference<World> world;
/**
* Be aware, this means region coord, not bottom-left chunk coord
*/
private final ChunkCoordIntPair coord;
private SuperRegion(World world, int regionX, int regionZ) {
this.world = new WeakReference<>(world);
this.coord = new ChunkCoordIntPair(regionX, regionZ);
backingStorage = new File(getSaveDirectory(world), String.format("%s.%d.%d.dat", mId, regionX, regionZ));
if (backingStorage.isFile()) load();
}
private SuperRegion(World world, ChunkCoordIntPair regionCoord) {
this.world = new WeakReference<>(world);
this.coord = regionCoord;
backingStorage = new File(
getSaveDirectory(world),
String.format("%s.%d.%d.dat", mId, regionCoord.chunkXPos, regionCoord.chunkZPos));
if (backingStorage.isFile()) load();
}
@SuppressWarnings("unchecked")
private T[] createData() {
return (T[]) Array.newInstance(elementtype, regionLength * regionLength);
}
public T get(int subRegionX, int subRegionZ) {
int index = getIndex(subRegionX, subRegionZ);
T datum = data[index];
if (datum == null) {
World world = Objects.requireNonNull(this.world.get());
T newElem = createElement(
world,
coord.chunkXPos * regionLength + subRegionX,
coord.chunkZPos * regionLength + subRegionZ);
data[index] = newElem;
return newElem;
}
return datum;
}
public void set(int subRegionX, int subRegionZ, T data) {
this.data[getIndex(subRegionX, subRegionZ)] = data;
}
public boolean isCreated(int subRegionX, int subRegionZ) {
return this.data[getIndex(subRegionX, subRegionZ)] != null;
}
public ChunkCoordIntPair getCoord() {
return coord;
}
private int getIndex(int subRegionX, int subRegionY) {
return subRegionX * regionLength + subRegionY;
}
private int getChunkX(int index) {
return index / regionLength + coord.chunkXPos * regionLength;
}
private int getChunkZ(int index) {
return index % regionLength + coord.chunkZPos * regionLength;
}
public boolean isDirty() {
for (T datum : data) {
if (datum != null && !datum.isSameAsDefault()) return true;
}
return false;
}
public void save() {
try {
save0();
} catch (IOException e) {
GTLog.err.println("Error saving data " + backingStorage.getPath());
e.printStackTrace(GTLog.err);
}
}
private void save0() throws IOException {
// noinspection ResultOfMethodCallIgnored
backingStorage.getParentFile()
.mkdirs();
File tmpFile = getTmpFile();
World world = Objects.requireNonNull(this.world.get(), "Attempting to save region of another world!");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(tmpFile))) {
int ptr = 0;
boolean nullRange = data[0] == null;
// write a magic byte as storage format version
output.writeByte(0);
// write a magic byte as data format version
output.writeByte(version);
output.writeBoolean(nullRange);
while (ptr < data.length) {
// work out how long is this range
int rangeStart = ptr;
while (ptr < data.length
&& (data[ptr] == null || (!saveDefaults && data[ptr].isSameAsDefault())) == nullRange) ptr++;
// write range length
output.writeShort(ptr - rangeStart);
if (!nullRange)
// write element data
for (int i = rangeStart; i < ptr; i++)
writeElement(output, data[i], world, getChunkX(ptr), getChunkZ(ptr));
// or not
nullRange = !nullRange;
}
}
// first try to replace the destination file
// since atomic operation, no need to keep the backup in place
try {
Files.move(
tmpFile.toPath(),
backingStorage.toPath(),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException ignored) {
// in case some dumb system/jre combination would cause this
// or if **somehow** two file inside the same directory belongs two separate filesystem
FileUtils.copyFile(tmpFile, backingStorage);
}
}
public void load() {
try {
loadFromFile(backingStorage);
} catch (IOException | RuntimeException e) {
GTLog.err.println("Primary storage file broken in " + backingStorage.getPath());
e.printStackTrace(GTLog.err);
// in case the primary storage is broken
try {
loadFromFile(getTmpFile());
} catch (IOException | RuntimeException e2) {
GTLog.err.println("Backup storage file broken in " + backingStorage.getPath());
e2.printStackTrace(GTLog.err);
}
}
}
private void loadFromFile(File file) throws IOException {
World world = Objects.requireNonNull(this.world.get(), "Attempting to load region of another world!");
try (DataInputStream input = new DataInputStream(new FileInputStream(file))) {
byte b = input.readByte();
if (b == 0) {
loadV0(input, world);
} else {
GTLog.err.printf("Unknown ChunkAssociatedData version %d\n", b);
}
}
}
private void loadV0(DataInput input, World world) throws IOException {
int version = input.readByte();
boolean nullRange = input.readBoolean();
int ptr = 0;
while (ptr != data.length) {
int rangeEnd = ptr + input.readUnsignedShort();
if (!nullRange) {
for (; ptr < rangeEnd; ptr++) {
data[ptr] = readElement(input, version, world, getChunkX(ptr), getChunkZ(ptr));
}
} else {
Arrays.fill(data, ptr, rangeEnd, null);
ptr = rangeEnd;
}
nullRange = !nullRange;
}
}
private File getTmpFile() {
return new File(backingStorage.getParentFile(), backingStorage.getName() + ".tmp");
}
}
public static class EventHandler {
private EventHandler() {
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void onWorldSave(WorldEvent.Save e) {
for (GTChunkAssociatedData<?> d : instances.values()) {
d.save(e.world);
}
}
@SubscribeEvent
public void onWorldUnload(WorldEvent.Unload e) {
for (GTChunkAssociatedData<?> d : instances.values()) {
// there is no need to explicitly do a save here
// forge will send a WorldEvent.Save on server thread before this event is distributed
d.masterMap.remove(e.world.provider.dimensionId);
}
}
}
}
|