Skip to content

Commit

Permalink
Move chunk deletion logic into a new class VanillaWorldState
Browse files Browse the repository at this point in the history
instead of keeping it all in the state tracker, which doesn't really
make much sense
  • Loading branch information
NotStirred committed Jul 25, 2022
1 parent 309ec74 commit 54f1ffb
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 183 deletions.
22 changes: 11 additions & 11 deletions src/main/java/io/github/notstirred/chunkyeditor/Editor.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.notstirred.chunkyeditor;

import io.github.notstirred.chunkyeditor.minecraft.WorldLock;
import io.github.notstirred.chunkyeditor.state.vanilla.VanillaStateTracker;
import io.github.notstirred.chunkyeditor.ui.EditorTab;
import io.github.notstirred.chunkyeditor.state.vanilla.VanillaWorldState;
import se.llbit.chunky.Plugin;
import se.llbit.chunky.main.Chunky;
import se.llbit.chunky.main.ChunkyOptions;
Expand All @@ -27,7 +27,7 @@ public class Editor implements Plugin {
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1));

@Nullable
private VanillaStateTracker stateTracker = null;
private VanillaWorldState worldState = null;

@Nullable
private WorldMapLoader mapLoader;
Expand Down Expand Up @@ -67,18 +67,18 @@ public static void main(String[] args) {
*/
public void worldLoaded(World world, Boolean isSameWorld) {
if(!isSameWorld) {
this.stateTracker = null;
this.worldState = null;
}
}

/**
* Create a state tracker for the specified world
* Create a world state for the specified world
* Will ask the user for confirmation if the world has been accessed recently
*
* @return The state tracker for the world, or null if the user cancelled, or null if error
* @return The world state for the world, or null if the user cancelled, or null if error
*/
@Nullable
private static VanillaStateTracker createStateTracker(@NotNull World world) {
private static VanillaWorldState createWorldState(@NotNull World world) {
try {
File worldDirectory = world.getWorldDirectory();
if (worldDirectory == null) {
Expand All @@ -87,7 +87,7 @@ private static VanillaStateTracker createStateTracker(@NotNull World world) {

WorldLock worldLock = WorldLock.of(worldDirectory.toPath());
if (worldLock.tryLock()) {
return new VanillaStateTracker(world, worldLock);
return new VanillaWorldState(world, worldLock);
} else {
return null;
}
Expand All @@ -98,13 +98,13 @@ private static VanillaStateTracker createStateTracker(@NotNull World world) {
}

@Nullable
public VanillaStateTracker getStateTracker() {
if (this.mapLoader != null && stateTracker == null) {
public VanillaWorldState getWorldState() {
if (this.mapLoader != null && this.worldState == null) {
World world = this.mapLoader.getWorld();
this.stateTracker = createStateTracker(world);
this.worldState = createWorldState(world);
}

return stateTracker;
return this.worldState;
}

public void setMapLoader(@NotNull WorldMapLoader mapLoader) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;

import static io.github.notstirred.chunkyeditor.state.vanilla.VanillaWorldState.HEADER_SIZE_BYTES;

/**
* Externally modified region state, such as minecraft writing to the region file
*/
Expand Down Expand Up @@ -43,12 +44,12 @@ public boolean isInternal() {
public boolean headerMatches(State<VanillaRegionPos> other) {
if (other.isInternal()) {
InternalState internal = (InternalState) other;
return Arrays.equals(this.state, 0, VanillaStateTracker.HEADER_SIZE_BYTES,
internal.state, 0, VanillaStateTracker.HEADER_SIZE_BYTES);
return Arrays.equals(this.state, 0, HEADER_SIZE_BYTES,
internal.state, 0, HEADER_SIZE_BYTES);
} else {
ExternalState external = (ExternalState) other;
return Arrays.equals(this.state, 0, VanillaStateTracker.HEADER_SIZE_BYTES,
external.state, 0, VanillaStateTracker.HEADER_SIZE_BYTES);
return Arrays.equals(this.state, 0, HEADER_SIZE_BYTES,
external.state, 0, HEADER_SIZE_BYTES);
}
}

Expand All @@ -68,8 +69,8 @@ public boolean dataMatches(State<VanillaRegionPos> other) {
return false;
}
ExternalState external = ((ExternalState) other);
return Arrays.equals(this.state, VanillaStateTracker.HEADER_SIZE_BYTES, this.state.length,
external.state, VanillaStateTracker.HEADER_SIZE_BYTES, external.state.length);
return Arrays.equals(this.state, HEADER_SIZE_BYTES, this.state.length,
external.state, HEADER_SIZE_BYTES, external.state.length);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.nio.file.Path;
import java.util.Arrays;

import static io.github.notstirred.chunkyeditor.state.vanilla.VanillaWorldState.HEADER_SIZE_BYTES;

public class InternalState implements State<VanillaRegionPos> {
private final VanillaRegionPos pos;
/** The entire header for this state */
Expand Down Expand Up @@ -42,8 +44,8 @@ public boolean headerMatches(State<VanillaRegionPos> other) {
return Arrays.equals(this.state, internal.state);
} else {
ExternalState external = (ExternalState) other;
return Arrays.equals(this.state, 0, VanillaStateTracker.HEADER_SIZE_BYTES,
external.state, 0, VanillaStateTracker.HEADER_SIZE_BYTES);
return Arrays.equals(this.state, 0, HEADER_SIZE_BYTES,
external.state, 0, HEADER_SIZE_BYTES);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,30 @@
package io.github.notstirred.chunkyeditor.state.vanilla;

import io.github.notstirred.chunkyeditor.Accessor;
import io.github.notstirred.chunkyeditor.VanillaRegionPos;
import io.github.notstirred.chunkyeditor.minecraft.WorldLock;
import io.github.notstirred.chunkyeditor.state.State;
import javafx.application.Platform;
import se.llbit.chunky.world.Chunk;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.EmptyChunk;
import se.llbit.chunky.world.World;
import se.llbit.chunky.world.region.MCRegion;
import se.llbit.chunky.world.region.Region;
import se.llbit.log.Log;
import se.llbit.util.annotation.Nullable;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;

import static io.github.notstirred.chunkyeditor.state.vanilla.VanillaWorldState.HEADER_SIZE_BYTES;

/**
* Before any changes are made to the world, it should be checked against the current state to verify nothing has changed
* If there are changes
*/
public class VanillaStateTracker {
private static final int NO_STATE = -1;
protected static final int HEADER_SIZE_BYTES = 4096;

private final Path regionDirectory;

private final List<Map<VanillaRegionPos, State<VanillaRegionPos>>> states = new ArrayList<>();
private int currentStateIdx = NO_STATE;

private final World world;
private final WorldLock worldLock;

public VanillaStateTracker(World world, WorldLock worldLock) throws FileNotFoundException {
this.regionDirectory = world.getWorldDirectory().toPath().resolve("region");
this.world = world;
this.worldLock = worldLock;
public VanillaStateTracker(Path regionDirectory) {
this.regionDirectory = regionDirectory;
}

private InternalState internalStateForRegion(VanillaRegionPos regionPos) throws IOException {
Expand Down Expand Up @@ -174,6 +158,32 @@ public boolean snapshotState(List<VanillaRegionPos> regionPositions) throws IOEx
return true;
}

public boolean hasPreviousState() {
return this.currentStateIdx > 0;
}

public Map<VanillaRegionPos, State<VanillaRegionPos>> previousState() {
if (this.currentStateIdx == 0) {
throw new ArrayIndexOutOfBoundsException("Tried to get previous state when none exists");
}

currentStateIdx--;
return this.states.get(currentStateIdx);
}

public boolean hasNextState() {
return this.currentStateIdx + 1 < this.states.size();
}

public Map<VanillaRegionPos, State<VanillaRegionPos>> nextState() {
if (this.currentStateIdx + 1 >= this.states.size()) {
throw new ArrayIndexOutOfBoundsException("Tried to get next state when none exists");
}

currentStateIdx++;
return this.states.get(currentStateIdx);
}

/**
* Remove all states after the current one
*/
Expand All @@ -194,101 +204,6 @@ public void removeAllStates() {
this.currentStateIdx = NO_STATE;
}

public CompletableFuture<Boolean> deleteChunks(Executor taskExecutor, Map<VanillaRegionPos, List<ChunkPosition>> regionSelection) {
if (!worldLock.tryLock()) {
return CompletableFuture.completedFuture(false);
}

CompletableFuture<Boolean> deletionFuture = CompletableFuture.supplyAsync(() -> {
regionSelection.forEach((regionPos, chunkPositions) -> {
File regionFile = this.regionDirectory.resolve(regionPos.fileName()).toFile();

try (RandomAccessFile file = new RandomAccessFile(regionFile, "rw")) {
long length = file.length();
if (length < 2 * HEADER_SIZE_BYTES) {
Log.warn("Missing header in region file, despite trying to delete chunks from it?!\nThis is really bad");
return;
}

for (ChunkPosition chunkPos : chunkPositions) {
int x = chunkPos.x & 31;
int z = chunkPos.z & 31;
int index = x + z * 32;

file.seek(4 * index);
file.writeInt(0);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
return true;
}, taskExecutor);

deletionFuture.whenCompleteAsync((result, throwable) -> {
if (result == null || !result) { // execution or lock failure, no need to update
return;
}
regionSelection.forEach((regionPos, chunkPositions) -> {
Region region = world.getRegion(ChunkPosition.get(regionPos.x, regionPos.z));
for (ChunkPosition chunkPos : chunkPositions) {
Chunk chunk = world.getChunk(chunkPos);
if (!chunk.isEmpty()) {
chunk.reset();
Accessor.invoke_MCRegion$setChunk((MCRegion) region, chunkPos, EmptyChunk.INSTANCE);
world.chunkDeleted(chunkPos);
}
}
});
}, Platform::runLater);

return deletionFuture;
}

public CompletableFuture<Boolean> undo() {
if (this.currentStateIdx <= 0) {
return CompletableFuture.completedFuture(false);
}

if (!worldLock.tryLock())
return CompletableFuture.completedFuture(false);

currentStateIdx--; // we decrement first so that if there are errors the user can cancel and redo
Map<VanillaRegionPos, State<VanillaRegionPos>> previousState = this.states.get(currentStateIdx);

List<VanillaRegionPos> writtenRegions = new ArrayList<>();
CompletableFuture<Boolean> undoFuture = CompletableFuture.supplyAsync(() -> {
previousState.forEach((regionPos, state) -> {
try {
//TODO: only write to regions modified since the snapshot was taken
state.writeState(this.regionDirectory);
writtenRegions.add(state.position());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return true;
});

undoFuture.whenCompleteAsync((success, throwable) -> {
if (!success) {
return;
}
writtenRegions.forEach(regionPos -> {
Region region = world.getRegion(ChunkPosition.get(regionPos.x, regionPos.z));
region.parse(0, 0);
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
ChunkPosition chunkPos = ChunkPosition.get(x, z);
world.chunkUpdated(chunkPos);
// TODO: should I MCRegion#setChunk here to make refreshing the map view faster?
}
}
});
}, Platform::runLater);
return undoFuture;
}

private static class StateGroup {
Map<VanillaRegionPos, State<VanillaRegionPos>> state = new HashMap<>();
boolean hasExternal = false;
Expand Down
Loading

0 comments on commit 54f1ffb

Please # to comment.