package tc.oc.pgm.map; import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Provider; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import com.google.common.io.Files; import com.google.inject.Injector; import tc.oc.commons.core.exception.ExceptionHandler; import tc.oc.pgm.map.inject.MapInjectionScope; import tc.oc.pgm.module.ModuleLoadException; import tc.oc.pgm.xml.UnsupportedMapProtocolException; import static tc.oc.commons.core.inject.Injection.unwrappingExceptions; /** * Base class for an XML configured map. * * This class is responsible for parsing an XML file into a * {@link MapModuleContext}, detecting changes that require a reload, * and creation of a child {@link Injector} with bindings specific to * the map. * * This class is relatively decoupled from the rest of PGM, so that it can * potentially be used to integrate PGM features into other applications, * such as the lobby, or a non-match based game, e.g. an MMO. * * Anything related specifically to matches should be in {@link PGMMap}. */ public class MapDefinition { @Inject private MapConfiguration configuration; @Inject private Provider<MapModuleContext> contextProvider; @Inject private ExceptionHandler exceptionHandler; @Inject private MapInjectionScope mapInjectionScope; private MapLogger logger; @Inject void init(MapLogger.Factory loggerFactory) { this.logger = loggerFactory.create(this); } private final MapFolder folder; protected @Nullable MapModuleContext context; protected MapDefinition(MapFolder folder) { this.folder = folder; } public MapLogger getLogger() { return logger; } public MapFolder getFolder() { return folder; } public Optional<String> getThumbnailUri() { return getFolder().getThumbnailUri(); } public String getDottedPath() { return Joiner.on(".").join(getFolder().getRelativePath()); } public String getName() { return getFolder().getRelativePath().toString(); } public boolean isLoaded() { return context != null; } public MapModuleContext getContext() { if(context == null) { throw new IllegalStateException("Map is not loaded: " + this); } return context; } public boolean shouldReload() { if(context == null) return true; if(!configuration.autoReload()) return false; if(context.loadedFiles().isEmpty()) return configuration.reloadWhenError(); try { for(Map.Entry<Path, HashCode> loaded : context.loadedFiles().entrySet()) { HashCode latest = Files.hash(loaded.getKey().toFile(), Hashing.sha256()); if(!latest.equals(loaded.getValue())) return true; } return false; } catch (IOException e) { return true; } } public boolean reload() throws MapNotFoundException { List<? extends ModuleLoadException> errors; try { final MapModuleContext newContext = mapInjectionScope.withNewStore(this, () -> { final MapModuleContext context = unwrappingExceptions(ModuleLoadException.class, contextProvider); context.load(); return context; }); if(!newContext.hasErrors()) { this.context = newContext; return true; } errors = newContext.getErrors(); } catch(MapNotFoundException e) { throw e; } catch(UnsupportedMapProtocolException e) { logger.warning("Skipping map with unsupported proto " + e.getProto()); errors = ImmutableList.of(); } catch(ModuleLoadException e) { errors = ImmutableList.of(e); } catch(Throwable e) { exceptionHandler.handleException(e); errors = ImmutableList.of(new ModuleLoadException("Internal error", e)); } for(ModuleLoadException error : errors) { logger.log(new MapLogRecord(this, error)); } return false; } }