fix: improve FAWE stream history (#2844)

- reset "origin"/relative X/Z when larger than short min/max value so we do not write incorrect positions
 - history size can be larger than int max value
 - fixes #2583
This commit is contained in:
Jordan 2024-07-28 11:16:25 +02:00 committed by GitHub
parent 6052fc3128
commit d1f9d3d6d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 29 deletions

View File

@ -91,7 +91,7 @@ public class CopyPastaBrush implements Brush, ResettableTool {
newClipboard.setOrigin(position); newClipboard.setOrigin(position);
ClipboardHolder holder = new ClipboardHolder(newClipboard); ClipboardHolder holder = new ClipboardHolder(newClipboard);
session.setClipboard(holder); session.setClipboard(holder);
int blocks = builder.size(); long blocks = builder.longSize();
player.print(Caption.of("fawe.worldedit.copy.command.copy", blocks)); player.print(Caption.of("fawe.worldedit.copy.command.copy", blocks));
} else { } else {
AffineTransform transform = null; AffineTransform transform = null;

View File

@ -64,26 +64,47 @@ public class RollbackDatabase extends AsyncNotifyQueue {
public Future<Boolean> init() { public Future<Boolean> init() {
return call(() -> { return call(() -> {
try (PreparedStatement stmt = connection.prepareStatement("CREATE TABLE IF NOT EXISTS`" + this.prefix + try (PreparedStatement stmt = connection.prepareStatement("CREATE TABLE IF NOT EXISTS`" + this.prefix +
"edits` (`player` BLOB(16) NOT NULL,`id` INT NOT NULL, `time` INT NOT NULL,`x1`" + "_edits` (`player` BLOB(16) NOT NULL,`id` INT NOT NULL, `time` INT NOT NULL,`x1` " +
"INT NOT NULL,`x2` INT NOT NULL,`z1` INT NOT NULL,`z2` INT NOT NULL,`y1`" + "INT NOT NULL,`x2` INT NOT NULL,`z1` INT NOT NULL,`z2` INT NOT NULL,`y1` " +
"INT NOT NULL, `y2` INT NOT NULL, `size` INT NOT NULL, `command` VARCHAR, PRIMARY KEY (player, id))")) { "INT NOT NULL, `y2` INT NOT NULL, `size` BIGINT NOT NULL, `command` VARCHAR, PRIMARY KEY (player, id))")) {
stmt.executeUpdate(); stmt.executeUpdate();
} }
try (PreparedStatement stmt = connection.prepareStatement("ALTER TABLE`" + this.prefix + "edits` ADD COLUMN `command` VARCHAR")) { String alterTablePrefix = "ALTER TABLE`" + this.prefix + "edits` ";
try (PreparedStatement stmt =
connection.prepareStatement(alterTablePrefix + "ADD COLUMN `command` VARCHAR")) {
stmt.executeUpdate(); stmt.executeUpdate();
} catch (SQLException ignored) { } catch (SQLException ignored) {
} // Already updated } // Already updated
try (PreparedStatement stmt = connection.prepareStatement("ALTER TABLE`" + this.prefix + "edits` ADD SIZE INT DEFAULT 0 NOT NULL")) { try (PreparedStatement stmt =
connection.prepareStatement(alterTablePrefix + "ADD COLUMN `size` BIGINT DEFAULT 0 NOT NULL")) {
stmt.executeUpdate(); stmt.executeUpdate();
} catch (SQLException ignored) { } catch (SQLException ignored) {
} // Already updated } // Already updated
boolean migrated = false;
try (PreparedStatement stmt =
connection.prepareStatement("INSERT INTO `" + this.prefix + "_edits` " +
"(player, id, time, x1, x2, z1, z2, y1, y2, size, command) " +
"SELECT player, id, time, x1, x2, z1, z2, y1, y2, size, command " +
"FROM `" + this.prefix + "edits`")) {
stmt.executeUpdate();
migrated = true;
} catch (SQLException ignored) {
} // Already updated
if (migrated) {
try (PreparedStatement stmt = connection.prepareStatement("DROP TABLE `" + this.prefix + "edits`")) {
stmt.executeUpdate();
}
}
return true; return true;
}); });
} }
public Future<Integer> delete(UUID uuid, int id) { public Future<Integer> delete(UUID uuid, int id) {
return call(() -> { return call(() -> {
try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "edits` WHERE `player`=? AND `id`=?")) { try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "_edits` WHERE `player`=? " +
"AND `id`=?")) {
stmt.setBytes(1, toBytes(uuid)); stmt.setBytes(1, toBytes(uuid));
stmt.setInt(2, id); stmt.setInt(2, id);
return stmt.executeUpdate(); return stmt.executeUpdate();
@ -94,7 +115,7 @@ public class RollbackDatabase extends AsyncNotifyQueue {
public Future<RollbackOptimizedHistory> getEdit(@Nonnull UUID uuid, int id) { public Future<RollbackOptimizedHistory> getEdit(@Nonnull UUID uuid, int id) {
return call(() -> { return call(() -> {
try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM`" + this.prefix + try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM`" + this.prefix +
"edits` WHERE `player`=? AND `id`=?")) { "_edits` WHERE `player`=? AND `id`=?")) {
stmt.setBytes(1, toBytes(uuid)); stmt.setBytes(1, toBytes(uuid));
stmt.setInt(2, id); stmt.setInt(2, id);
ResultSet result = stmt.executeQuery(); ResultSet result = stmt.executeQuery();
@ -119,7 +140,7 @@ public class RollbackDatabase extends AsyncNotifyQueue {
CuboidRegion region = new CuboidRegion(BlockVector3.at(x1, y1, z1), BlockVector3.at(x2, y2, z2)); CuboidRegion region = new CuboidRegion(BlockVector3.at(x1, y1, z1), BlockVector3.at(x2, y2, z2));
long time = result.getInt("time") * 1000L; long time = result.getInt("time") * 1000L;
long size = result.getInt("size"); long size = result.getLong("size");
String command = result.getString("command"); String command = result.getString("command");
@ -135,7 +156,7 @@ public class RollbackDatabase extends AsyncNotifyQueue {
long now = System.currentTimeMillis() / 1000; long now = System.currentTimeMillis() / 1000;
final int then = (int) (now - diff); final int then = (int) (now - diff);
return call(() -> { return call(() -> {
try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "edits` WHERE `time`<?")) { try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "_edits` WHERE `time`<?")) {
stmt.setInt(1, then); stmt.setInt(1, then);
return stmt.executeUpdate(); return stmt.executeUpdate();
} }
@ -160,7 +181,7 @@ public class RollbackDatabase extends AsyncNotifyQueue {
try { try {
int count = 0; int count = 0;
String stmtStr = """ String stmtStr = """
SELECT * FROM `%sedits` SELECT * FROM `%s_edits`
WHERE `time` > ? WHERE `time` > ?
AND `x2` >= ? AND `x2` >= ?
AND `x1` <= ? AND `x1` <= ?
@ -202,7 +223,8 @@ public class RollbackDatabase extends AsyncNotifyQueue {
} }
if (delete && uuid != null) { if (delete && uuid != null) {
try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix +
"edits` WHERE `player`=? AND `time`>? AND `x2`>=? AND `x1`<=? AND `y2`>=? AND `y1`<=? AND `z2`>=? AND `z1`<=?")) { "_edits` WHERE `player`=? AND `time`>? AND `x2`>=? AND `x1`<=? AND `y2`>=? AND `y1`<=? AND `z2`>=? " +
"AND `z1`<=?")) {
byte[] uuidBytes = ByteBuffer byte[] uuidBytes = ByteBuffer
.allocate(16) .allocate(16)
.putLong(uuid.getMostSignificantBits()) .putLong(uuid.getMostSignificantBits())
@ -249,7 +271,7 @@ public class RollbackDatabase extends AsyncNotifyQueue {
RollbackOptimizedHistory[] copy = IntStream.range(0, size) RollbackOptimizedHistory[] copy = IntStream.range(0, size)
.mapToObj(i -> historyChanges.poll()).toArray(RollbackOptimizedHistory[]::new); .mapToObj(i -> historyChanges.poll()).toArray(RollbackOptimizedHistory[]::new);
try (PreparedStatement stmt = connection.prepareStatement("INSERT OR REPLACE INTO`" + this.prefix + "edits`" + try (PreparedStatement stmt = connection.prepareStatement("INSERT OR REPLACE INTO`" + this.prefix + "_edits`" +
" (`player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)")) { " (`player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)")) {
// `player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)" // `player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
for (RollbackOptimizedHistory change : copy) { for (RollbackOptimizedHistory change : copy) {
@ -270,7 +292,7 @@ public class RollbackDatabase extends AsyncNotifyQueue {
stmt.setInt(8, pos1.y() - 128); stmt.setInt(8, pos1.y() - 128);
stmt.setInt(9, pos2.y() - 128); stmt.setInt(9, pos2.y() - 128);
stmt.setString(10, change.getCommand()); stmt.setString(10, change.getCommand());
stmt.setInt(11, change.size()); stmt.setLong(11, change.longSize());
stmt.executeUpdate(); stmt.executeUpdate();
stmt.clearParameters(); stmt.clearParameters();
} }

View File

@ -306,7 +306,7 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor {
} }
public boolean isEmpty() { public boolean isEmpty() {
return queue.isEmpty() && workerSemaphore.availablePermits() == 1 && size() == 0; return queue.isEmpty() && workerSemaphore.availablePermits() == 1 && longSize() == 0;
} }
public void add(BlockVector3 loc, BaseBlock from, BaseBlock to) { public void add(BlockVector3 loc, BaseBlock from, BaseBlock to) {

View File

@ -176,6 +176,11 @@ public class AbstractDelegateChangeSet extends AbstractChangeSet {
return parent.size(); return parent.size();
} }
@Override
public long longSize() {
return parent.longSize();
}
@Override @Override
public void delete() { public void delete() {
parent.delete(); parent.delete();

View File

@ -25,6 +25,7 @@ import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
@ -35,11 +36,18 @@ import java.util.NoSuchElementException;
public abstract class FaweStreamChangeSet extends AbstractChangeSet { public abstract class FaweStreamChangeSet extends AbstractChangeSet {
public static final int HEADER_SIZE = 9; public static final int HEADER_SIZE = 9;
private static final int version = 1; private static final int VERSION = 2;
// equivalent to Short#MIN_VALUE three times stored with [(x) & 0xff, ((rx) >> 8) & 0xff]
private static final byte[] MAGIC_NEW_RELATIVE = new byte[]{0, (byte) 128, 0, (byte) 128, 0, (byte) 128};
private int mode; private int mode;
private final int compression; private final int compression;
private final int minY; private final int minY;
protected long blockSize;
private int originX;
private int originZ;
private int version;
protected FaweStreamIdDelegate idDel; protected FaweStreamIdDelegate idDel;
protected FaweStreamPositionDelegate posDel; protected FaweStreamPositionDelegate posDel;
@ -192,6 +200,20 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet {
int rx = -lx + (lx = x); int rx = -lx + (lx = x);
int ry = -ly + (ly = y); int ry = -ly + (ly = y);
int rz = -lz + (lz = z); int rz = -lz + (lz = z);
// Use LE/GE to ensure we don't accidentally write MAGIC_NEW_RELATIVE
if (rx >= Short.MAX_VALUE || rz >= Short.MAX_VALUE || rx <= Short.MIN_VALUE || rz <= Short.MIN_VALUE) {
stream.write(MAGIC_NEW_RELATIVE);
stream.write((byte) (x >> 24));
stream.write((byte) (x >> 16));
stream.write((byte) (x >> 8));
stream.write((byte) (x));
stream.write((byte) (z >> 24));
stream.write((byte) (z >> 16));
stream.write((byte) (z >> 8));
stream.write((byte) (z));
rx = 0;
rz = 0;
}
stream.write((rx) & 0xff); stream.write((rx) & 0xff);
stream.write(((rx) >> 8) & 0xff); stream.write(((rx) >> 8) & 0xff);
stream.write((rz) & 0xff); stream.write((rz) & 0xff);
@ -203,6 +225,12 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet {
@Override @Override
public int readX(FaweInputStream is) throws IOException { public int readX(FaweInputStream is) throws IOException {
is.readFully(buffer); is.readFully(buffer);
// Don't break reading version 1 history (just in case)
if (version == 2 && Arrays.equals(buffer, MAGIC_NEW_RELATIVE)) {
lx = ((is.read() << 24) + (is.read() << 16) + (is.read() << 8) + is.read());
lz = ((is.read() << 24) + (is.read() << 16) + (is.read() << 8) + is.read());
is.readFully(buffer);
}
return lx = lx + ((buffer[0] & 0xFF) | (buffer[1] << 8)); return lx = lx + ((buffer[0] & 0xFF) | (buffer[1] << 8));
} }
@ -222,7 +250,7 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet {
public void writeHeader(OutputStream os, int x, int y, int z) throws IOException { public void writeHeader(OutputStream os, int x, int y, int z) throws IOException {
os.write(mode); os.write(mode);
// Allows for version detection of history in case of changes to format. // Allows for version detection of history in case of changes to format.
os.write(version); os.write(VERSION);
setOrigin(x, z); setOrigin(x, z);
os.write((byte) (x >> 24)); os.write((byte) (x >> 24));
os.write((byte) (x >> 16)); os.write((byte) (x >> 16));
@ -238,8 +266,8 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet {
public void readHeader(InputStream is) throws IOException { public void readHeader(InputStream is) throws IOException {
// skip mode // skip mode
int mode = is.read(); int mode = is.read();
int version = is.read(); version = is.read();
if (version != FaweStreamChangeSet.version) { if (version != 1 && version != VERSION) { // version 1 is fine
throw new UnsupportedOperationException(String.format("Version %s history not supported!", version)); throw new UnsupportedOperationException(String.format("Version %s history not supported!", version));
} }
// origin // origin
@ -266,12 +294,17 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet {
} }
@Override @Override
public int size() { public long longSize() {
// Flush so we can accurately get the size // Flush so we can accurately get the size
flush(); flush();
return blockSize; return blockSize;
} }
@Override
public int size() {
return (int) longSize();
}
public abstract int getCompressedSize(); public abstract int getCompressedSize();
public abstract long getSizeInMemory(); public abstract long getSizeInMemory();
@ -304,11 +337,6 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet {
public abstract NBTInputStream getTileRemoveIS() throws IOException; public abstract NBTInputStream getTileRemoveIS() throws IOException;
protected int blockSize;
private int originX;
private int originZ;
public void setOrigin(int x, int z) { public void setOrigin(int x, int z) {
originX = x; originX = x;
originZ = z; originZ = z;

View File

@ -217,7 +217,7 @@ public class MainUtil {
// } else if (changeSet instanceof CPUOptimizedChangeSet) { // } else if (changeSet instanceof CPUOptimizedChangeSet) {
// return changeSet.size() + 32; // return changeSet.size() + 32;
} else if (changeSet != null) { } else if (changeSet != null) {
return changeSet.size() * 128L; return changeSet.longSize() * 128; // Approx
} else { } else {
return 0; return 0;
} }

View File

@ -500,7 +500,7 @@ public class LocalSession implements TextureHolder {
if (Settings.settings().HISTORY.USE_DISK) { if (Settings.settings().HISTORY.USE_DISK) {
LocalSession.MAX_HISTORY_SIZE = Integer.MAX_VALUE; LocalSession.MAX_HISTORY_SIZE = Integer.MAX_VALUE;
} }
if (changeSet.size() == 0) { if (changeSet.longSize() == 0) {
return; return;
} }
loadSessionHistoryFromDisk(player.getUniqueId(), world); loadSessionHistoryFromDisk(player.getUniqueId(), world);

View File

@ -251,7 +251,7 @@ public class HistorySubCommands {
long seconds = (System.currentTimeMillis() - edit.getBDFile().lastModified()) / 1000; long seconds = (System.currentTimeMillis() - edit.getBDFile().lastModified()) / 1000;
String timeStr = MainUtil.secToTime(seconds); String timeStr = MainUtil.secToTime(seconds);
int size = edit.size(); long size = edit.longSize();
boolean biomes = edit.getBioFile().exists(); boolean biomes = edit.getBioFile().exists();
boolean createdEnts = edit.getEnttFile().exists(); boolean createdEnts = edit.getEnttFile().exists();
boolean removedEnts = edit.getEntfFile().exists(); boolean removedEnts = edit.getEntfFile().exists();
@ -335,7 +335,7 @@ public class HistorySubCommands {
long seconds = (System.currentTimeMillis() - rollback.getBDFile().lastModified()) / 1000; long seconds = (System.currentTimeMillis() - rollback.getBDFile().lastModified()) / 1000;
String timeStr = MainUtil.secToTime(seconds); String timeStr = MainUtil.secToTime(seconds);
int size = edit.size(); long size = edit.longSize();
TranslatableComponent elem = Caption.of( TranslatableComponent elem = Caption.of(
"fawe.worldedit.history.find.element", "fawe.worldedit.history.find.element",

View File

@ -81,11 +81,24 @@ public interface ChangeSet extends Closeable {
* Get the number of stored changes. * Get the number of stored changes.
* *
* @return the change count * @return the change count
* @deprecated History could be larger than int max value so FAWE prefers {@link ChangeSet#longSize()}
*/ */
@Deprecated(since = "TODO")
int size(); int size();
//FAWE start //FAWE start
/**
* Get the number of stored changes.
* History could be larger than int max value so FAWE prefers this method.
*
* @return the change count
* @since TODO
*/
default long longSize() {
return size();
}
/** /**
* Close the changeset. * Close the changeset.
*/ */