/*******************************************************************************
* Copyright (C) 2014 Travis Ralston (turt2live)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package com.turt2live.antishare.engine;
import com.turt2live.antishare.configuration.Configuration;
import com.turt2live.antishare.configuration.InventoryMergeSettings;
import com.turt2live.antishare.configuration.MemoryConfiguration;
import com.turt2live.antishare.configuration.groups.GroupManager;
import com.turt2live.antishare.events.EventDispatcher;
import com.turt2live.antishare.events.engine.EngineShutdownEvent;
import com.turt2live.antishare.events.worldengine.WorldEngineCreateEvent;
import com.turt2live.antishare.io.InventoryManager;
import com.turt2live.antishare.io.memory.MemoryInventoryManager;
import com.turt2live.antishare.object.AInventory;
import com.turt2live.antishare.object.APlayer;
import com.turt2live.antishare.object.AWorld;
import com.turt2live.antishare.object.pattern.PatternManager;
import com.turt2live.lib.items.AbstractedItem;
import com.turt2live.lib.items.provider.ItemProvider;
import com.turt2live.lib.items.provider.ProviderManager;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;
/**
* Represents the AntiShare engine
*
* @author turt2live
*/
public final class Engine {
// TODO: Cleanup configuration system
/**
* The default cache increment (60 seconds)
*/
public static final long DEFAULT_CACHE_INCREMENT = 60000; // 60 seconds
/**
* The default cache maximum time (120 seconds)
*/
public static final long DEFAULT_CACHE_MAXIMUM = 120000; // 120 seconds
/**
* The default save interval (0, off)
*/
public static final long DEFAULT_SAVE_INTERVAL = 0; // Default no save
/**
* Configuration key for 'break attachments as placed'
*/
public static final String CONFIG_BREAK_ATTACHMENTS_AS_PLACED = "blocks.attachemnts.break-as-placed";
/**
* Configuration key for 'deny break if attachment type mismatch'
*/
public static final String CONFIG_MISMATCHED_ATTACHMENTS_DENY = "blocks.attachments.deny-break";
/**
* Configuration key for 'block spread with gamemode'
*/
public static final String CONFIG_PHYSICS_GROW_WITH_GAMEMODE = "blocks.physics.grow-with-gamemode";
/**
* Configuration key for 'random break as gamemode'
*/
public static final String CONFIG_PHYSICS_BREAK_AS_GAMEMODE = "blocks.physics.block-item-drop";
/**
* Configuration key for 'hopper mismatch type transfer'
*/
public static final String CONFIG_HOPPER_MISMATCH_INTERACTION = "blocks.hoppers.deny-mixed";
/**
* Configuration key for 'deny pistons if lineup mismatch'
*/
public static final String CONFIG_PISTON_MISMATCH = "blocks.pistons.deny-mismatch";
/**
* Configuration key for 'classic mode interaction' handling
*/
public static final String CONFIG_INTERACT_CLASSIC_MODE = "blocks.interaction.classic-mode";
/**
* Configuration key for 'if not classic mode, can creative players open natural containers?'
* <p/>
* This will be 'true' to ALLOW the interaction.
*/
public static final String CONFIG_INTERACT_NATURAL_CONTAINERS = "blocks.interaction.creative-natural-containers";
/**
* Configuration key for 'if not classic mode, will containers inherit gamemodes from players?'
*/
public static final String CONFIG_INTERACT_CONTAINER_INHERIT = "blocks.interaction.natural-container-absorb-gamemode";
/**
* Configuration key for 'can players attack cross gamemode?'
*/
public static final String CONFIG_ENTITIES_CROSS_GAMEMODE_ATTACK = "blocks.entities.cross-gamemode-attack";
private static Engine instance;
private long saveInterval = DEFAULT_SAVE_INTERVAL;
private long cacheMaximum = DEFAULT_CACHE_MAXIMUM;
private long cacheIncrement = DEFAULT_CACHE_INCREMENT;
private ConcurrentMap<String, WorldEngine> engines = new ConcurrentHashMap<>();
private Timer cacheTimer, saveTimer;
private Logger logger = Logger.getLogger(getClass().getName());
private GroupManager groupManager = null;
private Configuration configuration = new MemoryConfiguration();
private PatternManager patterns = new PatternManager();
private InventoryManager inventoryManager = new MemoryInventoryManager();
private ItemProvider itemProvider = null;
private WorldProvider worlds = null;
private InventoryMergeSettings mergeSettings = new InventoryMergeSettings(configuration);
private Engine() {
newCacheTimer();
newSaveTimer();
setCacheIncrement(cacheIncrement);
setSaveInterval(saveInterval);
// Setup the item provider base package
String packName = AbstractedItem.class.getPackage().getName();
ProviderManager.setBasePackage(packName);
}
/**
* @deprecated For use by tests only
*/
@Deprecated
void forceNotInitialized() {
groupManager = null;
worlds = null;
}
/**
* Determines if this engine is ready or not. If this engine
* is not ready, {@link com.turt2live.antishare.engine.EngineNotInitializedException}
* may be thrown from various methods.
*
* @return true if ready, false otherwise.
*/
public boolean isReady() {
if (groupManager == null) return false;
return worlds != null;
}
/**
* Gets the world provider.
*
* @return the world provider
*/
public WorldProvider getWorldProvider() {
if (!isReady()) throw new EngineNotInitializedException();
return worlds;
}
/**
* Sets the world provider.
*
* @param provider the world provider, cannot be null
*/
public void setWorldProvider(WorldProvider provider) {
if (provider == null) throw new IllegalArgumentException();
DevEngine.log("[Engine] New world provider: " + (provider.getClass().getName()));
this.worlds = provider;
}
/**
* Gets a world by name. If the engine is not ready (does not have a
* world provider), then this will throw an exception.
*
* @param name the world name to lookup, cannot be null
*
* @return the world found, or null if none
*/
public AWorld getWorld(String name) {
if (!isReady()) throw new EngineNotInitializedException();
if (name == null) throw new IllegalArgumentException();
return worlds.getWorld(name);
}
/**
* Gets the current inventory manager
*
* @return the current inventory manager
*/
public InventoryManager getInventoryManager() {
if (!isReady()) throw new EngineNotInitializedException();
return inventoryManager;
}
/**
* Sets the inventory manager to use in this engine
*
* @param manager the new manager, cannot be null
*/
public void setInventoryManager(InventoryManager manager) {
if (manager == null) throw new IllegalArgumentException();
DevEngine.log("[Engine] New inventory manager: " + manager.getClass().getName());
this.inventoryManager = manager;
}
/**
* Gets the item provider for this engine.
*
* @return the item provider, may be null if not initialized
*/
public ItemProvider getItemProvider() {
if (!isReady()) throw new EngineNotInitializedException();
return itemProvider;
}
/**
* Loads the item provider for use. If no provider can be found, an exception is raised.
*
* @throws java.lang.IllegalArgumentException thrown if the stream yields an invalid provider
*/
public void loadItemProvider() {
DevEngine.log("[Engine] Attempting to load item provider ");
ProviderManager providerManager = ProviderManager.getInstance();
List<ItemProvider> providers = providerManager.getProviders();
DevEngine.log("[Engine] There are " + providers.size() + " possible item providers.");
for (ItemProvider provider : providers) {
DevEngine.log("[Engine] Loaded item provider: " + provider.getClass().getName());
}
ItemProvider chosen = providerManager.getProvider();
if (chosen == null) throw new IllegalArgumentException("No provider loaded");
DevEngine.log("[Engine] New item provider: " + chosen.getClass().getName());
itemProvider = chosen;
}
/**
* Gets the group manager instance for this engine.
*
* @return the group manager
*/
public GroupManager getGroupManager() {
if (!isReady()) throw new EngineNotInitializedException();
return groupManager;
}
/**
* Sets the new group manager for this engine to use. This will internally call
* {@link com.turt2live.antishare.configuration.groups.GroupManager#loadAll()}.
*
* @param manager the new group manager, cannot be null
*/
public void setGroupManager(GroupManager manager) {
if (manager == null) throw new IllegalArgumentException("group manager cannot be null");
DevEngine.log("[Engine] New group manager: " + manager.getClass().getName());
this.groupManager = manager;
this.groupManager.loadAll();
}
/**
* Gets the logger for this engine
*
* @return the logger
*/
public Logger getLogger() {
if (!isReady()) throw new EngineNotInitializedException();
return logger;
}
/**
* Sets the new logger for this engine to use
*
* @param logger the new logger, cannot be null
*/
public void setLogger(Logger logger) {
if (logger == null) throw new IllegalArgumentException("logger may not be null");
DevEngine.log("[Engine] New logger: " + logger.getClass().getName());
this.logger = logger;
}
/**
* Gets the pattern manager for this Engine instance
*
* @return the patterns
*/
public PatternManager getPatterns() {
return patterns;
}
/**
* Gets the engine for the specified world. If none exists, a new WorldEngine is
* created and registered.
*
* @param world the world to lookup, cannot be null
*
* @return the world engine
*/
public WorldEngine getEngine(String world) {
if (!isReady()) throw new EngineNotInitializedException();
if (world == null) throw new IllegalArgumentException("world cannot be null");
WorldEngine engine = engines.get(world);
if (engine == null) engine = createWorldEngine(world);
return engine;
}
/**
* Creates a world engine for the supplied world. If the world engine already exists,
* the existing world engine is created.
*
* @param world the world to create an engine for
*
* @return the world engine
*/
public WorldEngine createWorldEngine(String world) {
if (!isReady()) throw new EngineNotInitializedException();
if (world == null) throw new IllegalArgumentException("world cannot be null");
if (engines.containsKey(world)) return engines.get(world);
DevEngine.log("[Engine] Creating world engine for '" + world + "'...");
WorldEngine engine = new WorldEngine(world);
engines.put(world, engine);
EventDispatcher.dispatch(new WorldEngineCreateEvent(engine));
return engine;
}
/**
* Unloads a world engine from the core engine. If the passed world is
* null or not found, this will do nothing.
*
* @param world the world to unload
*/
public void unloadWorldEngine(String world) {
if (!isReady()) throw new EngineNotInitializedException();
if (world != null) {
WorldEngine engine = engines.get(world);
if (engine != null) {
DevEngine.log("[Engine] Unloading world engine for '" + world + "'...");
engine.prepareShutdown();
engines.remove(world);
}
}
}
/**
* Prepares the engine for shutdown. This will save all world engines, cancel the
* cache timer, and revoke all listeners.
*/
public void prepareShutdown() {
EventDispatcher.dispatch(new EngineShutdownEvent());
DevEngine.log("[Engine] Shutting down");
newCacheTimer(); // Cancels internally, resetting the timer to no task
newSaveTimer(); // Cancels internally, resetting the timer to no task
for (WorldEngine engine : engines.values())
engine.prepareShutdown();
engines.clear();
}
/**
* Gets the maximum time the cache is permitted to hold an object
*
* @return the maximum cache time, in milliseconds
*/
public long getCacheMaximum() {
if (!isReady()) throw new EngineNotInitializedException();
return cacheMaximum;
}
/**
* Sets the cache maximum. The value is a millisecond value for how long an object
* may remain stale before being removed
*
* @param cacheMaximum the new cache maximum, cannot be less than or equal to zero
*/
public void setCacheMaximum(long cacheMaximum) {
if (cacheMaximum <= 0) throw new IllegalArgumentException("maximum cannot be less than or equal to zero");
DevEngine.log("[Engine] New cache maximum: " + cacheMaximum);
this.cacheMaximum = cacheMaximum;
}
/**
* Gets the number of milliseconds it takes for the cache timer to tick
*
* @return the milliseconds for a tick
*/
public long getCacheIncrement() {
if (!isReady()) throw new EngineNotInitializedException();
return cacheIncrement;
}
/**
* Sets the new cache increment. This is a millisecond value for how often a cache
* cleanup check is issued. Once this is called with a valid value, the cache timer
* is rescheduled to occur immediately and will have a period equal to the value
* passed.
*
* @param cacheIncrement the new increment, cannot be less than or equal to zero
*/
public void setCacheIncrement(long cacheIncrement) {
if (cacheIncrement <= 0)
throw new IllegalArgumentException("cache increment must not be less than or equal to zero");
DevEngine.log("[Engine] New cache increment: " + cacheIncrement);
this.cacheIncrement = cacheIncrement;
newCacheTimer();
cacheTimer.schedule(new TimerTask() {
@Override
public void run() {
for (WorldEngine engine : engines.values()) {
engine.getBlockManager().cleanup();
}
}
}, 0L, cacheIncrement);
}
/**
* Gets the save interval for the periodic save function. If the returned value
* is less than or equal to zero, the periodic save function is disabled and not
* operating. Any other positive value is used to indicate the period by which
* the engine triggers a save.
*
* @return the save interval
*/
public long getSaveInterval() {
if (!isReady()) throw new EngineNotInitializedException();
return saveInterval;
}
/**
* Sets the new save interval. This is a millisecond value for how often the engine
* should periodically save data in the subsequent world engines and itself. Values
* less than or equal to zero are considered to be "do not save periodically" and
* strictly follow that behaviour. Once called with a value that will trigger a
* periodic save, the timer will save immediately and fire every interval until
* cancelled.
*
* @param saveInterval the new save interval
*/
public void setSaveInterval(long saveInterval) {
DevEngine.log("[Engine] New save interval: " + saveInterval);
this.saveInterval = saveInterval;
newSaveTimer();
if (saveInterval > 0) {
saveTimer.schedule(new TimerTask() {
@Override
public void run() {
for (WorldEngine engine : engines.values()) {
engine.getBlockManager().saveAll();
}
}
}, 0, saveInterval);
}
}
/**
* Gets the active Engine configuration
*
* @return the engine configuration
*/
public Configuration getConfiguration() {
return configuration;
}
/**
* Sets the configuration for this Engine to use. This will call load()
* internally.
*
* @param configuration the configuration to use, cannot be null
*/
public void setConfiguration(Configuration configuration) {
if (configuration == null) throw new IllegalArgumentException();
this.configuration = configuration;
configuration.load();
mergeSettings = new InventoryMergeSettings(configuration);
}
/**
* Gets the applicable inventory merge settings for this engine
*
* @return the inventory merge settings
*/
public InventoryMergeSettings getMergeSettings() {
return mergeSettings;
}
/**
* Gets a particular flag setting from the internal configuration of this
* engine. If the key is not found, the default is returned.
*
* @param configKey the configuration key to lookup, cannot be null
* @param def the default to use if not found
*
* @return the flag or the default setting
*/
public boolean getFlag(String configKey, boolean def) {
return configuration.getBoolean(configKey, def); // Does it's own null check
}
/**
* Processes a player changing worlds. This will perform any actions required
* to keep a proper player state.
*
* @param player the player changing worlds, cannot be null
* @param from the world the player is travelling from, cannot be null
* @param to the world the player is travelling to, cannot be null
*/
// TODO: Unit test
public void processWorldChange(APlayer player, AWorld from, AWorld to) {
if (player == null || from == null || to == null) throw new IllegalArgumentException();
// TODO: Permission check
DevEngine.log("[Engine] Processing player world change",
"[Engine] \t\tplayer = " + player,
"[Engine] \t\tfrom = " + from,
"[Engine] \t\tto = " + to);
AInventory inventory = player.getInventory();
inventory.setWorld(from);
List<AInventory> resulting = processInventoryMerge(inventory, player.getUUID());
for (AInventory i : resulting) getInventoryManager().setInventory(player.getUUID(), i);
player.setInventory(getInventoryManager().getInventory(player.getUUID(), player.getGameMode(), to));
}
/**
* Processes a player joining the server. This will perform any actions that
* need to occur once the player joins the server (such as loading data or
* updating the player's state).
*
* @param player the player joining the server, cannot be null
*/
// TODO: Unit test
public void processPlayerJoin(APlayer player) {
if (player == null) throw new IllegalArgumentException();
// TODO: Handle case of players not having an initial inventory
DevEngine.log("[Engine] Processing player join",
"[Engine] \t\tplayer = " + player);
// We need to find the inventory for their current world/gamemode and set them
AInventory inventory = getInventoryManager().getInventory(player.getUUID(), player.getGameMode(), player.getWorld());
player.setInventory(inventory);
}
/**
* Processes a player leaving the server for any reason. This will perform
* the required cleanup calls as well as any final actions that need to occur
* upon the player's exit.
*
* @param player the player leaving the server, cannot be null
*/
// TODO: Unit test
public void processPlayerQuit(APlayer player) {
if (player == null) throw new IllegalArgumentException();
DevEngine.log("[Engine] Processing player quit",
"[Engine] \t\tplayer = " + player);
List<AInventory> resulting = processInventoryMerge(player.getInventory(), player.getUUID());
for (AInventory inventory : resulting) getInventoryManager().setInventory(player.getUUID(), inventory);
getInventoryManager().save(player.getUUID());
}
/**
* Processes an inventory merge based upon a newly created inventory. This will gather a collection
* of applicable inventories for the player and merge the passed inventory into the collected inventories
* where applicable. The returned set contains all the collected inventories (modified) as well as
* the passed inventory.
*
* @param created the newly created inventory, cannot be null
* @param player the player that is applicable, cannot be null
*
* @return the resulting list of modified inventories, including the passed inventory
*/
// TODO: Unit test
List<AInventory> processInventoryMerge(AInventory created, UUID player) {
if (created == null || player == null) throw new IllegalArgumentException();
List<AInventory> others = getInventoryManager().getInventories(player);
List<Integer> remove = new ArrayList<>();
for (int i = 0; i < others.size(); i++) {
AInventory val = others.get(i);
if (val.getGameMode() == created.getGameMode() && val.getWorld().equals(created.getWorld())) {
remove.add(i);
}
}
for (int i : remove) others.remove(i);
getMergeSettings().mergeInventories(created, others);
others.add(created);
return others;
}
private void newCacheTimer() {
if (cacheTimer != null) cacheTimer.cancel();
cacheTimer = new Timer();
}
private void newSaveTimer() {
if (saveTimer != null) saveTimer.cancel();
saveTimer = new Timer();
}
/**
* Gets the engine instance
*
* @return the engine instance
*/
public static Engine getInstance() {
if (instance == null) instance = new Engine();
return instance;
}
}