package net.knarcraft.stargate.utility; import net.knarcraft.stargate.Stargate; import net.knarcraft.stargate.container.BlockChangeRequest; import net.knarcraft.stargate.container.ControlBlockUpdateRequest; import net.knarcraft.stargate.portal.Portal; import net.knarcraft.stargate.portal.PortalRegistry; import net.knarcraft.stargate.portal.property.PortalLocation; import net.knarcraft.stargate.portal.property.PortalOptions; import net.knarcraft.stargate.portal.property.PortalOwner; import net.knarcraft.stargate.portal.property.PortalStrings; import net.knarcraft.stargate.portal.property.gate.Gate; import net.knarcraft.stargate.portal.property.gate.GateHandler; import net.knarcraft.stargate.transformation.SimpleVectorOperation; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Directional; import org.bukkit.block.data.Waterlogged; import org.bukkit.util.BlockVector; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.List; import java.util.Scanner; import static net.knarcraft.stargate.portal.PortalSignDrawer.markPortalWithInvalidGate; /** * Helper class for saving and loading portal save files */ public final class PortalFileHelper { private PortalFileHelper() { } /** * Saves all portals for the given world * * @param world

The world to save portals for

*/ public static void saveAllPortals(@NotNull World world) { Stargate.getStargateConfig().addManagedWorld(world.getName()); String saveFileLocation = Stargate.getPortalFolder() + "/" + world.getName() + ".db"; try { BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(saveFileLocation, false)); for (Portal portal : PortalRegistry.getAllPortals()) { //Skip portals in other worlds String worldName = portal.getLocation().getWorld().getName(); if (!worldName.equalsIgnoreCase(world.getName())) { continue; } //Save the portal savePortal(bufferedWriter, portal); } bufferedWriter.close(); } catch (Exception exception) { Stargate.logSevere(String.format("Exception while writing stargates to %s: %s", saveFileLocation, exception)); } } /** * Saves one portal * * @param bufferedWriter

The buffered writer to write to

* @param portal

The portal to save

* @throws IOException

If unable to write to the buffered writer

*/ private static void savePortal(@NotNull BufferedWriter bufferedWriter, @NotNull Portal portal) throws IOException { StringBuilder builder = new StringBuilder(); Block button = portal.getStructure().getButton(); //WARNING: Because of the primitive save format, any change in order will break everything! builder.append(portal.getName()).append(':'); builder.append(portal.getLocation().getSignBlock()).append(':'); builder.append((button != null) ? button.toString() : "").append(':'); //Add removes config values to keep indices consistent builder.append(0).append(':'); builder.append(0).append(':'); builder.append(DirectionHelper.getYawFromBlockFace(portal.getLocation().getFacing())).append(':'); builder.append(portal.getLocation().getTopLeft()).append(':'); builder.append(portal.getGate().getFilename()).append(':'); //Only save the destination name if the gate is fixed as it doesn't matter otherwise builder.append(portal.getOptions().isFixed() ? portal.getDestinationName() : "").append(':'); builder.append(portal.getNetwork()).append(':'); //Name is saved as a fallback if the UUID is unavailable builder.append(portal.getOwner().getIdentifier()); //Save all the portal options savePortalOptions(portal, builder); bufferedWriter.append(builder.toString()); bufferedWriter.newLine(); } /** * Saves all portal options for the given portal * * @param portal

The portal to save

* @param builder

The string builder to append to

*/ private static void savePortalOptions(@NotNull Portal portal, @NotNull StringBuilder builder) { PortalOptions options = portal.getOptions(); builder.append(':'); builder.append(options.isHidden()).append(':'); builder.append(options.isAlwaysOn()).append(':'); builder.append(options.isPrivate()).append(':'); builder.append(portal.getLocation().getWorld().getName()).append(':'); builder.append(options.isFree()).append(':'); builder.append(options.isBackwards()).append(':'); builder.append(options.isShown()).append(':'); builder.append(options.isNoNetwork()).append(':'); builder.append(options.isRandom()).append(':'); builder.append(options.isBungee()).append(':'); builder.append(options.isQuiet()).append(':'); builder.append(options.hasNoSign()); } /** * Loads all portals for the given world * * @param world

The world to load portals for

* @return

True if portals could be loaded

*/ public static boolean loadAllPortals(@NotNull World world) { String location = Stargate.getPortalFolder(); File database = new File(location, world.getName() + ".db"); if (database.exists()) { return loadPortals(world, database); } else { Stargate.logInfo(String.format("{%s} No stargates for world ", world.getName())); } return false; } /** * Loads all the given portals * * @param world

The world to load portals for

* @param database

The database file containing the portals

* @return

True if the portals were loaded successfully

*/ private static boolean loadPortals(@NotNull World world, @NotNull File database) { int lineIndex = 0; try { Scanner scanner = new Scanner(database); boolean needsToSaveDatabase = false; while (scanner.hasNextLine()) { //Read the line and do whatever needs to be done needsToSaveDatabase = readPortalLine(scanner, ++lineIndex, world) || needsToSaveDatabase; } scanner.close(); //Do necessary tasks after all portals have loaded Stargate.debug("PortalFileHelper::loadPortals", String.format("Finished loading portals for %s. " + "Starting post loading tasks", world)); doPostLoadTasks(world, needsToSaveDatabase); return true; } catch (Exception exception) { Stargate.logSevere(String.format("Exception while reading stargates from %s: %d! Message: %s", database.getName(), lineIndex, exception.getMessage())); } return false; } /** * Reads one file line containing information about one portal * * @param scanner

The scanner to read

* @param lineIndex

The index of the read line

* @param world

The world for which portals are currently being read

* @return

True if the read portal has changed and the world's database needs to be saved

*/ private static boolean readPortalLine(@NotNull Scanner scanner, int lineIndex, @NotNull World world) { String line = scanner.nextLine().trim(); //Ignore empty and comment lines if (line.startsWith("#") || line.isEmpty()) { return false; } //Check if the min. required portal data is present String[] portalData = line.split(":"); if (portalData.length < 8) { Stargate.logInfo(String.format("Invalid line - %s", lineIndex)); return false; } //Load the portal defined in the current line return loadPortal(portalData, world, lineIndex); } /** * Performs tasks which must be run after portals have loaded * *

This will open always on portals, print info about loaded stargates and re-draw portal signs for loaded * portals.

* * @param world

The world portals have been loaded for

* @param needsToSaveDatabase

Whether the portal database's file needs to be updated

*/ private static void doPostLoadTasks(@NotNull World world, boolean needsToSaveDatabase) { //Open any always-on portals. Do this here as it should be more efficient than in the loop. PortalUtil.verifyAllPortals(); int portalCount = PortalRegistry.getAllPortals().size(); int openCount = PortalUtil.openAlwaysOpenPortals(); //Print info about loaded stargates so that admins can see if all stargates loaded Stargate.logInfo(String.format("{%s} Loaded %d stargates with %d set as always-on", world.getName(), portalCount, openCount)); //Re-draw the signs in case a bug in the config prevented the portal from loading and has been fixed since Stargate.debug("PortalFileHelper::doPostLoadTasks::update", String.format("Queueing portal sign/button updates for %s", world)); for (Portal portal : PortalRegistry.getAllPortals()) { if (portal.isRegistered() && portal.getLocation().getWorld().equals(world) && world.getWorldBorder().isInside(portal.getLocation().getSignBlock().getLocation())) { Stargate.addControlBlockUpdateRequest(new ControlBlockUpdateRequest(portal)); Stargate.debug("UpdateSignsButtons", String.format("Queued sign and button updates for portal %s", portal.getName())); } } //Save the portals to disk to update with any changes Stargate.debug("PortalFileHelper::doPostLoadTasks", String.format("Saving database for world %s", world)); if (needsToSaveDatabase) { saveAllPortals(world); } } /** * Loads one portal from a data array * * @param portalData

The array describing the portal

* @param world

The world to create the portal in

* @param lineIndex

The line index to report in case the user needs to fix an error

* @return

True if the portal's data has changed and its database needs to be updated

*/ private static boolean loadPortal(@NotNull String[] portalData, @NotNull World world, int lineIndex) { //Load min. required portal data String name = portalData[0]; Block button = (!portalData[2].isEmpty()) ? getBlock(world, portalData[2]) : null; //Load the portal's location PortalLocation portalLocation = new PortalLocation(getBlock(world, portalData[6]), DirectionHelper.getBlockFaceFromYaw(Float.parseFloat(portalData[5])), getBlock(world, portalData[1]), button); //Check if the portal's gate type exists and is loaded Gate gate = GateHandler.getGateByName(portalData[7]); if (gate == null) { //Mark the sign as invalid to reduce some player confusion markPortalWithInvalidGate(portalLocation, portalData[7], lineIndex); return false; } //Load extra portal data String destination = (portalData.length > 8) ? portalData[8] : ""; String network = (portalData.length > 9 && !portalData[9].isEmpty()) ? portalData[9] : Stargate.getDefaultNetwork(); String ownerString = (portalData.length > 10) ? portalData[10] : ""; //Get the owner from the owner string PortalOwner owner = new PortalOwner(ownerString); //Create the new portal PortalStrings portalStrings = new PortalStrings(name, network, destination); Portal portal = new Portal(portalLocation, button, portalStrings, gate, owner, PortalUtil.getPortalOptions(portalData)); //Register the portal, and close it in case it wasn't properly closed when the server stopped boolean buttonLocationChanged = updateButtonVector(portal); PortalUtil.registerPortal(portal); portal.getPortalOpener().closePortal(true); return buttonLocationChanged; } /** * Decides the material to use for removing a portal's button/sign * * @param block

The location of the button/sign to replace

* @param portal

The portal the button/sign belongs to

* @return

The material to use for removing the button/sign

*/ @NotNull public static Material decideRemovalMaterial(@NotNull Block block, @NotNull Portal portal) { //Get the blocks to each side of the location SimpleVectorOperation vectorOperation = portal.getLocation().getVectorOperation(); Location leftLocation = block.getLocation().clone().add(vectorOperation.performToRealSpaceOperation(new Vector(-1, 0, 0))); Location rightLocation = block.getLocation().clone().add(vectorOperation.performToRealSpaceOperation(new Vector(1, 0, 0))); //If the block is water or is waterlogged, assume the portal is underwater if (isUnderwater(leftLocation) || isUnderwater(rightLocation)) { return Material.WATER; } else { return Material.AIR; } } /** * Checks whether the given location is underwater * *

If the location has a water block, or a block which is waterlogged, it will be considered underwater.

* * @param location

The location to check

* @return

True if the location is underwater

*/ private static boolean isUnderwater(@NotNull Location location) { BlockData blockData = location.getBlock().getBlockData(); return blockData.getMaterial() == Material.WATER || (blockData instanceof Waterlogged waterlogged && waterlogged.isWaterlogged()); } /** * Updates the button vector for the given portal * *

As the button vector isn't saved, it is null when the portal is loaded. This method allows it to be * explicitly set when necessary.

* * @param portal

The portal to update the button vector for

* @return

True if the calculated button location is not the same as the one in the portal file

*/ private static boolean updateButtonVector(@NotNull Portal portal) { for (BlockVector control : portal.getGate().getLayout().getControls()) { Block buttonLocation = portal.getLocation().getRelative(control.clone().add(new Vector(0, 0, 1)).toBlockVector()); if (!buttonLocation.equals(portal.getLocation().getSignBlock())) { portal.getLocation().setButtonBlock(buttonLocation); Block oldButtonLocation = portal.getStructure().getButton(); if (oldButtonLocation != null && !oldButtonLocation.equals(buttonLocation)) { Stargate.addControlBlockUpdateRequest(new BlockChangeRequest(oldButtonLocation, Material.AIR, null)); portal.getStructure().setButton(buttonLocation); return true; } } } return false; } /** * Generates a button for a portal * * @param portal

The portal to generate button for

* @param buttonFacing

The direction the button should be facing

*/ public static void generatePortalButton(@NotNull Portal portal, @NotNull BlockFace buttonFacing) { //Go one block outwards to find the button's location rather than the control block's location Block button = portal.getLocation().getButtonBlock(); // If the button location is null here, it is assumed that the button generation wasn't necessary if (button == null) { return; } if (!MaterialHelper.isButtonCompatible(button.getType())) { @NotNull List possibleMaterials = MaterialHelper.specifiersToMaterials( portal.getGate().getPortalButtonMaterials()).stream().toList(); Material buttonType = ListHelper.getRandom(possibleMaterials); Directional buttonData = (Directional) Bukkit.createBlockData(buttonType); buttonData.setFacing(buttonFacing); button.setBlockData(buttonData); } portal.getStructure().setButton(button); } /** * Gets the block specified in the input * * @param world

The world the block belongs to

* @param string

Comma-separated coordinate string

* @return

The specified block

* @throws NumberFormatException

If non-numeric values are encountered

*/ @NotNull private static Block getBlock(@NotNull World world, @NotNull String string) throws NumberFormatException { String[] parts = string.split(","); return new Location(world, Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])).getBlock(); } }