mirror of
https://github.com/boy0001/FastAsyncWorldedit.git
synced 2024-12-01 07:03:52 +01:00
API and brush improvements.
This commit is contained in:
parent
70362d348f
commit
14dd048662
@ -23,11 +23,14 @@ import com.boydti.fawe.object.FaweCommand;
|
||||
import com.boydti.fawe.object.FawePlayer;
|
||||
import com.boydti.fawe.regions.FaweMaskManager;
|
||||
import com.boydti.fawe.util.FaweQueue;
|
||||
import com.boydti.fawe.util.ReflectionUtils;
|
||||
import com.boydti.fawe.util.StringMan;
|
||||
import com.boydti.fawe.util.TaskManager;
|
||||
import com.sk89q.worldedit.EditSession;
|
||||
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
@ -64,6 +67,14 @@ public class FaweBukkit extends JavaPlugin implements IFawe, Listener {
|
||||
try {
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
Fawe.set(this);
|
||||
if (Bukkit.getVersion().contains("git-Spigot") && FaweAPI.checkVersion(this.getVersion(), 1, 7, 10)) {
|
||||
debug("====== USE PAPER SPIGOT ======");
|
||||
debug("DOWNLOAD: https://tcpr.ca/downloads/paperspigot");
|
||||
debug("GUIDE: https://www.spigotmc.org/threads/21726/");
|
||||
debug(" - This is only a recommendation");
|
||||
debug("==============================");
|
||||
}
|
||||
|
||||
} catch (final Throwable e) {
|
||||
e.printStackTrace();
|
||||
this.getServer().shutdown();
|
||||
@ -204,6 +215,19 @@ public class FaweBukkit extends JavaPlugin implements IFawe, Listener {
|
||||
return new BukkitQueue_1_8(world);
|
||||
} catch (Throwable ignore) {}
|
||||
if (hasNMS) {
|
||||
try {
|
||||
ReflectionUtils.init();
|
||||
Field fieldDirtyCount = ReflectionUtils.getRefClass("{nms}.PlayerChunk").getField("dirtyCount").getRealField();
|
||||
fieldDirtyCount.setAccessible(true);
|
||||
int mod = fieldDirtyCount.getModifiers();
|
||||
if ((mod & Modifier.VOLATILE) == 0) {
|
||||
Field modifiersField = Field.class.getDeclaredField("modifiers");
|
||||
modifiersField.setAccessible(true);
|
||||
modifiersField.setInt(fieldDirtyCount, mod + Modifier.VOLATILE);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
debug("====== NO NMS BLOCK PLACER FOUND ======");
|
||||
debug("FAWE couldn't find a fast block placer");
|
||||
debug("Bukkit version: " + Bukkit.getVersion());
|
||||
|
@ -6,6 +6,7 @@ import com.boydti.fawe.bukkit.v1_8.BukkitChunk_1_8;
|
||||
import com.boydti.fawe.config.Settings;
|
||||
import com.boydti.fawe.object.FaweChunk;
|
||||
import com.boydti.fawe.object.IntegerPair;
|
||||
import com.boydti.fawe.object.RunnableVal;
|
||||
import com.boydti.fawe.object.exception.FaweException;
|
||||
import com.boydti.fawe.util.TaskManager;
|
||||
import com.sk89q.worldedit.LocalWorld;
|
||||
@ -14,7 +15,6 @@ import com.sk89q.worldedit.bukkit.BukkitUtil;
|
||||
import com.sk89q.worldedit.world.biome.BaseBiome;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
@ -26,27 +26,9 @@ import org.bukkit.plugin.Plugin;
|
||||
import org.spigotmc.AsyncCatcher;
|
||||
|
||||
public class BukkitQueue_All extends BukkitQueue_0 {
|
||||
public final LinkedBlockingDeque<IntegerPair> loadQueue = new LinkedBlockingDeque<>();
|
||||
|
||||
public BukkitQueue_All(final String world) {
|
||||
super(world);
|
||||
TaskManager.IMP.repeat(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (loadQueue) {
|
||||
while (loadQueue.size() > 0) {
|
||||
IntegerPair loc = loadQueue.poll();
|
||||
if (bukkitWorld == null) {
|
||||
bukkitWorld = Bukkit.getServer().getWorld(world);
|
||||
}
|
||||
if (!bukkitWorld.isChunkLoaded(loc.x, loc.z)) {
|
||||
bukkitWorld.loadChunk(loc.x, loc.z, true);
|
||||
}
|
||||
}
|
||||
loadQueue.notifyAll();
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
if (getClass() == BukkitQueue_All.class) {
|
||||
TaskManager.IMP.task(new Runnable() {
|
||||
@Override
|
||||
@ -213,10 +195,6 @@ public class BukkitQueue_All extends BukkitQueue_0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void loadChunk(IntegerPair chunk) {
|
||||
loadQueue.add(chunk);
|
||||
}
|
||||
|
||||
public int lastChunkX = Integer.MIN_VALUE;
|
||||
public int lastChunkZ = Integer.MIN_VALUE;
|
||||
public int lastChunkY = Integer.MIN_VALUE;
|
||||
@ -241,6 +219,15 @@ public class BukkitQueue_All extends BukkitQueue_0 {
|
||||
return combined;
|
||||
}
|
||||
|
||||
private final RunnableVal<IntegerPair> loadChunk = new RunnableVal<IntegerPair>() {
|
||||
@Override
|
||||
public void run(IntegerPair coord) {
|
||||
bukkitWorld.loadChunk(coord.x, coord.z, true);
|
||||
}
|
||||
};
|
||||
|
||||
long average = 0;
|
||||
|
||||
@Override
|
||||
public int getCombinedId4Data(int x, int y, int z) throws FaweException.FaweChunkLoadException {
|
||||
if (y < 0 || y > 255) {
|
||||
@ -256,24 +243,26 @@ public class BukkitQueue_All extends BukkitQueue_0 {
|
||||
lastChunkX = cx;
|
||||
lastChunkZ = cz;
|
||||
if (!bukkitWorld.isChunkLoaded(cx, cz)) {
|
||||
long start = System.currentTimeMillis();
|
||||
boolean sync = Thread.currentThread() == Fawe.get().getMainThread();
|
||||
if (sync) {
|
||||
bukkitWorld.loadChunk(cx, cz, true);
|
||||
} else if (Settings.CHUNK_WAIT > 0) {
|
||||
synchronized (loadQueue) {
|
||||
loadQueue.add(new IntegerPair(cx, cz));
|
||||
try {
|
||||
loadQueue.wait(Settings.CHUNK_WAIT);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
loadChunk.value = new IntegerPair(cx, cz);
|
||||
TaskManager.IMP.sync(loadChunk, Settings.CHUNK_WAIT);
|
||||
if (!bukkitWorld.isChunkLoaded(cx, cz)) {
|
||||
throw new FaweException.FaweChunkLoadException();
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
long diff = System.currentTimeMillis() - start;
|
||||
if (average == 0) {
|
||||
average = diff;
|
||||
} else {
|
||||
average = ((average * 15) + diff) / 16;
|
||||
}
|
||||
System.out.println(average);
|
||||
}
|
||||
lastChunk = getCachedChunk(cx, cz);
|
||||
lastSection = getCachedSection(lastChunk, cy);
|
||||
|
@ -24,6 +24,7 @@ import com.sk89q.worldedit.WorldEdit;
|
||||
import com.sk89q.worldedit.command.SchematicCommands;
|
||||
import com.sk89q.worldedit.command.ScriptingCommands;
|
||||
import com.sk89q.worldedit.extension.platform.CommandManager;
|
||||
import com.sk89q.worldedit.extension.platform.PlatformManager;
|
||||
import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
|
||||
import com.sk89q.worldedit.extent.transform.BlockTransformExtent;
|
||||
import com.sk89q.worldedit.function.operation.Operations;
|
||||
@ -240,6 +241,7 @@ public class Fawe {
|
||||
Vector.inject();
|
||||
try {
|
||||
CommandManager.inject();
|
||||
PlatformManager.inject();
|
||||
} catch (Throwable e) {
|
||||
debug("====== UPDATE WORLDEDIT TO 6.1.1 ======");
|
||||
e.printStackTrace();
|
||||
@ -255,16 +257,18 @@ public class Fawe {
|
||||
}
|
||||
try {
|
||||
Native.load();
|
||||
String arch = System.getenv("PROCESSOR_ARCHITECTURE");
|
||||
String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432");
|
||||
boolean x86OS = arch.endsWith("64") || wow64Arch != null && wow64Arch.endsWith("64") ? false : true;
|
||||
boolean x86JVM = System.getProperty("sun.arch.data.model").equals("32");
|
||||
if (x86OS != x86JVM) {
|
||||
debug("====== UPGRADE TO 64-BIT JAVA ======");
|
||||
debug("You are running 32-bit Java on a 64-bit machine");
|
||||
debug(" - This is a recommendation");
|
||||
debug("====================================");
|
||||
}
|
||||
try {
|
||||
String arch = System.getenv("PROCESSOR_ARCHITECTURE");
|
||||
String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432");
|
||||
boolean x86OS = arch.endsWith("64") || wow64Arch != null && wow64Arch.endsWith("64") ? false : true;
|
||||
boolean x86JVM = System.getProperty("sun.arch.data.model").equals("32");
|
||||
if (x86OS != x86JVM) {
|
||||
debug("====== UPGRADE TO 64-BIT JAVA ======");
|
||||
debug("You are running 32-bit Java on a 64-bit machine");
|
||||
debug(" - This is only a recommendation");
|
||||
debug("====================================");
|
||||
}
|
||||
} catch (Throwable ignore) {}
|
||||
} catch (Throwable e) {
|
||||
debug("====== LZ4 COMPRESSION BINDING NOT FOUND ======");
|
||||
e.printStackTrace();
|
||||
@ -277,7 +281,7 @@ public class Fawe {
|
||||
if (getJavaVersion() < 1.8) {
|
||||
debug("====== UPGRADE TO JAVA 8 ======");
|
||||
debug("You are running " + System.getProperty("java.version"));
|
||||
debug(" - This is a recommendation");
|
||||
debug(" - This is only a recommendation");
|
||||
debug("====================================");
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,27 @@
|
||||
package com.boydti.fawe;
|
||||
|
||||
import com.boydti.fawe.config.BBC;
|
||||
import com.boydti.fawe.config.Settings;
|
||||
import com.boydti.fawe.object.FaweLocation;
|
||||
import com.boydti.fawe.object.FawePlayer;
|
||||
import com.boydti.fawe.object.RegionWrapper;
|
||||
import com.boydti.fawe.object.changeset.DiskStorageHistory;
|
||||
import com.boydti.fawe.regions.FaweMaskManager;
|
||||
import com.boydti.fawe.util.FaweQueue;
|
||||
import com.boydti.fawe.util.MemUtil;
|
||||
import com.boydti.fawe.util.SetQueue;
|
||||
import com.boydti.fawe.util.TaskManager;
|
||||
import com.boydti.fawe.util.WEManager;
|
||||
import com.intellectualcrafters.plot.object.PseudoRandom;
|
||||
import com.sk89q.jnbt.ByteArrayTag;
|
||||
import com.sk89q.jnbt.IntTag;
|
||||
import com.sk89q.jnbt.NBTInputStream;
|
||||
import com.sk89q.jnbt.ShortTag;
|
||||
import com.sk89q.jnbt.Tag;
|
||||
import com.sk89q.worldedit.WorldEdit;
|
||||
import com.sk89q.worldedit.WorldEditException;
|
||||
import com.sk89q.worldedit.extent.Extent;
|
||||
import com.sk89q.worldedit.world.World;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
@ -16,7 +29,15 @@ import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
@ -24,11 +45,229 @@ import org.bukkit.Location;
|
||||
/**
|
||||
* The FaweAPI class offers a few useful functions.<br>
|
||||
* - This class is not intended to replace the WorldEdit API<br>
|
||||
* - With FAWE installed, you can use the EditSession and other WorldEdit classes from an async thread.<br>
|
||||
* <br>
|
||||
* FaweAPI.[some method]
|
||||
*/
|
||||
public class FaweAPI {
|
||||
|
||||
/**
|
||||
* The TaskManager has some useful methods for doing things asynchronously
|
||||
* @return TaskManager
|
||||
*/
|
||||
public TaskManager getTaskManager() {
|
||||
return TaskManager.IMP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap some object into a FawePlayer<br>
|
||||
* - org.bukkit.entity.Player
|
||||
* - org.spongepowered.api.entity.living.player
|
||||
* - com.sk89q.worldedit.entity.Player
|
||||
* - String (name)
|
||||
* - UUID (player UUID)
|
||||
* @param obj
|
||||
* @return
|
||||
*/
|
||||
public FawePlayer wrapPlayer(Object obj) {
|
||||
return FawePlayer.wrap(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* You can either use a FaweQueue or an EditSession to change blocks<br>
|
||||
* - The FaweQueue skips a bit of overhead so it's faster<br>
|
||||
* - The WorldEdit EditSession can do a lot more<br>
|
||||
* Remember to enqueue it when you're done!<br>
|
||||
* @see com.boydti.fawe.util.FaweQueue#enqueue()
|
||||
* @param worldName The name of the world
|
||||
* @param autoqueue If it should start dispatching before you enqueue it.
|
||||
* @return
|
||||
*/
|
||||
public FaweQueue createQueue(String worldName, boolean autoqueue) {
|
||||
return SetQueue.IMP.getNewQueue(worldName, autoqueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of supported protection plugin masks.
|
||||
* @return Set of FaweMaskManager
|
||||
*/
|
||||
public Set<FaweMaskManager> getMaskManagers() {
|
||||
return new HashSet<>(WEManager.IMP.managers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server has more than the configured low memory threshold
|
||||
* @return True if the server has limited memory
|
||||
*/
|
||||
public boolean isMemoryLimited() {
|
||||
return MemUtil.isMemoryLimited();
|
||||
}
|
||||
|
||||
/**
|
||||
* If you just need things to look random, use this faster alternative
|
||||
* @return PseudoRandom
|
||||
*/
|
||||
public PseudoRandom getFastRandom() {
|
||||
return new PseudoRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player's allowed WorldEdit region
|
||||
* @param player
|
||||
* @return
|
||||
*/
|
||||
public Set<RegionWrapper> getRegions(FawePlayer player) {
|
||||
return WEManager.IMP.getMask(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the edit with the following extent<br>
|
||||
* - The extent must be the one being used by an EditSession, otherwise an error may be thrown <br>
|
||||
* - Insert an extent into the EditSession using the EditSessionEvent: http://wiki.sk89q.com/wiki/WorldEdit/API/Hooking_EditSession <br>
|
||||
* @see com.sk89q.worldedit.EditSession#getFaweExtent() To get the FaweExtent for an EditSession
|
||||
* @param extent
|
||||
* @param reason
|
||||
*/
|
||||
public void cancelEdit(Extent extent, BBC reason) {
|
||||
try {
|
||||
WEManager.IMP.cancelEdit(extent, reason);
|
||||
} catch (WorldEditException ignore) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DiskStorageHistory object representing a File
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public DiskStorageHistory getChangeSetFromFile(File file) {
|
||||
if (!file.exists() || file.isDirectory()) {
|
||||
throw new IllegalArgumentException("Not a file!");
|
||||
}
|
||||
if (!file.getName().toLowerCase().endsWith(".bd")) {
|
||||
throw new IllegalArgumentException("Not a BD file!");
|
||||
}
|
||||
if (Settings.STORE_HISTORY_ON_DISK) {
|
||||
throw new IllegalArgumentException("History on disk not enabled!");
|
||||
}
|
||||
String[] path = file.getPath().split(File.separator);
|
||||
if (path.length < 3) {
|
||||
throw new IllegalArgumentException("Not in history directory!");
|
||||
}
|
||||
String worldName = path[path.length - 3];
|
||||
String uuidString = path[path.length - 2];
|
||||
World world = null;
|
||||
for (World current : WorldEdit.getInstance().getServer().getWorlds()) {
|
||||
if (current.getName().equals(worldName)) {
|
||||
world = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (world == null) {
|
||||
throw new IllegalArgumentException("Corresponding world does not exist: " + worldName);
|
||||
}
|
||||
UUID uuid;
|
||||
try {
|
||||
uuid = UUID.fromString(uuidString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid UUID from file path: " + uuidString);
|
||||
}
|
||||
DiskStorageHistory history = new DiskStorageHistory(world, uuid, Integer.parseInt(file.getName().split("\\.")[0]));
|
||||
return history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the RollBack to generate a list of DiskStorageHistory objects<br>
|
||||
* - Note: An edit outside the radius may be included if it overlaps with an edit inside that depends on it.
|
||||
* @param origin - The origin location
|
||||
* @param user - The uuid (may be null)
|
||||
* @param radius - The radius from the origin of the edit
|
||||
* @param timediff - The max age of the file in milliseconds
|
||||
* @param shallow - If shallow is true, FAWE will only read the first Settings.BUFFER_SIZE bytes to obtain history info<br>
|
||||
* Reading only part of the file will result in unreliable bounds info for large edits
|
||||
* @return
|
||||
*/
|
||||
public static List<DiskStorageHistory> getBDFiles(FaweLocation origin, UUID user, int radius, long timediff, boolean shallow) {
|
||||
File history = new File(Fawe.imp().getDirectory(), "history" + File.separator + origin.world);
|
||||
if (!history.exists()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
ArrayList<File> files = new ArrayList<>();
|
||||
for (File userFile : history.listFiles()) {
|
||||
if (!userFile.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
UUID userUUID;
|
||||
try {
|
||||
userUUID = UUID.fromString(userFile.getName());
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
if (user != null && !userUUID.equals(user)) {
|
||||
continue;
|
||||
}
|
||||
ArrayList<Integer> ids = new ArrayList<>();
|
||||
for (File file : userFile.listFiles()) {
|
||||
if (file.getName().endsWith(".bd")) {
|
||||
if (timediff >= Integer.MAX_VALUE || now - file.lastModified() <= timediff) {
|
||||
files.add(file);
|
||||
if (files.size() > 2048) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
World world = origin.getWorld();
|
||||
Collections.sort(files, new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File a, File b) {
|
||||
long value = a.lastModified() - b.lastModified();
|
||||
return value == 0 ? 0 : value < 0 ? -1 : 1;
|
||||
}
|
||||
});
|
||||
RegionWrapper bounds = new RegionWrapper(origin.x - radius, origin.x + radius, origin.z - radius, origin.z + radius);
|
||||
RegionWrapper boundsPlus = new RegionWrapper(bounds.minX - 64, bounds.maxX + 512, bounds.minZ - 64, bounds.maxZ + 512);
|
||||
HashSet<RegionWrapper> regionSet = new HashSet<RegionWrapper>(Arrays.asList(bounds));
|
||||
ArrayList<DiskStorageHistory> result = new ArrayList<>();
|
||||
for (File file : files) {
|
||||
UUID uuid = UUID.fromString(file.getParentFile().getName());
|
||||
DiskStorageHistory dsh = new DiskStorageHistory(world, uuid, Integer.parseInt(file.getName().split("\\.")[0]));
|
||||
DiskStorageHistory.DiskStorageSummary summary = dsh.summarize(boundsPlus, shallow);
|
||||
RegionWrapper region = new RegionWrapper(summary.minX, summary.maxX, summary.minZ, summary.maxZ);
|
||||
boolean encompassed = false;
|
||||
boolean isIn = false;
|
||||
for (RegionWrapper allowed : regionSet) {
|
||||
isIn = isIn || allowed.intersects(region);
|
||||
if (encompassed = allowed.isIn(region.minX, region.maxX) && allowed.isIn(region.minZ, region.maxZ)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isIn) {
|
||||
result.add(0, dsh);
|
||||
if (!encompassed) {
|
||||
regionSet.add(region);
|
||||
}
|
||||
if (shallow && result.size() > 64) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The DiskStorageHistory class is what FAWE uses to represent the undo on disk.
|
||||
* @see com.boydti.fawe.object.changeset.DiskStorageHistory#toEditSession(com.sk89q.worldedit.entity.Player)
|
||||
* @param world
|
||||
* @param uuid
|
||||
* @param index
|
||||
* @return
|
||||
*/
|
||||
public DiskStorageHistory getChangeSetFromDisk(World world, UUID uuid, int index) {
|
||||
return new DiskStorageHistory(world, uuid, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two versions
|
||||
* @param version
|
||||
@ -41,12 +280,23 @@ public class FaweAPI {
|
||||
return (version[0] > major) || ((version[0] == major) && (version[1] > minor)) || ((version[0] == major) && (version[1] == minor) && (version[2] >= minor2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the lighting in a chunk
|
||||
* @param world
|
||||
* @param x
|
||||
* @param z
|
||||
* @param fixAll
|
||||
*/
|
||||
public static void fixLighting(String world, int x, int z, final boolean fixAll) {
|
||||
FaweQueue queue = SetQueue.IMP.getNewQueue(world, false);
|
||||
queue.fixLighting(queue.getChunk(x, z), fixAll);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fix the lighting in a chunk
|
||||
* @param chunk
|
||||
* @param fixAll
|
||||
*/
|
||||
public static void fixLighting(final Chunk chunk, final boolean fixAll) {
|
||||
FaweQueue queue = SetQueue.IMP.getNewQueue(chunk.getWorld().getName(), false);
|
||||
queue.fixLighting(queue.getChunk(chunk.getX(), chunk.getZ()), fixAll);
|
||||
@ -55,59 +305,49 @@ public class FaweAPI {
|
||||
/**
|
||||
* If a schematic is too large to be pasted normally<br>
|
||||
* - Skips any block history
|
||||
* - Ignores some block data
|
||||
* - No, it's not streaming it from disk, but it is a lot faster
|
||||
* - Ignores nbt
|
||||
* - No, technically I haven't added proper streaming yet (WIP)
|
||||
* @param file
|
||||
* @param loc
|
||||
* @return
|
||||
*/
|
||||
public static void streamSchematicAsync(final File file, final Location loc) {
|
||||
public static void streamSchematic(final File file, final Location loc) {
|
||||
final FaweLocation fl = new FaweLocation(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
|
||||
streamSchematicAsync(file, fl);
|
||||
streamSchematic(file, fl);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a schematic is too large to be pasted normally<br>
|
||||
* - Skips any block history
|
||||
* - Ignores some block data
|
||||
* - Ignores nbt
|
||||
* @param file
|
||||
* @param loc
|
||||
* @return
|
||||
*/
|
||||
public static void streamSchematicAsync(final File file, final FaweLocation loc) {
|
||||
TaskManager.IMP.async(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final FileInputStream is = new FileInputStream(file);
|
||||
streamSchematic(is, loc);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
public static void streamSchematic(final File file, final FaweLocation loc) {
|
||||
try {
|
||||
final FileInputStream is = new FileInputStream(file);
|
||||
streamSchematic(is, loc);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a schematic is too large to be pasted normally<br>
|
||||
* - Skips any block history
|
||||
* - Ignores some block data
|
||||
* - Ignores nbt
|
||||
* @param url
|
||||
* @param loc
|
||||
*/
|
||||
public static void streamSchematicAsync(final URL url, final FaweLocation loc) {
|
||||
TaskManager.IMP.async(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final ReadableByteChannel rbc = Channels.newChannel(url.openStream());
|
||||
final InputStream is = Channels.newInputStream(rbc);
|
||||
streamSchematic(is, loc);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
public static void streamSchematic(final URL url, final FaweLocation loc) {
|
||||
try {
|
||||
final ReadableByteChannel rbc = Channels.newChannel(url.openStream());
|
||||
final InputStream is = Channels.newInputStream(rbc);
|
||||
streamSchematic(is, loc);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,10 +481,34 @@ public class FaweAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a task to run when the async queue is empty
|
||||
* Set a task to run when the global queue (SetQueue class) is empty
|
||||
* @param whenDone
|
||||
*/
|
||||
public static void addTask(final Runnable whenDone) {
|
||||
SetQueue.IMP.addTask(whenDone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Have a task run when the server is low on memory (configured threshold)
|
||||
* @param run
|
||||
*/
|
||||
public static void addMemoryLimitedTask(Runnable run) {
|
||||
MemUtil.addMemoryLimitedTask(run);
|
||||
}
|
||||
|
||||
/**
|
||||
* Have a task run when the server is no longer low on memory (configured threshold)
|
||||
* @param run
|
||||
*/
|
||||
public static void addMemoryPlentifulTask(Runnable run) {
|
||||
MemUtil.addMemoryPlentifulTask(run);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BBC
|
||||
* @return
|
||||
*/
|
||||
public BBC[] getTranslations() {
|
||||
return BBC.values();
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import com.sk89q.worldedit.regions.RegionSelector;
|
||||
import com.sk89q.worldedit.regions.selector.CuboidRegionSelector;
|
||||
import com.sk89q.worldedit.world.World;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@ -32,7 +33,35 @@ public abstract class FawePlayer<T> {
|
||||
*/
|
||||
private volatile ConcurrentHashMap<String, Object> meta;
|
||||
|
||||
/**
|
||||
* Wrap some object into a FawePlayer<br>
|
||||
* - org.bukkit.entity.Player
|
||||
* - org.spongepowered.api.entity.living.player
|
||||
* - com.sk89q.worldedit.entity.Player
|
||||
* - String (name)
|
||||
* - UUID (player UUID)
|
||||
* @param obj
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
public static <V> FawePlayer<V> wrap(final Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
if (obj instanceof Player) {
|
||||
Player actor = (Player) obj;
|
||||
try {
|
||||
Field fieldBasePlayer = actor.getClass().getDeclaredField("basePlayer");
|
||||
fieldBasePlayer.setAccessible(true);
|
||||
Player player = (Player) fieldBasePlayer.get(actor);
|
||||
Field fieldPlayer = player.getClass().getDeclaredField("player");
|
||||
fieldPlayer.setAccessible(true);
|
||||
return Fawe.imp().wrap(fieldPlayer.get(player));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
return Fawe.imp().wrap(actor.getName());
|
||||
}
|
||||
}
|
||||
return Fawe.imp().wrap(obj);
|
||||
}
|
||||
|
||||
@ -49,7 +78,7 @@ public abstract class FawePlayer<T> {
|
||||
if (world != null) {
|
||||
if (world.getName().equals(currentWorldName)) {
|
||||
getSession().clearHistory();
|
||||
loadSessionFromDisk(world);
|
||||
loadSessionsFromDisk(world);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -58,6 +87,10 @@ public abstract class FawePlayer<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current World
|
||||
* @return
|
||||
*/
|
||||
public World getWorld() {
|
||||
String currentWorldName = getLocation().world;
|
||||
for (World world : WorldEdit.getInstance().getServer().getWorlds()) {
|
||||
@ -68,7 +101,12 @@ public abstract class FawePlayer<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void loadSessionFromDisk(World world) {
|
||||
/**
|
||||
* Load all the undo EditSession's from disk for a world <br>
|
||||
* - Usually already called when a player joins or changes world
|
||||
* @param world
|
||||
*/
|
||||
public void loadSessionsFromDisk(World world) {
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
@ -94,26 +132,68 @@ public abstract class FawePlayer<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's limit
|
||||
* @return
|
||||
*/
|
||||
public FaweLimit getLimit() {
|
||||
return Settings.getLimit(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's name
|
||||
* @return
|
||||
*/
|
||||
public abstract String getName();
|
||||
|
||||
/**
|
||||
* Get the player's UUID
|
||||
* @return
|
||||
*/
|
||||
public abstract UUID getUUID();
|
||||
|
||||
/**
|
||||
* Check the player's permission
|
||||
* @param perm
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean hasPermission(final String perm);
|
||||
|
||||
/**
|
||||
* Set a permission (requires Vault)
|
||||
* @param perm
|
||||
* @param flag
|
||||
*/
|
||||
public abstract void setPermission(final String perm, final boolean flag);
|
||||
|
||||
/**
|
||||
* Send a message to the player
|
||||
* @param message
|
||||
*/
|
||||
public abstract void sendMessage(final String message);
|
||||
|
||||
/**
|
||||
* Have the player execute a command
|
||||
* @param substring
|
||||
*/
|
||||
public abstract void executeCommand(final String substring);
|
||||
|
||||
/**
|
||||
* Get the player's location
|
||||
* @return
|
||||
*/
|
||||
public abstract FaweLocation getLocation();
|
||||
|
||||
/**
|
||||
* Get the WorldEdit player object
|
||||
* @return
|
||||
*/
|
||||
public abstract Player getPlayer();
|
||||
|
||||
/**
|
||||
* Get the player's current selection (or null)
|
||||
* @return
|
||||
*/
|
||||
public Region getSelection() {
|
||||
try {
|
||||
return this.getSession().getSelection(this.getPlayer().getWorld());
|
||||
@ -122,20 +202,36 @@ public abstract class FawePlayer<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's current LocalSession
|
||||
* @return
|
||||
*/
|
||||
public LocalSession getSession() {
|
||||
return (this.session != null || this.getPlayer() == null) ? this.session : (session = Fawe.get().getWorldEdit().getSession(this.getPlayer()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's current allowed WorldEdit regions
|
||||
* @return
|
||||
*/
|
||||
public HashSet<RegionWrapper> getCurrentRegions() {
|
||||
return WEManager.IMP.getMask(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's WorldEdit selection to the following CuboidRegion
|
||||
* @param region
|
||||
*/
|
||||
public void setSelection(final RegionWrapper region) {
|
||||
final Player player = this.getPlayer();
|
||||
final RegionSelector selector = new CuboidRegionSelector(player.getWorld(), region.getBottomVector(), region.getTopVector());
|
||||
this.getSession().setRegionSelector(player.getWorld(), selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the largest region in the player's allowed WorldEdit region
|
||||
* @return
|
||||
*/
|
||||
public RegionWrapper getLargestRegion() {
|
||||
int area = 0;
|
||||
RegionWrapper max = null;
|
||||
@ -154,6 +250,10 @@ public abstract class FawePlayer<T> {
|
||||
return this.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the player has WorldEdit bypass enabled
|
||||
* @return
|
||||
*/
|
||||
public boolean hasWorldEditBypass() {
|
||||
return this.hasPermission("fawe.bypass");
|
||||
}
|
||||
@ -183,6 +283,13 @@ public abstract class FawePlayer<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metadata for a specific key (or return the default provided)
|
||||
* @param key
|
||||
* @param def
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
public <V> V getMeta(String key, V def) {
|
||||
if (this.meta != null) {
|
||||
V value = (V) this.meta.get(key);
|
||||
@ -201,6 +308,10 @@ public abstract class FawePlayer<T> {
|
||||
return this.meta == null ? null : this.meta.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister this player (delets all metadata etc)
|
||||
* - Usually called on logout
|
||||
*/
|
||||
public void unregister() {
|
||||
getSession().setClipboard(null);
|
||||
getSession().clearHistory();
|
||||
|
@ -19,6 +19,11 @@ public class IntegerPair {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return x + "," + z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (this == obj) {
|
||||
|
@ -46,6 +46,10 @@ public class RegionWrapper {
|
||||
return minZ - z;
|
||||
}
|
||||
|
||||
public boolean intersects(RegionWrapper other) {
|
||||
return other.minX <= this.maxX && other.maxX >= this.minX && other.minZ <= this.maxZ && other.maxZ >= this.minZ;
|
||||
}
|
||||
|
||||
public int distance(int x, int z) {
|
||||
if (isIn(x, z)) {
|
||||
return 0;
|
||||
|
@ -130,7 +130,7 @@ public class DiskStorageHistory implements ChangeSet, FaweChangeSet {
|
||||
init(world, uuid, index);
|
||||
}
|
||||
|
||||
public void init(World world, UUID uuid, int i) {
|
||||
private void init(World world, UUID uuid, int i) {
|
||||
this.uuid = uuid;
|
||||
this.world = world;
|
||||
String base = "history" + File.separator + world.getName() + File.separator + uuid;
|
||||
|
@ -15,10 +15,10 @@ public class FaweException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static FaweException get(Throwable e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof FaweException) {
|
||||
return (FaweException) cause;
|
||||
if (e instanceof FaweException) {
|
||||
return (FaweException) e;
|
||||
}
|
||||
Throwable cause = e.getCause();
|
||||
if (cause == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -2,24 +2,15 @@ package com.boydti.fawe.util;
|
||||
|
||||
import com.boydti.fawe.Fawe;
|
||||
import com.boydti.fawe.config.BBC;
|
||||
import com.boydti.fawe.object.FaweLocation;
|
||||
import com.boydti.fawe.object.FawePlayer;
|
||||
import com.boydti.fawe.object.RegionWrapper;
|
||||
import com.boydti.fawe.object.RunnableVal;
|
||||
import com.boydti.fawe.object.changeset.DiskStorageHistory;
|
||||
import com.sk89q.jnbt.CompoundTag;
|
||||
import com.sk89q.jnbt.EndTag;
|
||||
import com.sk89q.jnbt.ListTag;
|
||||
import com.sk89q.jnbt.Tag;
|
||||
import com.sk89q.worldedit.world.World;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MainUtil {
|
||||
/*
|
||||
@ -139,62 +130,6 @@ public class MainUtil {
|
||||
return time;
|
||||
}
|
||||
|
||||
public static List<DiskStorageHistory> getBDFiles(FaweLocation origin, UUID user, int radius, long timediff, boolean shallow) {
|
||||
File history = new File(Fawe.imp().getDirectory(), "history" + File.separator + origin.world);
|
||||
if (!history.exists()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
ArrayList<File> files = new ArrayList<>();
|
||||
for (File userFile : history.listFiles()) {
|
||||
if (!userFile.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
UUID userUUID;
|
||||
try {
|
||||
userUUID = UUID.fromString(userFile.getName());
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
if (user != null && !userUUID.equals(user)) {
|
||||
continue;
|
||||
}
|
||||
ArrayList<Integer> ids = new ArrayList<>();
|
||||
for (File file : userFile.listFiles()) {
|
||||
if (file.getName().endsWith(".bd")) {
|
||||
if (timediff > Integer.MAX_VALUE || now - file.lastModified() <= timediff) {
|
||||
files.add(file);
|
||||
if (files.size() > 2048) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
World world = origin.getWorld();
|
||||
Collections.sort(files, new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File a, File b) {
|
||||
long value = a.lastModified() - b.lastModified();
|
||||
return value == 0 ? 0 : value < 0 ? 1 : -1;
|
||||
}
|
||||
});
|
||||
ArrayList<DiskStorageHistory> result = new ArrayList<>();
|
||||
for (File file : files) {
|
||||
UUID uuid = UUID.fromString(file.getParentFile().getName());
|
||||
DiskStorageHistory dsh = new DiskStorageHistory(world, uuid, Integer.parseInt(file.getName().split("\\.")[0]));
|
||||
DiskStorageHistory.DiskStorageSummary summary = dsh.summarize(new RegionWrapper(origin.x - 512, origin.x + 512, origin.z - 512, origin.z + 512), shallow);
|
||||
RegionWrapper region = new RegionWrapper(summary.minX, summary.maxX, summary.minZ, summary.maxZ);
|
||||
if (region.distance(origin.x, origin.z) <= radius) {
|
||||
result.add(dsh);
|
||||
if (result.size() > 64) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void deleteOlder(File directory, final long timeDiff) {
|
||||
final long now = System.currentTimeMillis();
|
||||
iterateFiles(directory, new RunnableVal<File>() {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.boydti.fawe.util;
|
||||
|
||||
import com.boydti.fawe.config.Settings;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class MemUtil {
|
||||
@ -30,11 +32,30 @@ public class MemUtil {
|
||||
return size;
|
||||
}
|
||||
|
||||
private static BlockingQueue<Runnable> memoryLimitedTasks = new LinkedBlockingQueue<>();
|
||||
private static BlockingQueue<Runnable> memoryPlentifulTasks = new LinkedBlockingQueue<>();
|
||||
|
||||
public static void addMemoryLimitedTask(Runnable run) {
|
||||
if (run != null)
|
||||
memoryLimitedTasks.add(run);
|
||||
}
|
||||
|
||||
public static void addMemoryPlentifulTask(Runnable run) {
|
||||
if (run != null)
|
||||
memoryPlentifulTasks.add(run);
|
||||
}
|
||||
|
||||
public static void memoryLimitedTask() {
|
||||
for (Runnable task : memoryLimitedTasks) {
|
||||
task.run();
|
||||
}
|
||||
memory.set(true);
|
||||
}
|
||||
|
||||
public static void memoryPlentifulTask() {
|
||||
for (Runnable task : memoryPlentifulTasks) {
|
||||
task.run();
|
||||
}
|
||||
memory.set(false);
|
||||
}
|
||||
}
|
||||
|
@ -4,19 +4,46 @@ import com.boydti.fawe.Fawe;
|
||||
import com.boydti.fawe.object.RunnableVal;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract class TaskManager {
|
||||
|
||||
public static TaskManager IMP;
|
||||
|
||||
/**
|
||||
* Run a repeating task on the main thread
|
||||
* @param r
|
||||
* @param interval in ticks
|
||||
* @return
|
||||
*/
|
||||
public abstract int repeat(final Runnable r, final int interval);
|
||||
|
||||
/**
|
||||
* Run a repeating task asynchronously
|
||||
* @param r
|
||||
* @param interval in ticks
|
||||
* @return
|
||||
*/
|
||||
public abstract int repeatAsync(final Runnable r, final int interval);
|
||||
|
||||
/**
|
||||
* Run a task asynchronously
|
||||
* @param r
|
||||
*/
|
||||
public abstract void async(final Runnable r);
|
||||
|
||||
/**
|
||||
* Run a task on the main thread
|
||||
* @param r
|
||||
*/
|
||||
public abstract void task(final Runnable r);
|
||||
|
||||
/**
|
||||
* Run a task on either the main thread or asynchronously
|
||||
* - If it's already the main thread, it will jst call run()
|
||||
* @param r
|
||||
* @param async
|
||||
*/
|
||||
public void task(final Runnable r, boolean async) {
|
||||
if (async) {
|
||||
async(r);
|
||||
@ -31,12 +58,35 @@ public abstract class TaskManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task later on the main thread
|
||||
* @param r
|
||||
* @param delay in ticks
|
||||
*/
|
||||
public abstract void later(final Runnable r, final int delay);
|
||||
|
||||
/**
|
||||
* Run a task later asynchronously
|
||||
* @param r
|
||||
* @param delay in ticks
|
||||
*/
|
||||
public abstract void laterAsync(final Runnable r, final int delay);
|
||||
|
||||
/**
|
||||
* Cancel a task
|
||||
* @param task
|
||||
*/
|
||||
public abstract void cancel(final int task);
|
||||
|
||||
/**
|
||||
* Break up a task and run it in fragments of 5ms.<br>
|
||||
* - Each task will run on the main thread.<br>
|
||||
* - Usualy wait time is around 25ms<br>
|
||||
* @param objects - The list of objects to run the task for
|
||||
* @param task - The task to run on each object
|
||||
* @param whenDone - When the object task completes
|
||||
* @param <T>
|
||||
*/
|
||||
public <T> void objectTask(Collection<T> objects, final RunnableVal<T> task, final Runnable whenDone) {
|
||||
final Iterator<T> iterator = objects.iterator();
|
||||
task(new Runnable() {
|
||||
@ -57,11 +107,31 @@ public abstract class TaskManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly run a task on the main thread, and wait for execution to finish:<br>
|
||||
* - Useful if you need to access something from the Bukkit API from another thread<br>
|
||||
* @param function
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
public <T> T sync(final RunnableVal<T> function) {
|
||||
return sync(function, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly run a task on the main thread, and wait for execution to finish:<br>
|
||||
* - Useful if you need to access something from the Bukkit API from another thread<br>
|
||||
* @param function
|
||||
* @param timeout - How long to wait for execution
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
public <T> T sync(final RunnableVal<T> function, int timeout) {
|
||||
if (Fawe.get().getMainThread() == Thread.currentThread()) {
|
||||
function.run();
|
||||
return function.value;
|
||||
}
|
||||
final AtomicBoolean running = new AtomicBoolean(true);
|
||||
RunnableVal<RuntimeException> run = new RunnableVal<RuntimeException>() {
|
||||
@Override
|
||||
public void run(RuntimeException value) {
|
||||
@ -71,6 +141,8 @@ public abstract class TaskManager {
|
||||
this.value = e;
|
||||
} catch (Throwable neverHappens) {
|
||||
neverHappens.printStackTrace();
|
||||
} finally {
|
||||
running.set(false);
|
||||
}
|
||||
synchronized (function) {
|
||||
function.notifyAll();
|
||||
@ -78,16 +150,18 @@ public abstract class TaskManager {
|
||||
}
|
||||
};
|
||||
TaskManager.IMP.task(run);
|
||||
if (run.value != null) {
|
||||
throw run.value;
|
||||
}
|
||||
try {
|
||||
synchronized (function) {
|
||||
function.wait(15000);
|
||||
while (running.get()) {
|
||||
function.wait(timeout);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (run.value != null) {
|
||||
throw run.value;
|
||||
}
|
||||
return function.value;
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@
|
||||
|
||||
package com.sk89q.worldedit.extension.platform;
|
||||
|
||||
import com.boydti.fawe.Fawe;
|
||||
import com.boydti.fawe.config.BBC;
|
||||
import com.boydti.fawe.object.FawePlayer;
|
||||
import com.boydti.fawe.object.exception.FaweException;
|
||||
@ -86,7 +85,6 @@ import com.sk89q.worldedit.util.logging.DynamicStreamHandler;
|
||||
import com.sk89q.worldedit.util.logging.LogFormat;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.logging.FileHandler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
@ -238,19 +236,8 @@ public final class CommandManager {
|
||||
LocalConfiguration config = worldEdit.getConfiguration();
|
||||
|
||||
CommandLocals locals = new CommandLocals();
|
||||
FawePlayer fp;
|
||||
if (actor != null && actor.isPlayer()) {
|
||||
try {
|
||||
Field fieldBasePlayer = actor.getClass().getDeclaredField("basePlayer");
|
||||
fieldBasePlayer.setAccessible(true);
|
||||
Player player = (Player) fieldBasePlayer.get(actor);
|
||||
Field fieldPlayer = player.getClass().getDeclaredField("player");
|
||||
fieldPlayer.setAccessible(true);
|
||||
fp = Fawe.imp().wrap(fieldPlayer.get(player));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
fp = Fawe.imp().wrap(actor.getName());
|
||||
}
|
||||
FawePlayer fp = FawePlayer.wrap(actor);
|
||||
if (fp != null) {
|
||||
if (fp.getMeta("fawe_action") != null) {
|
||||
BBC.WORLDEDIT_COMMAND_LIMIT.send(fp);
|
||||
return;
|
||||
@ -259,7 +246,6 @@ public final class CommandManager {
|
||||
locals.put(Actor.class, new PlayerWrapper((Player) actor));
|
||||
} else {
|
||||
locals.put(Actor.class, actor);
|
||||
fp = null;
|
||||
}
|
||||
locals.put("arguments", event.getArguments());
|
||||
final long start = System.currentTimeMillis();
|
||||
|
@ -0,0 +1,512 @@
|
||||
/*
|
||||
* 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.extension.platform;
|
||||
|
||||
import com.boydti.fawe.config.BBC;
|
||||
import com.boydti.fawe.object.exception.FaweException;
|
||||
import com.sk89q.worldedit.LocalConfiguration;
|
||||
import com.sk89q.worldedit.LocalSession;
|
||||
import com.sk89q.worldedit.ServerInterface;
|
||||
import com.sk89q.worldedit.Vector;
|
||||
import com.sk89q.worldedit.WorldEdit;
|
||||
import com.sk89q.worldedit.WorldVector;
|
||||
import com.sk89q.worldedit.command.tool.BlockTool;
|
||||
import com.sk89q.worldedit.command.tool.DoubleActionBlockTool;
|
||||
import com.sk89q.worldedit.command.tool.DoubleActionTraceTool;
|
||||
import com.sk89q.worldedit.command.tool.Tool;
|
||||
import com.sk89q.worldedit.command.tool.TraceTool;
|
||||
import com.sk89q.worldedit.entity.Player;
|
||||
import com.sk89q.worldedit.event.platform.BlockInteractEvent;
|
||||
import com.sk89q.worldedit.event.platform.ConfigurationLoadEvent;
|
||||
import com.sk89q.worldedit.event.platform.Interaction;
|
||||
import com.sk89q.worldedit.event.platform.PlatformInitializeEvent;
|
||||
import com.sk89q.worldedit.event.platform.PlatformReadyEvent;
|
||||
import com.sk89q.worldedit.event.platform.PlayerInputEvent;
|
||||
import com.sk89q.worldedit.extension.platform.permission.ActorSelectorLimits;
|
||||
import com.sk89q.worldedit.internal.ServerInterfaceAdapter;
|
||||
import com.sk89q.worldedit.regions.RegionSelector;
|
||||
import com.sk89q.worldedit.util.Location;
|
||||
import com.sk89q.worldedit.util.eventbus.Subscribe;
|
||||
import com.sk89q.worldedit.world.World;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Manages registered {@link Platform}s for WorldEdit. Platforms are
|
||||
* implementations of WorldEdit.
|
||||
*
|
||||
* <p>This class is thread-safe.</p>
|
||||
*/
|
||||
public class PlatformManager {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(PlatformManager.class.getCanonicalName());
|
||||
|
||||
private final WorldEdit worldEdit;
|
||||
private final CommandManager commandManager;
|
||||
private final List<Platform> platforms = new ArrayList<Platform>();
|
||||
private final Map<Capability, Platform> preferences = new EnumMap<Capability, Platform>(Capability.class);
|
||||
private @Nullable String firstSeenVersion;
|
||||
private final AtomicBoolean initialized = new AtomicBoolean();
|
||||
private final AtomicBoolean configured = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Create a new platform manager.
|
||||
*
|
||||
* @param worldEdit the WorldEdit instance
|
||||
*/
|
||||
public PlatformManager(WorldEdit worldEdit) {
|
||||
checkNotNull(worldEdit);
|
||||
this.worldEdit = worldEdit;
|
||||
this.commandManager = new CommandManager(worldEdit, this);
|
||||
|
||||
// Register this instance for events
|
||||
worldEdit.getEventBus().register(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a platform with WorldEdit.
|
||||
*
|
||||
* @param platform the platform
|
||||
*/
|
||||
public synchronized void register(Platform platform) {
|
||||
checkNotNull(platform);
|
||||
|
||||
logger.log(Level.FINE, "Got request to register " + platform.getClass() + " with WorldEdit [" + super.toString() + "]");
|
||||
|
||||
// Just add the platform to the list of platforms: we'll pick favorites
|
||||
// once all the platforms have been loaded
|
||||
platforms.add(platform);
|
||||
|
||||
// Make sure that versions are in sync
|
||||
if (firstSeenVersion != null) {
|
||||
if (!firstSeenVersion.equals(platform.getVersion())) {
|
||||
logger.log(Level.WARNING, "Multiple ports of WorldEdit are installed but they report different versions ({0} and {1}). " +
|
||||
"If these two versions are truly different, then you may run into unexpected crashes and errors.",
|
||||
new Object[]{ firstSeenVersion, platform.getVersion() });
|
||||
}
|
||||
} else {
|
||||
firstSeenVersion = platform.getVersion();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a platform from WorldEdit.
|
||||
*
|
||||
* <p>If the platform has been chosen for any capabilities, then a new
|
||||
* platform will be found.</p>
|
||||
*
|
||||
* @param platform the platform
|
||||
*/
|
||||
public synchronized boolean unregister(Platform platform) {
|
||||
checkNotNull(platform);
|
||||
|
||||
boolean removed = platforms.remove(platform);
|
||||
|
||||
if (removed) {
|
||||
logger.log(Level.FINE, "Unregistering " + platform.getClass().getCanonicalName() + " from WorldEdit");
|
||||
|
||||
boolean choosePreferred = false;
|
||||
|
||||
// Check whether this platform was chosen to be the preferred one
|
||||
// for any capability and be sure to remove it
|
||||
Iterator<Entry<Capability, Platform>> it = preferences.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Entry<Capability, Platform> entry = it.next();
|
||||
if (entry.getValue().equals(platform)) {
|
||||
Capability key = entry.getKey();
|
||||
try {
|
||||
Method methodUnload = key.getClass().getDeclaredMethod("unload", PlatformManager.class, Platform.class);
|
||||
methodUnload.setAccessible(true);
|
||||
methodUnload.invoke(key, this, entry.getValue());
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
it.remove();
|
||||
choosePreferred = true; // Have to choose new favorites
|
||||
}
|
||||
}
|
||||
|
||||
if (choosePreferred) {
|
||||
choosePreferred();
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred platform for handling a certain capability. Returns
|
||||
* null if none is available.
|
||||
*
|
||||
* @param capability the capability
|
||||
* @return the platform
|
||||
* @throws NoCapablePlatformException thrown if no platform is capable
|
||||
*/
|
||||
public synchronized Platform queryCapability(Capability capability) throws NoCapablePlatformException {
|
||||
Platform platform = preferences.get(checkNotNull(capability));
|
||||
if (platform != null) {
|
||||
return platform;
|
||||
} else {
|
||||
throw new NoCapablePlatformException("No platform was found supporting " + capability.name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose preferred platforms and perform necessary initialization.
|
||||
*/
|
||||
private synchronized void choosePreferred() {
|
||||
for (Capability capability : Capability.values()) {
|
||||
Platform preferred = findMostPreferred(capability);
|
||||
if (preferred != null) {
|
||||
preferences.put(capability, preferred);
|
||||
try {
|
||||
Method methodInitialize = Capability.class.getDeclaredMethod("initialize", PlatformManager.class, Platform.class);
|
||||
methodInitialize.setAccessible(true);
|
||||
methodInitialize.invoke(capability, this, preferred);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fire configuration event
|
||||
if (preferences.containsKey(Capability.CONFIGURATION) && configured.compareAndSet(false, true)) {
|
||||
worldEdit.getEventBus().post(new ConfigurationLoadEvent(queryCapability(Capability.CONFIGURATION).getConfiguration()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most preferred platform for a given capability from the list of
|
||||
* platforms. This does not use the map of preferred platforms.
|
||||
*
|
||||
* @param capability the capability
|
||||
* @return the most preferred platform, or null if no platform was found
|
||||
*/
|
||||
private synchronized @Nullable Platform findMostPreferred(Capability capability) {
|
||||
Platform preferred = null;
|
||||
Preference highest = null;
|
||||
|
||||
for (Platform platform : platforms) {
|
||||
Preference preference = platform.getCapabilities().get(capability);
|
||||
if (preference != null && (highest == null || preference.isPreferredOver(highest))) {
|
||||
preferred = platform;
|
||||
highest = preference;
|
||||
}
|
||||
}
|
||||
|
||||
return preferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of loaded platforms.
|
||||
*
|
||||
* <p>The returned list is a copy of the original and is mutable.</p>
|
||||
*
|
||||
* @return a list of platforms
|
||||
*/
|
||||
public synchronized List<Platform> getPlatforms() {
|
||||
return new ArrayList<Platform>(platforms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a world, possibly return the same world but using a different
|
||||
* platform preferred for world editing operations.
|
||||
*
|
||||
* @param base the world to match
|
||||
* @return the preferred world, if one was found, otherwise the given world
|
||||
*/
|
||||
public World getWorldForEditing(World base) {
|
||||
checkNotNull(base);
|
||||
World match = queryCapability(Capability.WORLD_EDITING).matchWorld(base);
|
||||
return match != null ? match : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an actor, return a new one that may use a different platform
|
||||
* for permissions and world editing.
|
||||
*
|
||||
* @param base the base actor to match
|
||||
* @return a new delegate actor
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Actor> T createProxyActor(T base) {
|
||||
checkNotNull(base);
|
||||
|
||||
if (base instanceof Player) {
|
||||
Player player = (Player) base;
|
||||
|
||||
Player permActor = queryCapability(Capability.PERMISSIONS).matchPlayer(player);
|
||||
if (permActor == null) {
|
||||
permActor = player;
|
||||
}
|
||||
|
||||
Player cuiActor = queryCapability(Capability.WORLDEDIT_CUI).matchPlayer(player);
|
||||
if (cuiActor == null) {
|
||||
cuiActor = player;
|
||||
}
|
||||
try {
|
||||
Class<?> clazz = Class.forName("com.sk89q.worldedit.extension.platform.PlayerProxy");
|
||||
Constructor<?> constructor = clazz.getDeclaredConstructor(Player.class, Actor.class, Actor.class, World.class);
|
||||
constructor.setAccessible(true);
|
||||
return (T) constructor.newInstance(player, permActor, cuiActor, getWorldForEditing(player.getWorld()));
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command manager.
|
||||
*
|
||||
* @return the command manager
|
||||
*/
|
||||
public CommandManager getCommandManager() {
|
||||
return commandManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration.
|
||||
*
|
||||
* <p>If no platform has been registered yet, then a default configuration
|
||||
* will be returned.</p>
|
||||
*
|
||||
* @return the configuration
|
||||
*/
|
||||
public LocalConfiguration getConfiguration() {
|
||||
return queryCapability(Capability.CONFIGURATION).getConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a legacy {@link ServerInterface}.
|
||||
*
|
||||
* @return a {@link ServerInterface}
|
||||
* @throws IllegalStateException if no platform has been registered
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public ServerInterface getServerInterface() throws IllegalStateException {
|
||||
return ServerInterfaceAdapter.adapt(queryCapability(Capability.USER_COMMANDS));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void handlePlatformReady(PlatformReadyEvent event) {
|
||||
choosePreferred();
|
||||
if (initialized.compareAndSet(false, true)) {
|
||||
worldEdit.getEventBus().post(new PlatformInitializeEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Subscribe
|
||||
public void handleBlockInteract(BlockInteractEvent event) {
|
||||
// Create a proxy actor with a potentially different world for
|
||||
// making changes to the world
|
||||
Actor actor = createProxyActor(event.getCause());
|
||||
try {
|
||||
Location location = event.getLocation();
|
||||
Vector vector = location.toVector();
|
||||
|
||||
// At this time, only handle interaction from players
|
||||
if (actor instanceof Player) {
|
||||
Player player = (Player) actor;
|
||||
LocalSession session = worldEdit.getSessionManager().get(actor);
|
||||
|
||||
if (event.getType() == Interaction.HIT) {
|
||||
if (player.getItemInHand() == getConfiguration().wandItem) {
|
||||
if (!session.isToolControlEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actor.hasPermission("worldedit.selection.pos")) {
|
||||
return;
|
||||
}
|
||||
|
||||
RegionSelector selector = session.getRegionSelector(player.getWorld());
|
||||
|
||||
if (selector.selectPrimary(location.toVector(), ActorSelectorLimits.forActor(player))) {
|
||||
selector.explainPrimarySelection(actor, session, vector);
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.isHoldingPickAxe() && session.hasSuperPickAxe()) {
|
||||
final BlockTool superPickaxe = session.getSuperPickaxe();
|
||||
if (superPickaxe != null && superPickaxe.canUse(player)) {
|
||||
event.setCancelled(superPickaxe.actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Tool tool = session.getTool(player.getItemInHand());
|
||||
if (tool != null && tool instanceof DoubleActionBlockTool) {
|
||||
if (tool.canUse(player)) {
|
||||
((DoubleActionBlockTool) tool).actSecondary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (event.getType() == Interaction.OPEN) {
|
||||
if (player.getItemInHand() == getConfiguration().wandItem) {
|
||||
if (!session.isToolControlEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actor.hasPermission("worldedit.selection.pos")) {
|
||||
return;
|
||||
}
|
||||
|
||||
RegionSelector selector = session.getRegionSelector(player.getWorld());
|
||||
if (selector.selectSecondary(vector, ActorSelectorLimits.forActor(player))) {
|
||||
selector.explainSecondarySelection(actor, session, vector);
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Tool tool = session.getTool(player.getItemInHand());
|
||||
if (tool != null && tool instanceof BlockTool) {
|
||||
if (tool.canUse(player)) {
|
||||
((BlockTool) tool).actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session, location);
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
FaweException faweException = FaweException.get(e);
|
||||
if (faweException != null) {
|
||||
actor.printError(BBC.PREFIX.s() + " " + BBC.WORLDEDIT_CANCEL_REASON.format(faweException.getMessage()));
|
||||
} else {
|
||||
actor.printError("Please report this error: [See console]");
|
||||
actor.printRaw(e.getClass().getName() + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Subscribe
|
||||
public void handlePlayerInput(PlayerInputEvent event) {
|
||||
// Create a proxy actor with a potentially different world for
|
||||
// making changes to the world
|
||||
Player player = createProxyActor(event.getPlayer());
|
||||
try {
|
||||
switch (event.getInputType()) {
|
||||
case PRIMARY: {
|
||||
if (player.getItemInHand() == getConfiguration().navigationWand) {
|
||||
if (getConfiguration().navigationWandMaxDistance <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.hasPermission("worldedit.navigation.jumpto.tool")) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorldVector pos = player.getSolidBlockTrace(getConfiguration().navigationWandMaxDistance);
|
||||
if (pos != null) {
|
||||
player.findFreePosition(pos);
|
||||
} else {
|
||||
player.printError("No block in sight (or too far)!");
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
LocalSession session = worldEdit.getSessionManager().get(player);
|
||||
|
||||
Tool tool = session.getTool(player.getItemInHand());
|
||||
if (tool != null && tool instanceof DoubleActionTraceTool) {
|
||||
if (tool.canUse(player)) {
|
||||
((DoubleActionTraceTool) tool).actSecondary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session);
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SECONDARY: {
|
||||
if (player.getItemInHand() == getConfiguration().navigationWand) {
|
||||
if (getConfiguration().navigationWandMaxDistance <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.hasPermission("worldedit.navigation.thru.tool")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.passThroughForwardWall(40)) {
|
||||
player.printError("Nothing to pass through!");
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
LocalSession session = worldEdit.getSessionManager().get(player);
|
||||
|
||||
Tool tool = session.getTool(player.getItemInHand());
|
||||
if (tool != null && tool instanceof TraceTool) {
|
||||
if (tool.canUse(player)) {
|
||||
((TraceTool) tool).actPrimary(queryCapability(Capability.WORLD_EDITING), getConfiguration(), player, session);
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
FaweException faweException = FaweException.get(e);
|
||||
if (faweException != null) {
|
||||
player.printError(BBC.PREFIX.s() + " " + BBC.WORLDEDIT_CANCEL_REASON.format(faweException.getMessage()));
|
||||
} else {
|
||||
player.printError("Please report this error: [See console]");
|
||||
player.printRaw(e.getClass().getName() + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Class<?> inject() {
|
||||
return PlatformManager.class;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user