Upstream Merge

This commit is contained in:
MattBDev
2020-01-10 22:32:12 -05:00
parent b2be1ea9fb
commit 0d2fff2cd2
81 changed files with 2528 additions and 23695 deletions

View File

@ -153,6 +153,7 @@ public abstract class AbstractWorld implements World {
@Override
public void setWeather(WeatherType weatherType, long duration) {
}
private class QueuedEffect implements Comparable<QueuedEffect> {
private final Vector3 position;
private final BlockType blockType;

View File

@ -69,6 +69,7 @@ public class NullWorld extends AbstractWorld {
public String getId() {
return "null";
}
@Override
public <B extends BlockStateHolder<B>> boolean setBlock(BlockVector3 position, B block, boolean notifyAndLight) throws WorldEditException {
return false;

View File

@ -23,11 +23,12 @@ import com.sk89q.worldedit.registry.state.Property;
import com.sk89q.worldedit.world.block.BlockState;
import com.sk89q.worldedit.world.block.BlockType;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.Map;
import java.util.OptionalInt;
import javax.annotation.Nullable;
/**
* A block registry that uses {@link BundledBlockData} to serve information
* about blocks.

View File

@ -33,4 +33,4 @@ public interface ItemMaterial {
* @return the maximum damage, or 0 if not applicable
*/
int getMaxDamage();
}
}

View File

@ -56,15 +56,7 @@ import java.util.Map;
public final class LegacyMapper {
private static final Logger log = LoggerFactory.getLogger(LegacyMapper.class);
private static LegacyMapper INSTANCE = new LegacyMapper();
static {
try {
INSTANCE.loadFromResource();
} catch (Throwable e) {
log.warn("Failed to load the built-in legacy id registry", e);
}
}
private static LegacyMapper INSTANCE;
private final Int2ObjectArrayMap<Integer> blockStateToLegacyId4Data = new Int2ObjectArrayMap<>();
private final Int2ObjectArrayMap<Integer> extraId4DataToStateId = new Int2ObjectArrayMap<>();
@ -80,6 +72,12 @@ public final class LegacyMapper {
* Create a new instance.
*/
private LegacyMapper() {
try {
loadFromResource();
} catch (Throwable e) {
log.warn("Failed to load the built-in legacy id registry", e);
}
}
/**
@ -95,9 +93,8 @@ public final class LegacyMapper {
if (url == null) {
throw new IOException("Could not find legacy.json");
}
String source = Resources.toString(url, Charset.defaultCharset());
LegacyDataFile dataFile = gson.fromJson(source, new TypeToken<LegacyDataFile>() {
}.getType());
String data = Resources.toString(url, Charset.defaultCharset());
LegacyDataFile dataFile = gson.fromJson(data, new TypeToken<LegacyDataFile>() {}.getType());
DataFixer fixer = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataFixer();
ParserContext parserContext = new ParserContext();
@ -110,49 +107,51 @@ public final class LegacyMapper {
Integer combinedId = getCombinedId(blockEntry.getKey());
final String value = blockEntry.getValue();
blockEntries.put(id, value);
BlockState blockState = null;
BlockState state = null;
try {
blockState = BlockState.get(null, blockEntry.getValue());
BlockType type = blockState.getBlockType();
state = BlockState.get(null, blockEntry.getValue());
BlockType type = state.getBlockType();
if (type.hasProperty(PropertyKey.WATERLOGGED)) {
blockState = blockState.with(PropertyKey.WATERLOGGED, false);
state = state.with(PropertyKey.WATERLOGGED, false);
}
} catch (InputParseException e) {
} catch (InputParseException f) {
BlockFactory blockFactory = WorldEdit.getInstance().getBlockFactory();
// if fixer is available, try using that first, as some old blocks that were renamed share names with new blocks
if (fixer != null) {
try {
String newEntry = fixer.fixUp(DataFixer.FixTypes.BLOCK_STATE, value, 1631);
blockState = blockFactory.parseFromInput(newEntry, parserContext).toImmutableState();
} catch (InputParseException f) {
state = blockFactory.parseFromInput(newEntry, parserContext).toImmutableState();
} catch (InputParseException e) {
}
}
// if it's still null, the fixer was unavailable or failed
if (blockState == null) {
if (state == null) {
try {
blockState = blockFactory.parseFromInput(value, parserContext).toImmutableState();
} catch (InputParseException f) {
state = blockFactory.parseFromInput(value, parserContext).toImmutableState();
} catch (InputParseException e) {
}
}
// if it's still null, both fixer and default failed
if (blockState == null) {
if (state == null) {
log.debug("Unknown block: " + value);
} else {
// it's not null so one of them succeeded, now use it
blockToStringMap.put(blockState, id);
stringToBlockMap.put(id, blockState);
blockToStringMap.put(state, id);
stringToBlockMap.put(id, state);
}
}
if (blockState != null) {
blockArr[combinedId] = blockState.getInternalId();
blockStateToLegacyId4Data.put(blockState.getInternalId(), (Integer) combinedId);
blockStateToLegacyId4Data.putIfAbsent(blockState.getInternalBlockTypeId(), combinedId);
if (state != null) {
blockArr[combinedId] = state.getInternalId();
blockStateToLegacyId4Data.put(state.getInternalId(), (Integer) combinedId);
blockStateToLegacyId4Data.putIfAbsent(state.getInternalBlockTypeId(), combinedId);
}
}
for (int id = 0; id < 256; id++) {
int combinedId = id << 4;
int base = blockArr[combinedId];
if (base != 0) {
for (int data = 0; data < 16; data++, combinedId++) {
for (int data_ = 0; data_ < 16; data_++, combinedId++) {
if (blockArr[combinedId] == 0) blockArr[combinedId] = base;
}
}
@ -166,14 +165,14 @@ public final class LegacyMapper {
value = fixer.fixUp(DataFixer.FixTypes.ITEM_TYPE, value, 1631);
type = ItemTypes.get(value);
}
if (type != null) {
if (type == null) {
log.debug("Unknown item: " + value);
} else {
try {
itemMap.put(getCombinedId(id), type);
continue;
} catch (Exception e) {
} catch (Exception ignored) {
}
}
log.debug("Unknown item: " + value);
}
}
@ -289,7 +288,10 @@ public final class LegacyMapper {
return combinedId == null ? null : new int[] { combinedId >> 4, combinedId & 0xF };
}
public final static LegacyMapper getInstance() {
public static LegacyMapper getInstance() {
if (INSTANCE == null) {
INSTANCE = new LegacyMapper();
}
return INSTANCE;
}

View File

@ -42,4 +42,4 @@ public class PassthroughItemMaterial implements ItemMaterial {
public int getMaxDamage() {
return itemMaterial.getMaxDamage();
}
}
}

View File

@ -19,7 +19,7 @@
package com.sk89q.worldedit.world.registry;
class SimpleItemMaterial implements ItemMaterial {
public class SimpleItemMaterial implements ItemMaterial {
private int maxStackSize;
private int maxDamage;
@ -38,4 +38,4 @@ class SimpleItemMaterial implements ItemMaterial {
public int getMaxDamage() {
return maxDamage;
}
}
}

View File

@ -22,6 +22,7 @@
package com.sk89q.worldedit.world.snapshot;
import com.sk89q.worldedit.world.storage.MissingWorldException;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FilenameFilter;

View File

@ -0,0 +1,62 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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.sk89q.worldedit.world.snapshot.experimental;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.world.DataException;
import com.sk89q.worldedit.world.chunk.Chunk;
import com.sk89q.worldedit.world.storage.ChunkStoreHelper;
import java.io.Closeable;
import java.io.IOException;
/**
* Represents a world snapshot.
*/
public interface Snapshot extends Closeable {
SnapshotInfo getInfo();
/**
* Get the chunk information for the given position. Implementations may ignore the Y-chunk
* if its chunks are only stored in 2D.
*
* @param position the position of the chunk
* @return the tag containing chunk data
*/
CompoundTag getChunkTag(BlockVector3 position) throws DataException, IOException;
/**
* Get the chunk information for the given position.
*
* @see #getChunkTag(BlockVector3)
* @see ChunkStoreHelper#getChunk(CompoundTag)
*/
default Chunk getChunk(BlockVector3 position) throws DataException, IOException {
return ChunkStoreHelper.getChunk(getChunkTag(position));
}
/**
* Close this snapshot. This releases the IO handles used to load chunk information.
*/
@Override
void close() throws IOException;
}

View File

@ -0,0 +1,35 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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.sk89q.worldedit.world.snapshot.experimental;
import java.util.Comparator;
public class SnapshotComparator {
private static final Comparator<Snapshot> COMPARATOR =
Comparator.comparing(Snapshot::getInfo);
public static Comparator<Snapshot> getInstance() {
return COMPARATOR;
}
private SnapshotComparator() {
}
}

View File

@ -0,0 +1,81 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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.sk89q.worldedit.world.snapshot.experimental;
import java.io.IOException;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.stream.Stream;
import static com.sk89q.worldedit.util.collection.MoreStreams.takeWhile;
/**
* Handler for querying snapshot storage.
*/
public interface SnapshotDatabase {
/**
* Get the URI scheme handled by this database.
*/
String getScheme();
/**
* Get a snapshot by name.
*
* @param name the name of the snapshot
* @return the snapshot if available
*/
Optional<Snapshot> getSnapshot(URI name) throws IOException;
/**
* Get all snapshots by world, unsorted. The stream should be
* {@linkplain Stream#close() closed}, as it may allocate filesystem or network resources.
*
* @param worldName the name of the world
* @return a stream of all snapshots for the given world in this database
*/
Stream<Snapshot> getSnapshots(String worldName) throws IOException;
default Stream<Snapshot> getSnapshotsNewestFirst(String worldName) throws IOException {
return getSnapshots(worldName).sorted(SnapshotComparator.getInstance().reversed());
}
default Stream<Snapshot> getSnapshotsOldestFirst(String worldName) throws IOException {
return getSnapshots(worldName).sorted(SnapshotComparator.getInstance());
}
default Stream<Snapshot> getSnapshotsBefore(String worldName, ZonedDateTime date) throws IOException {
return takeWhile(
// sorted from oldest -> newest, so all `before` are at the front
getSnapshotsOldestFirst(worldName),
snap -> snap.getInfo().getDateTime().isBefore(date)
);
}
default Stream<Snapshot> getSnapshotsAfter(String worldName, ZonedDateTime date) throws IOException {
return takeWhile(
// sorted from newest -> oldest, so all `after` are at the front
getSnapshotsNewestFirst(worldName),
snap -> snap.getInfo().getDateTime().isAfter(date)
);
}
}

View File

@ -0,0 +1,90 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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.sk89q.worldedit.world.snapshot.experimental;
import com.google.common.collect.ComparisonChain;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Objects;
/**
* Information about a snapshot, such as name and date.
*/
public final class SnapshotInfo implements Comparable<SnapshotInfo> {
public static SnapshotInfo create(URI name, ZonedDateTime dateTime) {
return new SnapshotInfo(name, dateTime);
}
private final URI name;
private final ZonedDateTime dateTime;
private SnapshotInfo(URI name, ZonedDateTime dateTime) {
this.name = name;
this.dateTime = dateTime;
}
public URI getName() {
return name;
}
public String getDisplayName() {
if (name.getScheme().equals("snapfs")) {
// Stored raw as the scheme specific part
return name.getSchemeSpecificPart();
}
return name.toString();
}
public ZonedDateTime getDateTime() {
return dateTime;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SnapshotInfo that = (SnapshotInfo) o;
return Objects.equals(name, that.name) &&
Objects.equals(dateTime, that.dateTime);
}
@Override
public int hashCode() {
return Objects.hash(name, dateTime);
}
@Override
public String toString() {
return "SnapshotInfo{" +
"name='" + name + '\'' +
",date=" + dateTime +
'}';
}
@Override
public int compareTo(SnapshotInfo o) {
return ComparisonChain.start()
.compare(dateTime, o.dateTime)
.compare(name, o.name)
.result();
}
}

View File

@ -0,0 +1,202 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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.sk89q.worldedit.world.snapshot.experimental;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.math.BlockVector2;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.world.DataException;
import com.sk89q.worldedit.world.chunk.Chunk;
import com.sk89q.worldedit.world.storage.ChunkStore;
import com.sk89q.worldedit.world.storage.MissingChunkException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* A snapshot restore operation.
*/
public class SnapshotRestore {
private final Map<BlockVector2, ArrayList<BlockVector3>> neededChunks = new LinkedHashMap<>();
private final Snapshot snapshot;
private final EditSession editSession;
private ArrayList<BlockVector2> missingChunks;
private ArrayList<BlockVector2> errorChunks;
private String lastErrorMessage;
/**
* Construct the snapshot restore operation.
*
* @param snapshot The {@link Snapshot} to restore from
* @param editSession The {@link EditSession} to restore to
* @param region The {@link Region} to restore to
*/
public SnapshotRestore(Snapshot snapshot, EditSession editSession, Region region) {
this.snapshot = snapshot;
this.editSession = editSession;
if (region instanceof CuboidRegion) {
findNeededCuboidChunks(region);
} else {
findNeededChunks(region);
}
}
/**
* Find needed chunks in the axis-aligned bounding box of the region.
*
* @param region The {@link Region} to iterate
*/
private void findNeededCuboidChunks(Region region) {
BlockVector3 min = region.getMinimumPoint();
BlockVector3 max = region.getMaximumPoint();
// First, we need to group points by chunk so that we only need
// to keep one chunk in memory at any given moment
for (int x = min.getBlockX(); x <= max.getBlockX(); ++x) {
for (int y = min.getBlockY(); y <= max.getBlockY(); ++y) {
for (int z = min.getBlockZ(); z <= max.getBlockZ(); ++z) {
BlockVector3 pos = BlockVector3.at(x, y, z);
checkAndAddBlock(pos);
}
}
}
}
/**
* Find needed chunks in the region.
*
* @param region The {@link Region} to iterate
*/
private void findNeededChunks(Region region) {
// First, we need to group points by chunk so that we only need
// to keep one chunk in memory at any given moment
for (BlockVector3 pos : region) {
checkAndAddBlock(pos);
}
}
private void checkAndAddBlock(BlockVector3 pos) {
if (editSession.getMask() != null && !editSession.getMask().test(pos))
return;
BlockVector2 chunkPos = ChunkStore.toChunk(pos);
// Unidentified chunk
if (!neededChunks.containsKey(chunkPos)) {
neededChunks.put(chunkPos, new ArrayList<>());
}
neededChunks.get(chunkPos).add(pos);
}
/**
* Get the number of chunks that are needed.
*
* @return a number of chunks
*/
public int getChunksAffected() {
return neededChunks.size();
}
/**
* Restores to world.
*
* @throws MaxChangedBlocksException
*/
public void restore() throws MaxChangedBlocksException {
missingChunks = new ArrayList<>();
errorChunks = new ArrayList<>();
// Now let's start restoring!
for (Map.Entry<BlockVector2, ArrayList<BlockVector3>> entry : neededChunks.entrySet()) {
BlockVector2 chunkPos = entry.getKey();
Chunk chunk;
try {
// This will need to be changed if we start officially supporting 3d snapshots.
chunk = snapshot.getChunk(chunkPos.toBlockVector3());
// Good, the chunk could be at least loaded
// Now just copy blocks!
for (BlockVector3 pos : entry.getValue()) {
try {
editSession.setBlock(pos, chunk.getBlock(pos));
} catch (DataException e) {
// this is a workaround: just ignore for now
}
}
} catch (MissingChunkException me) {
missingChunks.add(chunkPos);
} catch (IOException | DataException me) {
errorChunks.add(chunkPos);
lastErrorMessage = me.getMessage();
}
}
}
/**
* Get a list of the missing chunks. restore() must have been called
* already.
*
* @return a list of coordinates
*/
public List<BlockVector2> getMissingChunks() {
return missingChunks;
}
/**
* Get a list of the chunks that could not have been loaded for other
* reasons. restore() must have been called already.
*
* @return a list of coordinates
*/
public List<BlockVector2> getErrorChunks() {
return errorChunks;
}
/**
* Checks to see where the backup succeeded in any capacity. False will
* be returned if no chunk could be successfully loaded.
*
* @return true if there was total failure
*/
public boolean hadTotalFailure() {
return missingChunks.size() + errorChunks.size() == getChunksAffected();
}
/**
* Get the last error message.
*
* @return a message
*/
public String getLastErrorMessage() {
return lastErrorMessage;
}
}

View File

@ -0,0 +1,300 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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.sk89q.worldedit.world.snapshot.experimental.fs;
import com.google.common.collect.ImmutableList;
import com.google.common.net.UrlEscapers;
import com.sk89q.worldedit.util.function.IORunnable;
import com.sk89q.worldedit.util.io.Closer;
import com.sk89q.worldedit.util.io.file.ArchiveNioSupport;
import com.sk89q.worldedit.util.io.file.MorePaths;
import com.sk89q.worldedit.util.time.FileNameDateTimeParser;
import com.sk89q.worldedit.util.time.ModificationDateTimeParser;
import com.sk89q.worldedit.util.time.SnapshotDateTimeParser;
import com.sk89q.worldedit.world.snapshot.experimental.Snapshot;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotDatabase;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Implements a snapshot database based on a filesystem.
*/
public class FileSystemSnapshotDatabase implements SnapshotDatabase {
private static final Logger logger = LoggerFactory.getLogger(FileSystemSnapshotDatabase.class);
private static final String SCHEME = "snapfs";
private static final List<SnapshotDateTimeParser> DATE_TIME_PARSERS =
new ImmutableList.Builder<SnapshotDateTimeParser>()
.add(FileNameDateTimeParser.getInstance())
.addAll(ServiceLoader.load(SnapshotDateTimeParser.class))
.add(ModificationDateTimeParser.getInstance())
.build();
public static ZonedDateTime tryParseDate(Path path) {
return tryParseDateInternal(path)
.orElseThrow(() -> new IllegalStateException("Could not detect date of " + path));
}
private static Optional<ZonedDateTime> tryParseDateInternal(Path path) {
return DATE_TIME_PARSERS.stream()
.map(parser -> parser.detectDateTime(path))
.filter(Objects::nonNull)
.findFirst();
}
public static URI createUri(String name) {
return URI.create(SCHEME + ":" + UrlEscapers.urlFragmentEscaper().escape(name));
}
public static FileSystemSnapshotDatabase maybeCreate(
Path root,
ArchiveNioSupport archiveNioSupport
) throws IOException {
Files.createDirectories(root);
return new FileSystemSnapshotDatabase(root, archiveNioSupport);
}
private final Path root;
private final ArchiveNioSupport archiveNioSupport;
public FileSystemSnapshotDatabase(Path root, ArchiveNioSupport archiveNioSupport) {
checkArgument(Files.isDirectory(root), "Database root is not a directory");
this.root = root.toAbsolutePath();
this.archiveNioSupport = archiveNioSupport;
}
private SnapshotInfo createSnapshotInfo(Path fullPath, Path realPath) {
// Try full for parsing out of file name, real for parsing mod time.
ZonedDateTime date = tryParseDateInternal(fullPath).orElseGet(() -> tryParseDate(realPath));
return SnapshotInfo.create(createUri(fullPath.toString()), date);
}
private Snapshot createSnapshot(Path fullPath, Path realPath, @Nullable IORunnable closeCallback) {
return new FolderSnapshot(
createSnapshotInfo(fullPath, realPath), realPath, closeCallback
);
}
public Path getRoot() {
return root;
}
@Override
public String getScheme() {
return SCHEME;
}
@Override
public Optional<Snapshot> getSnapshot(URI name) throws IOException {
if (!name.getScheme().equals(SCHEME)) {
return Optional.empty();
}
// drop the / in the path to make it absolute
Path rawResolved = root.resolve(name.getSchemeSpecificPart());
// Catch trickery with paths:
Path realPath = rawResolved.normalize();
if (!realPath.startsWith(root)) {
return Optional.empty();
}
Optional<Snapshot> result = tryRegularFileSnapshot(root.relativize(realPath), realPath);
if (result.isPresent()) {
return result;
}
if (!Files.isDirectory(realPath)) {
return Optional.empty();
}
return Optional.of(createSnapshot(root.relativize(realPath), realPath, null));
}
private Optional<Snapshot> tryRegularFileSnapshot(Path fullPath, Path realPath) throws IOException {
Closer closer = Closer.create();
Path root = this.root;
Path relative = root.relativize(realPath);
Iterator<Path> iterator = null;
try {
while (true) {
if (iterator == null) {
iterator = MorePaths.iterPaths(relative).iterator();
}
if (!iterator.hasNext()) {
return Optional.empty();
}
Path relativeNext = iterator.next();
Path next = root.resolve(relativeNext);
if (!Files.isRegularFile(next)) {
// This will never be it.
continue;
}
Optional<Path> newRootOpt = archiveNioSupport.tryOpenAsDir(next);
if (newRootOpt.isPresent()) {
root = newRootOpt.get();
if (root.getFileSystem() != FileSystems.getDefault()) {
closer.register(root.getFileSystem());
}
// Switch path to path inside the archive
relative = root.resolve(relativeNext.relativize(relative).toString());
iterator = null;
// Check if it exists, if so open snapshot
if (Files.exists(relative)) {
return Optional.of(createSnapshot(fullPath, relative, closer::close));
}
// Otherwise, we may have more archives to open.
// Keep searching!
}
}
} catch (Throwable t) {
throw closer.rethrowAndClose(t);
}
}
@Override
public Stream<Snapshot> getSnapshots(String worldName) throws IOException {
/*
There are a few possible snapshot formats we accept:
- a world directory, identified by <worldName>/level.dat
- a world archive, identified by <worldName>.ext
* does not need to have level.dat inside
- a timestamped directory, identified by <stamp>, that can have
- the two world formats described above, inside the directory
- a timestamped archive, identified by <stamp>.ext, that can have
- the same as timestamped directory, but inside the archive.
- a directory with the world name, but no level.dat
- inside must be timestamped directory/archive, with the world inside that
All archives may have a root directory with the same name as the archive,
minus the extensions. Due to extension detection methods, this won't work properly
with some files, e.g. world.qux.zip/world.qux is invalid, but world.qux.zip/world isn't.
*/
return Stream.of(
listWorldEntries(Paths.get(""), root, worldName),
listTimestampedEntries(Paths.get(""), root, worldName)
).flatMap(Function.identity());
}
private Stream<Snapshot> listWorldEntries(Path fullPath, Path root, String worldName) throws IOException {
logger.debug("World check in: {}", root);
return Files.list(root)
.flatMap(candidate -> {
logger.debug("World trying: {}", candidate);
// Try world directory
String fileName = candidate.getFileName().toString();
if (isSameDirectoryName(fileName, worldName)) {
// Direct
if (Files.exists(candidate.resolve("level.dat"))) {
logger.debug("Direct!");
return Stream.of(createSnapshot(
fullPath.resolve(fileName), candidate, null
));
}
// Container for time-stamped entries
try {
return listTimestampedEntries(
fullPath.resolve(fileName), candidate, worldName
);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// Try world archive
if (Files.isRegularFile(candidate)
&& fileName.startsWith(worldName + ".")) {
logger.debug("Archive!");
try {
return tryRegularFileSnapshot(
fullPath.resolve(fileName), candidate
).map(Stream::of).orElse(null);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
logger.debug("Nothing!");
return null;
});
}
private boolean isSameDirectoryName(String fileName, String worldName) {
if (fileName.lastIndexOf('/') == fileName.length() - 1) {
fileName = fileName.substring(0, fileName.length() - 1);
}
return fileName.equalsIgnoreCase(worldName);
}
private Stream<Snapshot> listTimestampedEntries(Path fullPath, Path root, String worldName) throws IOException {
logger.debug("Timestamp check in: {}", root);
return Files.list(root)
.filter(candidate -> {
ZonedDateTime date = FileNameDateTimeParser.getInstance().detectDateTime(candidate);
return date != null;
})
.flatMap(candidate -> {
logger.debug("Timestamp trying: {}", candidate);
// Try timestamped directory
if (Files.isDirectory(candidate)) {
logger.debug("Timestamped directory");
try {
return listWorldEntries(
fullPath.resolve(candidate.getFileName().toString()), candidate, worldName
);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// Otherwise archive, get it as a directory & unpack it
try {
Optional<Path> newRoot = archiveNioSupport.tryOpenAsDir(candidate);
if (!newRoot.isPresent()) {
logger.debug("Nothing!");
return null;
}
logger.debug("Timestamped archive!");
return listWorldEntries(
fullPath.resolve(candidate.getFileName().toString()),
newRoot.get(),
worldName
);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}

View File

@ -0,0 +1,166 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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.sk89q.worldedit.world.snapshot.experimental.fs;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.worldedit.math.BlockVector2;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.util.function.IORunnable;
import com.sk89q.worldedit.world.DataException;
import com.sk89q.worldedit.world.snapshot.experimental.Snapshot;
import com.sk89q.worldedit.world.snapshot.experimental.SnapshotInfo;
import com.sk89q.worldedit.world.storage.ChunkStoreHelper;
import com.sk89q.worldedit.world.storage.LegacyChunkStore;
import com.sk89q.worldedit.world.storage.McRegionChunkStore;
import com.sk89q.worldedit.world.storage.McRegionReader;
import com.sk89q.worldedit.world.storage.MissingChunkException;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import static com.google.common.base.Preconditions.checkState;
/**
* Snapshot based on a world folder. Extracts chunks from the region folder.
*
* <p>
* Note that the Path can belong to another filesystem. This allows easy integration with
* zips due to Java's built-in zipfs support.
* </p>
*/
public class FolderSnapshot implements Snapshot {
/**
* Object used by {@code getRegionFolder(Path)} to indicate that the path does not exist.
*/
private static final Object NOT_FOUND_TOKEN = new Object();
private static Object getRegionFolder(Path folder) throws IOException {
Path regionDir = folder.resolve("region");
if (Files.exists(regionDir)) {
checkState(Files.isDirectory(regionDir), "Region folder is actually a file");
return regionDir;
}
// Might be in a DIM* folder
try (Stream<Path> paths = Files.list(folder)) {
Optional<Path> path = paths
.filter(Files::isDirectory)
.filter(p -> p.getFileName().toString().startsWith("DIM"))
.map(p -> p.resolve("region"))
.filter(Files::isDirectory)
.findFirst();
if (path.isPresent()) {
return path.get();
}
}
// Might be its own region folder, check if the appropriate files exist
try (Stream<Path> paths = Files.list(folder)) {
if (paths
.filter(Files::isRegularFile)
.anyMatch(p -> {
String fileName = p.getFileName().toString();
return fileName.startsWith("r") &&
(fileName.endsWith(".mca") || fileName.endsWith(".mcr"));
})) {
return folder;
}
}
return NOT_FOUND_TOKEN;
}
private final SnapshotInfo info;
private final Path folder;
private final AtomicReference<Object> regionFolder = new AtomicReference<>();
private final @Nullable IORunnable closeCallback;
public FolderSnapshot(SnapshotInfo info, Path folder, @Nullable IORunnable closeCallback) {
this.info = info;
// This is required to force TrueVfs to properly resolve parents.
// Kinda odd, but whatever works.
this.folder = folder.toAbsolutePath();
this.closeCallback = closeCallback;
}
public Path getFolder() {
return folder;
}
@Override
public SnapshotInfo getInfo() {
return info;
}
private Optional<Path> getRegionFolder() throws IOException {
Object regFolder = regionFolder.get();
if (regFolder == null) {
Object update = getRegionFolder(folder);
if (!regionFolder.compareAndSet(null, update)) {
// failed race, get existing value
regFolder = regionFolder.get();
} else {
regFolder = update;
}
}
return regFolder == NOT_FOUND_TOKEN ? Optional.empty() : Optional.of((Path) regFolder);
}
@Override
public CompoundTag getChunkTag(BlockVector3 position) throws DataException, IOException {
BlockVector2 pos = position.toBlockVector2();
Optional<Path> regFolder = getRegionFolder();
if (!regFolder.isPresent()) {
Path chunkFile = getFolder().resolve(LegacyChunkStore.getFilename(pos, "/"));
if (!Files.exists(chunkFile)) {
throw new MissingChunkException();
}
return ChunkStoreHelper.readCompoundTag(() ->
new GZIPInputStream(Files.newInputStream(chunkFile))
);
}
Path regionFile = regFolder.get().resolve(McRegionChunkStore.getFilename(pos));
if (!Files.exists(regionFile)) {
// Try mcr as well
regionFile = regionFile.resolveSibling(
regionFile.getFileName().toString().replace(".mca", ".mcr")
);
if (!Files.exists(regionFile)) {
throw new MissingChunkException();
}
}
try (InputStream stream = Files.newInputStream(regionFile)) {
McRegionReader regionReader = new McRegionReader(stream);
return ChunkStoreHelper.readCompoundTag(() -> regionReader.getChunkInputStream(pos));
}
}
@Override
public void close() throws IOException {
if (closeCallback != null) {
closeCallback.run();
}
}
}

View File

@ -0,0 +1,30 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program 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.
*
* This program 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/>.
*/
/**
* Experimental, in-testing, snapshot API. Do NOT rely on this API in plugin releases, as it will
* move to the existing snapshot package when testing is complete.
*
* <p>
* The existing snapshot API will be removed when this API is made official. It aims to have 100%
* compatibility with old snapshot storage, bar some odd date formats.
* </p>
*/
package com.sk89q.worldedit.world.snapshot.experimental;
// TODO Un-experimentalize when ready.

View File

@ -25,6 +25,7 @@ import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.world.DataException;
import com.sk89q.worldedit.world.World;
import com.sk89q.worldedit.world.chunk.Chunk;
import java.io.Closeable;
import java.io.IOException;

View File

@ -47,7 +47,7 @@ public class ChunkStoreHelper {
public static CompoundTag readCompoundTag(ChunkDataInputSupplier input) throws DataException, IOException {
try (InputStream stream = input.openInputStream();
NBTInputStream nbt = new NBTInputStream(stream)) {
NBTInputStream nbt = new NBTInputStream(stream)) {
Tag tag = nbt.readNamedTag().getTag();
if (!(tag instanceof CompoundTag)) {
throw new ChunkStoreException("CompoundTag expected for chunk; got "

View File

@ -23,6 +23,7 @@ import com.sk89q.jnbt.CompoundTag;
import com.sk89q.worldedit.math.BlockVector2;
import com.sk89q.worldedit.world.DataException;
import com.sk89q.worldedit.world.World;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

View File

@ -20,8 +20,6 @@
package com.sk89q.worldedit.world.storage;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.Tag;
import com.sk89q.worldedit.math.BlockVector2;
import com.sk89q.worldedit.world.DataException;
import com.sk89q.worldedit.world.World;
@ -67,19 +65,11 @@ public abstract class McRegionChunkStore extends ChunkStore {
@Override
public CompoundTag getChunkTag(BlockVector2 position, World world) throws DataException, IOException {
McRegionReader reader = getReader(position, world.getName());
return ChunkStoreHelper.readCompoundTag(() -> {
McRegionReader reader = getReader(position, world.getName());
InputStream stream = reader.getChunkInputStream(position);
Tag tag;
try (NBTInputStream nbt = new NBTInputStream(stream)) {
tag = nbt.readNamedTag().getTag();
if (!(tag instanceof CompoundTag)) {
throw new ChunkStoreException("CompoundTag expected for chunk; got " + tag.getClass().getName());
}
return (CompoundTag) tag;
}
return reader.getChunkInputStream(position);
});
}
/**