package tc.oc.pgm.match;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import com.google.common.collect.ImmutableSet;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventBus;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.pgm.events.CycleEvent;
import tc.oc.pgm.events.MapArchiveEvent;
import tc.oc.pgm.map.MapNotFoundException;
import tc.oc.pgm.map.PGMMap;
import tc.oc.pgm.module.ModuleLoadException;
import tc.oc.pgm.terrain.WorldManager;
/**
* Server-scoped object that creates/cycles/unloads matches
* and tracks currently loaded matches by world.
*
* This class does NOT make any decisions about when to cycle
* or what maps to use. Some other object does that and calls
* {@link #cycleTo(Match, PGMMap)} on this class.
*/
@Singleton
public class MatchLoader implements MatchFinder {
private final Logger log;
private final Provider<WorldManager> worldManager;
private final Provider<MatchInjectionScope> matchInjectionScope;
private final Provider<Match> matchProvider;
private final MatchCounter matchCounter;
private final EventBus eventBus;
/** Matches that are currently running. */
private final Map<World, Match> matches = new HashMap<World, Match>();
@Inject MatchLoader(Loggers loggers,
Provider<WorldManager> worldManager,
Provider<MatchInjectionScope> matchInjectionScope,
Provider<Match> matchProvider,
MatchCounter matchCounter,
EventBus eventBus) {
this.log = loggers.get(getClass());
this.worldManager = worldManager;
this.matchInjectionScope = matchInjectionScope;
this.matchProvider = matchProvider;
this.matchCounter = matchCounter;
this.eventBus = eventBus;
}
@Override
public Map<World, Match> matchesByWorld() {
return matches;
}
/**
* Force all {@link Match}es to end and unload. This should only be
* called immediately before the server shuts down as it will leave
* PGM in a completely dysfunctional state.
*/
public void unloadAllMatches() {
for(Match match : ImmutableSet.copyOf(this.matches.values())) {
unloadMatch(match);
}
}
/**
* Call {@link #cycleToUnsafe(Match, PGMMap)} and log any exceptions to the map logger.
* @return the new match, or null if loading failed
*/
public @Nullable Match cycleTo(@Nullable Match oldMatch, PGMMap newMap) throws MapNotFoundException {
try {
return this.cycleToUnsafe(oldMatch, newMap);
} catch(MapNotFoundException e) {
throw e;
} catch(ModuleLoadException e) {
if(e.module() != null) {
newMap.getLogger().log(Level.SEVERE, "Exception loading module " + e.module().getSimpleName() + ": " + e.getMessage(), e);
} else {
newMap.getLogger().log(Level.SEVERE, "Exception loading map: " + e.getMessage(), e);
}
} catch(Throwable e) {
log.log(Level.SEVERE, e.getClass().getSimpleName() + " cycling to " + newMap.getName(), e);
newMap.getLogger().log(Level.SEVERE, "Internal error cycling to " + newMap.getName() + " (" + e + ")", e);
}
return null;
}
/**
* Creates and loads a new {@link Match} on the given map, optionally unloading an old
* match and transferring all players to the new one.
*
* @param oldMatch if given, this match is unloaded and all players are transferred to the new match
* @param newMap the map to load for the new match
* @return the newly loaded {@link Match}
* @throws Throwable generally, any exceptions thrown during loading/unloading are propagated
*/
private Match cycleToUnsafe(@Nullable Match oldMatch, PGMMap newMap) throws Throwable {
this.log.info("Cycling to " + newMap.toString());
// End the old match if it's still running
if(oldMatch != null) oldMatch.ensureNotRunning();
// Create and load the new match.
// Starting here, there are two "current" matches. While that is true,
// we have to explicitly specify a current match during any calls that
// might try to acquire it, either by calling one of the getCurrentMatch
// methods in this class, or injecting a @MatchScoped dependency.
final Match newMatch = loadMatch(newMap);
if(oldMatch != null) {
// Build a list of players to move from the old match to the new one.
// Ensure player is online before moving them. This should clean up
// any mess caused by a previous failure to remove them.
final List<Player> players = oldMatch.players()
.filter(MatchPlayer::isOnline)
.map(MatchPlayer::getBukkit)
.collect(Collectors.toCollection(ArrayList::new));
// Add players to the new match in random order, to avoid any
// repetition between matches when auto-join is enabled.
Collections.shuffle(players);
// Teleport players between worlds
for(Player player : players) {
player.teleport(newMatch.getWorld().getSpawnLocation());
player.setArrowsStuck(0);
}
// Remove players from the old match
oldMatch.asCurrentScope(oldMatch::removeAllPlayers);
// Add them to the new one
newMatch.asCurrentScope(() -> newMatch.addAllPlayers(players.stream()));
// Unload the old match.
// After this method returns, there is a single "current match",
// and we don't need to specify it explicitly any more.
unloadMatch(oldMatch);
}
eventBus.callEvent(new CycleEvent(newMatch, oldMatch));
log.info("Loaded " + newMap.toString());
return newMatch;
}
/**
* Try to load a match. This will take care of copying and loading the
* new world into Bukkit. May throw an assortment of exceptions if
* something goes wrong.
*
* @param map Map to load.
*/
private Match loadMatch(PGMMap map) throws Throwable {
return map.getContext().asCurrentScope(() -> {
final WorldManager worldManager = this.worldManager.get();
final String worldName = Match.createSlug(matchCounter.get());
final World world = worldManager.createWorld(worldName);
return matchInjectionScope.get().withNewStore(world, () -> {
final Match match = matchProvider.get();
this.matches.put(world, match);
try {
match.load();
return match;
} catch(Throwable e) {
this.matches.remove(world);
worldManager.unloadWorld(world);
throw e;
}
});
});
}
/**
* Unload match modules, unload the world, and archive it
*/
private void unloadMatch(Match match) {
match.asCurrentScope(() -> {
match.ensureNotRunning();
match.removeAllPlayers();
match.unload();
final WorldManager worldManager = this.worldManager.get();
worldManager.unloadWorld(match.getWorld());
MapArchiveEvent archiveEvent = new MapArchiveEvent(match, null);
eventBus.callEvent(archiveEvent);
worldManager.destroyWorld(match.getWorld().getName(), archiveEvent.getOutputDirectory());
this.matches.remove(match.getWorld());
});
}
}