bentobox/src/main/java/world/bentobox/bentobox/managers/BlueprintsManager.java

645 lines
24 KiB
Java

package world.bentobox.bentobox.managers;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.InputMismatchException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.addons.Addon;
import world.bentobox.bentobox.api.addons.GameModeAddon;
import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.metadata.MetaDataValue;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.blueprints.Blueprint;
import world.bentobox.bentobox.blueprints.BlueprintPaster;
import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock;
import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBundle;
import world.bentobox.bentobox.database.json.BentoboxTypeAdapterFactory;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;
/**
* Handles Blueprints
*
* @author Poslovitch, tastybento
* @since 1.5.0
*/
public class BlueprintsManager {
private static final String BLUEPRINT_BUNDLE_SUFFIX = ".json";
public static final String BLUEPRINT_SUFFIX = ".blu";
public static final String DEFAULT_BUNDLE_NAME = "default";
@NonNull
public static final String FOLDER_NAME = "blueprints";
private static final String FOR = "' for ";
/**
* Map of blueprint bundles to game mode addon.
* Inner map's key is the uniqueId of the blueprint bundle so it's
* easy to get from a UI
*/
@NonNull
private final Map<GameModeAddon, List<BlueprintBundle>> blueprintBundles;
/**
* Map of blueprints. There can be many blueprints per game mode addon
* Inner map's key is the blueprint's name so it's easy to get from a UI
*/
@NonNull
private final Map<GameModeAddon, List<Blueprint>> blueprints;
/**
* Gson used for serializing/deserializing the bundle class
*/
private final Gson gson;
private final @NonNull BentoBox plugin;
@NonNull
private final Set<GameModeAddon> blueprintsLoaded;
public BlueprintsManager(@NonNull BentoBox plugin) {
this.plugin = plugin;
// Must use ConcurrentHashMap because the maps are loaded async and they need to be thread safe
this.blueprintBundles = new ConcurrentHashMap<>();
this.blueprints = new ConcurrentHashMap<>();
@SuppressWarnings({"rawtypes", "unchecked"})
GsonBuilder builder = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.enableComplexMapKeySerialization()
.setPrettyPrinting()
// This enables gson to deserialize enum maps
.registerTypeAdapter(EnumMap.class, (InstanceCreator<EnumMap>) type -> {
Type[] types = (((ParameterizedType) type).getActualTypeArguments());
return new EnumMap((Class<?>) types[0]);
});
// Disable <>'s escaping etc.
builder.disableHtmlEscaping();
// Register adapter factory
builder.registerTypeAdapterFactory(new BentoboxTypeAdapterFactory(plugin));
gson = builder.create();
// Loaded tracker
blueprintsLoaded = new HashSet<>();
}
/**
* Extracts the blueprints and bundles provided by this {@link GameModeAddon} in its .jar file.
* This will do nothing if the blueprints folder already exists for this GameModeAddon.
*
* @param addon the {@link GameModeAddon} to extract the blueprints from.
*/
public void extractDefaultBlueprints(@NonNull GameModeAddon addon) {
File folder = getBlueprintsFolder(addon);
if (folder.exists()) {
// If the folder exists, do not copy anything from the jar
return;
}
if (!folder.exists() && !folder.mkdirs()) {
plugin.logError("Could not create the '" + FOLDER_NAME + "' folder!");
plugin.logError("This might be due to incorrectly set-up write permissions on the operating system.");
return;
}
// Get any blueprints or bundles from the jar and save them.
try (JarFile jar = new JarFile(addon.getFile())) {
Util.listJarFiles(jar, FOLDER_NAME, BLUEPRINT_BUNDLE_SUFFIX).forEach(name -> addon.saveResource(name, false));
Util.listJarFiles(jar, FOLDER_NAME, BLUEPRINT_SUFFIX).forEach(name -> addon.saveResource(name, false));
} catch (IOException e) {
plugin.logError("Could not load blueprint files from addon jar " + e.getMessage());
}
}
/**
* Get the blueprint bundles of this addon.
*
* @param addon the {@link GameModeAddon} to get the blueprint bundles.
*/
@NonNull
public Map<String, BlueprintBundle> getBlueprintBundles(@NonNull GameModeAddon addon) {
if (!blueprintBundles.containsKey(addon)) {
return new HashMap<>();
}
return blueprintBundles.get(addon).stream().collect(Collectors.toMap(BlueprintBundle::getUniqueId, b -> b));
}
/**
* Get the default blueprint bundle for game mode
* @param addon - game mode addon
* @return the default blueprint bundle or null if none
* @since 1.8.0
*/
@Nullable
public BlueprintBundle getDefaultBlueprintBundle(@NonNull GameModeAddon addon) {
if (blueprintBundles.containsKey(addon)) {
return blueprintBundles.get(addon).stream().filter(bb -> bb.getUniqueId().equals(DEFAULT_BUNDLE_NAME)).findFirst().orElse(null);
}
return null;
}
/**
* Returns a {@link File} instance of the blueprints folder of this {@link GameModeAddon}.
*
* @param addon the {@link GameModeAddon}
* @return a {@link File} instance of the blueprints folder of this GameModeAddon.
*/
@NonNull
private File getBlueprintsFolder(@NonNull GameModeAddon addon) {
return new File(addon.getDataFolder(), FOLDER_NAME);
}
/**
* Loads the blueprint bundles of this addon from its blueprints folder.
*
* @param addon the {@link GameModeAddon} to load the blueprints of.
*/
public void loadBlueprintBundles(@NonNull GameModeAddon addon) {
// Set loading flag
blueprintsLoaded.add(addon);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Load bundles
blueprintBundles.put(addon, new ArrayList<>());
if (!loadBundles(addon)) {
makeDefaults(addon);
loadBundles(addon);
}
// Load blueprints
loadBlueprints(addon);
// Clear loading flag
blueprintsLoaded.remove(addon);
});
}
/**
* Check if all blueprints are loaded. Only query after all GameModes have been loaded.
* @return true if all blueprints are loaded
*/
public boolean isBlueprintsLoaded() {
return blueprintsLoaded.isEmpty();
}
private boolean loadBundles(@NonNull GameModeAddon addon) {
File bpf = getBlueprintsFolder(addon);
if (!bpf.exists()) {
plugin.logError("There is no blueprint folder for addon " + addon.getDescription().getName());
bpf.mkdirs();
}
boolean loaded = false;
File[] bundles = bpf.listFiles((dir, name) -> name.endsWith(BLUEPRINT_BUNDLE_SUFFIX));
if (bundles == null || bundles.length == 0) {
return false;
}
for (File file : bundles) {
try (FileReader fileReader = new FileReader(file, StandardCharsets.UTF_8))
{
if (!file.getName().equals(Util.sanitizeInput(file.getName())))
{
// fail on all blueprints with incorrect names.
throw new InputMismatchException(file.getName());
}
BlueprintBundle bb = gson.fromJson(fileReader, BlueprintBundle.class);
if (bb != null) {
// Make sure there is no existing bundle with the same uniqueId
if (blueprintBundles.get(addon).stream().noneMatch(bundle -> bundle.getUniqueId().equals(bb.getUniqueId()))) {
blueprintBundles.get(addon).add(bb);
plugin.log("Loaded Blueprint Bundle '" + bb.getUniqueId() + FOR + addon.getDescription().getName() + ".");
loaded = true;
} else {
// There is a bundle that already uses this uniqueId.
// In that case, we log that and do not load the new bundle.
plugin.logWarning("Could not load blueprint bundle '" + file.getName() + FOR + addon.getDescription().getName() + ".");
plugin.logWarning("The uniqueId '" + bb.getUniqueId() + "' is already used by another Blueprint Bundle.");
plugin.logWarning("This can occur if the Blueprint Bundles' files were manually edited.");
plugin.logWarning("Please review your Blueprint Bundles' files and make sure their uniqueIds are not in duplicate.");
}
}
} catch (Exception e) {
plugin.logError("Could not load blueprint bundle '" + file.getName() + "'. Cause: " + e.getMessage() + ".");
plugin.logStacktrace(e);
}
}
return loaded;
}
private BlueprintBundle getDefaultBlueprintBundle() {
BlueprintBundle bb = new BlueprintBundle();
bb.setIcon(Material.PAPER);
bb.setUniqueId(DEFAULT_BUNDLE_NAME);
bb.setDisplayName("Default bundle");
bb.setDescription(Collections.singletonList(ChatColor.AQUA + "Default bundle of blueprints"));
return bb;
}
private Blueprint getDefaultBlueprint() {
Blueprint defaultBp = new Blueprint();
defaultBp.setName("bedrock");
defaultBp.setDescription(Collections.singletonList(ChatColor.AQUA + "A bedrock block"));
defaultBp.setBedrock(new Vector(0, 0, 0));
Map<Vector, BlueprintBlock> map = new HashMap<>();
map.put(new Vector(0, 0, 0), new BlueprintBlock("minecraft:bedrock"));
defaultBp.setBlocks(map);
return defaultBp;
}
/**
* This should never be needed and is just a boot strap
*
* @param addon addon
*/
private void makeDefaults(@NonNull GameModeAddon addon) {
plugin.logError("No blueprint bundles found! Creating a default one.");
BlueprintBundle bb = getDefaultBlueprintBundle();
// Default blueprints
Blueprint defaultBp = getDefaultBlueprint();
// Save a default "bedrock" blueprint
new BlueprintClipboardManager(plugin, getBlueprintsFolder(addon)).saveBlueprint(defaultBp);
// This blueprint is used for all environments
bb.setBlueprint(World.Environment.NORMAL, defaultBp);
bb.setBlueprint(World.Environment.NETHER, defaultBp);
bb.setBlueprint(World.Environment.THE_END, defaultBp);
bb.setUniqueId(DEFAULT_BUNDLE_NAME);
blueprintBundles.get(addon).add(bb);
this.saveBlueprintBundles();
}
/**
* Loads all the blueprints of this addon from its blueprints folder.
*
* @param addon the {@link GameModeAddon} to load the blueprints of.
*/
public void loadBlueprints(@NonNull GameModeAddon addon) {
blueprints.put(addon, new ArrayList<>());
File bpf = getBlueprintsFolder(addon);
if (!bpf.exists()) {
plugin.logError("There is no blueprint folder for addon " + addon.getDescription().getName());
bpf.mkdirs();
}
File[] bps = bpf.listFiles((dir, name) -> name.endsWith(BLUEPRINT_SUFFIX));
if (bps == null || bps.length == 0) {
plugin.logError("No blueprints found for " + addon.getDescription().getName());
return;
}
for (File file : bps) {
// Input sanitization is required for weirdos that edit files manually.
String fileName = Util.sanitizeInput(file.getName().substring(0, file.getName().length() - BLUEPRINT_SUFFIX.length()));
try {
Blueprint bp = new BlueprintClipboardManager(plugin, bpf).loadBlueprint(fileName);
bp.setName(fileName);
blueprints.get(addon).add(bp);
plugin.log("Loaded blueprint '" + bp.getName() + FOR + addon.getDescription().getName());
} catch (Exception e) {
plugin.logError("Could not load blueprint " + fileName + " " + e.getMessage());
plugin.logStacktrace(e);
}
}
}
/**
* Adds a blueprint to addon's list of blueprints. If the list already contains a blueprint with the same name
* it is replaced.
*
* @param addon - the {@link GameModeAddon}
* @param bp - blueprint
*/
public void addBlueprint(@NonNull GameModeAddon addon, @NonNull Blueprint bp) {
blueprints.putIfAbsent(addon, new ArrayList<>());
blueprints.get(addon).removeIf(b -> b.getName().equals(bp.getName()));
blueprints.get(addon).add(bp);
plugin.log("Added blueprint '" + bp.getName() + FOR + addon.getDescription().getName());
}
/**
* Saves a blueprint into addon's blueprint folder
*
* @param addon - the {@link GameModeAddon}
* @param bp - blueprint to save
*/
public boolean saveBlueprint(@NonNull GameModeAddon addon, @NonNull Blueprint bp) {
return new BlueprintClipboardManager(plugin, getBlueprintsFolder(addon)).saveBlueprint(bp);
}
/**
* Save blueprint bundle for game mode
*
* @param addon - gamemode addon
* @param bb blueprint bundle to save
*/
public void saveBlueprintBundle(GameModeAddon addon, BlueprintBundle bb) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
File bpf = getBlueprintsFolder(addon);
if (!bpf.exists()) {
bpf.mkdirs();
}
File fileName = new File(bpf, bb.getUniqueId() + BLUEPRINT_BUNDLE_SUFFIX);
String toStore = gson.toJson(bb, BlueprintBundle.class);
try (FileWriter fileWriter = new FileWriter(fileName, StandardCharsets.UTF_8)) {
fileWriter.write(toStore);
} catch (IOException e) {
plugin.logError("Could not save blueprint bundle file: " + e.getMessage());
}
});
}
/**
* Saves all the blueprint bundles
*/
public void saveBlueprintBundles() {
blueprintBundles.forEach((k, v) -> v.forEach(m -> saveBlueprintBundle(k, m)));
}
/**
* Get blueprints for this game mode
*
* @param addon - game mode addon
* @return Map of name and blueprint or empty map
*/
public Map<String, Blueprint> getBlueprints(GameModeAddon addon) {
if (!blueprints.containsKey(addon)) {
return new HashMap<>();
}
return blueprints.get(addon).stream().collect(Collectors.toMap(Blueprint::getName, b -> b));
}
/**
* Unregisters the Blueprint from the manager and deletes the file.
* @param addon game mode addon
* @param name name of the Blueprint to delete
* @since 1.9.0
*/
public void deleteBlueprint(GameModeAddon addon, String name)
{
List<Blueprint> addonBlueprints = this.blueprints.get(addon);
Iterator<Blueprint> it = addonBlueprints.iterator();
while (it.hasNext())
{
Blueprint b = it.next();
if (b.getName().equalsIgnoreCase(name))
{
it.remove();
File file = new File(this.getBlueprintsFolder(addon), b.getName() + BLUEPRINT_SUFFIX);
// Delete the file
try
{
Files.deleteIfExists(file.toPath());
}
catch (IOException e)
{
this.plugin.logError("Could not delete Blueprint " + e.getLocalizedMessage());
}
}
}
}
/**
* Paste the islands to world
*
* @param addon - GameModeAddon
* @param island - island
* @param name - bundle name
*/
public void paste(GameModeAddon addon, Island island, String name) {
paste(addon, island, name, null, true);
}
/**
* Paste islands to the world and run task afterwards
*
* @param addon - the game mode addon
* @param island - the island
* @param name - name of bundle to paste
* @param task - task to run after pasting is completed
* @param useNMS - true to use NMS pasting
* @return true if okay, false is there is a problem
*/
public boolean paste(GameModeAddon addon, Island island, String name, Runnable task, boolean useNMS) {
if (validate(addon, name) == null) {
plugin.logError("Tried to paste '" + name + "' but the bundle is not loaded!");
return false;
}
BlueprintBundle bb = getBlueprintBundles(addon).get(name.toLowerCase());
if (!blueprints.containsKey(addon) || blueprints.get(addon).isEmpty()) {
plugin.logError("No blueprints loaded for bundle '" + name + "'!");
return false;
}
Blueprint bp = getBlueprints(addon).get(bb.getBlueprint(World.Environment.NORMAL));
if (bp == null) {
// Oops, no overworld
bp = getBlueprints(addon).get("island");
plugin.logError("Blueprint bundle has no normal world blueprint, using default");
if (bp == null) {
plugin.logError("NO DEFAULT BLUEPRINT FOUND! Make sure 'island.blu' exists!");
}
}
// Paste
if (bp != null) {
new BlueprintPaster(plugin, bp, addon.getOverWorld(), island).paste(useNMS)
.thenAccept(b -> pasteNether(addon, bb, island).thenAccept(
b2 ->
pasteEnd(addon, bb, island).thenAccept(message -> sendMessage(island)).thenAccept(b3 -> Bukkit.getScheduler().runTask(plugin, task))));
}
// Set the bundle name
island.putMetaData("bundle", new MetaDataValue(name));
return true;
}
private CompletableFuture<Boolean> pasteNether(GameModeAddon addon, BlueprintBundle bb, Island island) {
if (bb.getBlueprint(World.Environment.NETHER) != null
&& addon.getWorldSettings().isNetherGenerate()
&& addon.getWorldSettings().isNetherIslands()
&& addon.getNetherWorld() != null) {
Blueprint bp = getBlueprints(addon).get(bb.getBlueprint(World.Environment.NETHER));
if (bp != null) {
return new BlueprintPaster(plugin, bp, addon.getNetherWorld(), island).paste();
}
}
return CompletableFuture.completedFuture(false);
}
private CompletableFuture<Boolean> pasteEnd(GameModeAddon addon, BlueprintBundle bb, Island island) {
// Make end island
if (bb.getBlueprint(World.Environment.THE_END) != null
&& addon.getWorldSettings().isEndGenerate()
&& addon.getWorldSettings().isEndIslands()
&& addon.getEndWorld() != null) {
Blueprint bp = getBlueprints(addon).get(bb.getBlueprint(World.Environment.THE_END));
if (bp != null) {
return new BlueprintPaster(plugin, bp, addon.getEndWorld(), island).paste();
}
}
return CompletableFuture.completedFuture(false);
}
/**
* This method just sends a message to the island owner that island creating is completed.
* @param island Island which owner must receive a message.
*/
private void sendMessage(Island island) {
if (island != null && island.getOwner() != null) {
final Optional<User> owner = Optional.of(island).map(i -> User.getInstance(i.getOwner()));
owner.ifPresent(user -> user.sendMessage("commands.island.create.pasting.done"));
}
}
/**
* Validate if the bundle name is valid or not
*
* @param addon - game mode addon
* @param name - bundle name
* @return bundle name or null if it's invalid
*/
public @Nullable String validate(GameModeAddon addon, String name) {
if (name == null) {
return null;
}
if (blueprintBundles.containsKey(addon) && getBlueprintBundles(addon).containsKey(name)) {
return name;
}
return null;
}
/**
* Adds a blueprint bundle. If a bundle with the same uniqueId exists, it will be replaced
*
* @param addon - the game mode addon
* @param bb - the blueprint bundle
*/
public void addBlueprintBundle(GameModeAddon addon, BlueprintBundle bb) {
if (blueprintBundles.containsKey(addon)) {
// Remove any bundles with the same name
blueprintBundles.get(addon).removeIf(b -> b.getUniqueId().equals(bb.getUniqueId()));
}
blueprintBundles.computeIfAbsent(addon, k -> new ArrayList<>()).add(bb);
}
/**
* Checks if a player has permission to see or use this blueprint bundle.
*
* @param addon - addon making the request
* @param user - user making the request
* @param name - name of the blueprint bundle
* @return <tt>true</tt> if allowed, <tt>false</tt> if not or bundle does not exist
*/
public boolean checkPerm(@NonNull Addon addon, @NonNull User user, @NonNull String name) {
// Permission
String permission = addon.getPermissionPrefix() + "island.create." + name;
// Get Blueprint bundle
BlueprintBundle bb = getBlueprintBundles((GameModeAddon) addon).get(name.toLowerCase());
if (bb == null || (bb.isRequirePermission() && !name.equals(DEFAULT_BUNDLE_NAME) && !user.hasPermission(permission))) {
user.sendMessage("general.errors.no-permission", TextVariables.PERMISSION, permission);
return false;
}
return true;
}
/**
* Removes a blueprint bundle
*
* @param addon - Game Mode Addon
* @param bb - Blueprint Bundle to delete
*/
public void deleteBlueprintBundle(@NonNull GameModeAddon addon, BlueprintBundle bb) {
if (blueprintBundles.containsKey(addon)) {
blueprintBundles.get(addon).removeIf(k -> k.getUniqueId().equals(bb.getUniqueId()));
}
File bpf = getBlueprintsFolder(addon);
File fileName = new File(bpf, bb.getUniqueId() + BLUEPRINT_BUNDLE_SUFFIX);
try {
Files.deleteIfExists(fileName.toPath());
} catch (IOException e) {
plugin.logError("Could not delete Blueprint Bundle " + e.getLocalizedMessage());
}
}
/**
* Rename a blueprint
*
* @param addon - Game Mode Addon
* @param bp - blueprint
* @param name - new name
* @param displayName - display name for blueprint
*/
public void renameBlueprint(GameModeAddon addon, Blueprint bp, String name, String displayName)
{
if (bp.getName().equalsIgnoreCase(name))
{
// If the name is the same, do not do anything
return;
}
File bpf = this.getBlueprintsFolder(addon);
// Get the filename
File fileName = new File(bpf, bp.getName() + BLUEPRINT_SUFFIX);
// Delete the old file
try
{
Files.deleteIfExists(fileName.toPath());
}
catch (IOException e)
{
this.plugin.logError("Could not delete old Blueprint " + e.getLocalizedMessage());
}
// Remove blueprint from the blueprints.
this.blueprints.get(addon).remove(bp);
// Set new name
bp.setName(name);
bp.setDisplayName(displayName);
// Save it
this.saveBlueprint(addon, bp);
this.addBlueprint(addon, bp);
}
}