package tc.oc.pgm.match; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.World; import org.bukkit.configuration.Configuration; import org.bukkit.event.EventBus; import tc.oc.api.util.Permissions; import tc.oc.commons.core.logging.Loggers; import tc.oc.pgm.development.MapErrorTracker; import tc.oc.pgm.events.SetNextMapEvent; import tc.oc.pgm.map.MapLibrary; import tc.oc.pgm.map.MapLoader; import tc.oc.pgm.map.MapNotFoundException; import tc.oc.pgm.map.PGMMap; import tc.oc.pgm.rotation.FileRotationProviderFactory; import tc.oc.pgm.rotation.RotationManager; import tc.oc.pgm.rotation.RotationState; @Singleton public class MatchManager implements MatchFinder { // Maximum randomly selected maps to attempt to load from the library before giving up private static final int MAX_CYCLE_TRIES = 10; private final Logger log; private final Path pluginDataFolder; private final Provider<Configuration> config; private final MapLibrary mapLibrary; private final MapLoader mapLoader; private final MapErrorTracker mapErrorTracker; private final FileRotationProviderFactory fileRotationProviderFactory; private final EventBus eventBus; private final MatchLoader matchLoader; private @Nullable RotationManager rotationManager; /** Custom set next map. */ private PGMMap nextMap = null; /** * Creates a new map manager with a specified map rotation. */ @Inject MatchManager(Loggers loggers, @Named("pluginData") Path pluginDataFolder, Provider<Configuration> config, MapLibrary mapLibrary, MapLoader mapLoader, MapErrorTracker mapErrorTracker, FileRotationProviderFactory fileRotationProviderFactory, EventBus eventBus, MatchLoader matchLoader) throws MapNotFoundException { this.pluginDataFolder = pluginDataFolder; this.mapErrorTracker = mapErrorTracker; this.fileRotationProviderFactory = fileRotationProviderFactory; this.log = loggers.get(getClass()); this.config = config; this.mapLibrary = mapLibrary; this.mapLoader = mapLoader; this.eventBus = eventBus; this.matchLoader = matchLoader; } @Override public Map<World, Match> matchesByWorld() { return matchLoader.matchesByWorld(); } /** Gets the currently loaded maps. */ public Collection<PGMMap> getMaps() { return this.mapLibrary.getMaps(); } public Set<PGMMap> loadNewMaps() throws MapNotFoundException { log.info("Loading maps..."); Set<Path> added = new HashSet<>(), updated = new HashSet<>(), removed = new HashSet<>(); List<PGMMap> maps = mapLoader.loadNewMaps(mapLibrary.getMapsByPath(), added, updated, removed); mapLibrary.removeMaps(removed); Set<PGMMap> newMaps = mapLibrary.addMaps(maps); mapLibrary.pushDirtyMaps(); log.info("Loaded " + newMaps.size() + " maps"); if(mapLibrary.getMaps().isEmpty()) { throw new MapNotFoundException(); } return newMaps; } public boolean loadRotations() { return getRotationManager().load(mapLibrary.getMaps().iterator().next()); } public Set<PGMMap> loadMapsAndRotations() throws MapNotFoundException { Set<PGMMap> maps = loadNewMaps(); loadRotations(); return maps; } public RotationManager getRotationManager() { if(rotationManager == null) { rotationManager = new RotationManager( log, config.get(), mapLibrary.getMaps().iterator().next(), fileRotationProviderFactory.parse( mapLibrary, pluginDataFolder, config.get() ) ); } return this.rotationManager; } /** * Gets the next map that will be loaded at this point in time. If a map * had been specified explicitly (setNextMap) that will be returned, * otherwise the next map in the rotation will be returned. * * @return Next map that would be loaded at this point in time. */ public PGMMap getNextMap() { if(this.nextMap == null) { return this.getRotationManager().getRotation().getNext(); } else { return this.nextMap; } } /** * @return the number of cycles before the rotation starts repeating */ public int cyclesBeforeRepeat() { int size = getRotationManager().getRotation().getMaps().size(); if(nextMap != null) size++; // Extra map is available due to /setnext return size; } private PGMMap advanceRotation() { PGMMap currentMap; if(nextMap == null) { RotationState rotation = getRotationManager().getRotation(); currentMap = rotation.getNext(); rotation = rotation.skip(1); getRotationManager().setRotation(rotation); } else { currentMap = nextMap; this.nextMap = null; } eventBus.callEvent(new SetNextMapEvent(getNextMap())); return currentMap; } /** * Specified an explicit map for the next cycle. * * @param map to be loaded next. */ public void setNextMap(PGMMap map) { if(map != nextMap) { this.nextMap = map; eventBus.callEvent(new SetNextMapEvent(map)); } } /** * Cycle to the next map in the rotation * @param oldMatch The current match, if any * @param retryRotation Try every map in the rotation until one loads successfully * @param retryLibrary Try every map in the library, after trying the entire rotation * @return The new match, or null if no map could be loaded */ public @Nullable Match cycleToNext(@Nullable Match oldMatch, boolean retryRotation, boolean retryLibrary) { // Match unload also does this, but doing it earlier avoids some problems. // Specifically, RestartCountdown cannot cancel itself during a cycle. if(oldMatch != null) { oldMatch.countdowns().cancelAll(); } Set<PGMMap> failed = new HashSet<>(); // Don't try any map more than once // Try to load a rotation map int maxCycles = cyclesBeforeRepeat(); for(int cycles = 0; cycles < maxCycles; cycles++) { PGMMap map = advanceRotation(); if(!failed.contains(map)) { Match match = cycleTo(oldMatch, map); if(match != null) return match; } // If retryRotation is false, give up after the first failure if(!retryRotation) return null; failed.add(map); } // If all rotation maps failed, and we're not allowed to try non-rotation maps, give up if(!retryLibrary) return null; // Try every map in the library, in random order, to avoid getting stuck in a folder full of broken maps final List<PGMMap> maps = new ArrayList<>(mapLibrary.getMaps()); maps.removeAll(failed); Collections.shuffle(maps); int tries = 0; for(PGMMap map : maps) { if(++tries >= MAX_CYCLE_TRIES) return null; Match match = cycleTo(oldMatch, map); if(match != null) return match; failed.add(map); } return null; } private @Nullable Match cycleTo(@Nullable Match oldMatch, PGMMap map) { try { mapErrorTracker.clearErrors(map); if(map.shouldReload()) { Bukkit.broadcast(ChatColor.GREEN + "XML changes detected, reloading", Permissions.MAPERRORS); mapLoader.loadMap(map); mapLibrary.pushDirtyMaps(); } return matchLoader.cycleTo(oldMatch, map); } catch(MapNotFoundException e) { // Maps are sometimes removed, must handle it gracefully log.warning("Skipping deleted map " + map.getName()); try { loadMapsAndRotations(); } catch(MapNotFoundException e2) { log.severe("No maps could be loaded, server cannot cycle"); } return null; } } }