Java tutorial
/* * This file is part of Spout. * * Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/> * Spout is licensed under the SpoutDev License Version 1. * * Spout is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * In addition, 180 days after any changes are published, you can use the * software, incorporating those changes, under the terms of the MIT license, * as described in the SpoutDev License Version 1. * * Spout 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License, * the MIT license and the SpoutDev License Version 1 along with this program. * If not, see <http://www.gnu.org/licenses/> for the GNU Lesser General Public * License and see <http://www.spout.org/SpoutDevLicenseV1.txt> for the full license, * including the MIT license. */ package org.spout.engine; import java.io.File; import java.io.FilenameFilter; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.jboss.netty.channel.group.ChannelGroup; import org.jboss.netty.channel.group.ChannelGroupFuture; import org.jboss.netty.channel.group.DefaultChannelGroup; import org.spout.api.Engine; import org.spout.api.Spout; import org.spout.api.chat.completion.CompletionManager; import org.spout.api.chat.completion.CompletionManagerImpl; import org.spout.api.chat.console.MultiConsole; import org.spout.api.command.CommandRegistrationsFactory; import org.spout.api.command.CommandSource; import org.spout.api.command.annotated.AnnotatedCommandRegistrationFactory; import org.spout.api.command.annotated.SimpleInjector; import org.spout.api.entity.Entity; import org.spout.api.entity.Player; import org.spout.api.event.EventManager; import org.spout.api.event.SimpleEventManager; import org.spout.api.event.server.permissions.PermissionGetAllWithNodeEvent; import org.spout.api.event.world.WorldLoadEvent; import org.spout.api.event.world.WorldUnloadEvent; import org.spout.api.exception.ConfigurationException; import org.spout.api.exception.SpoutRuntimeException; import org.spout.api.generator.EmptyWorldGenerator; import org.spout.api.generator.WorldGenerator; import org.spout.api.generator.biome.BiomeRegistry; import org.spout.api.geo.World; import org.spout.api.geo.cuboid.Region; import org.spout.api.geo.discrete.Point; import org.spout.api.inventory.recipe.RecipeManager; import org.spout.api.inventory.recipe.SimpleRecipeManager; import org.spout.api.io.store.simple.BinaryFileStore; import org.spout.api.material.MaterialRegistry; import org.spout.api.permissions.DefaultPermissions; import org.spout.api.permissions.PermissionsSubject; import org.spout.api.plugin.CommonPluginLoader; import org.spout.api.plugin.CommonPluginManager; import org.spout.api.plugin.CommonServiceManager; import org.spout.api.plugin.Platform; import org.spout.api.plugin.Plugin; import org.spout.api.plugin.PluginManager; import org.spout.api.plugin.ServiceManager; import org.spout.api.plugin.security.CommonSecurityManager; import org.spout.api.protocol.Protocol; import org.spout.api.protocol.SessionRegistry; import org.spout.api.scheduler.TaskManager; import org.spout.api.scheduler.TaskPriority; import org.spout.api.util.StringMap; import org.spout.api.util.StringUtil; import org.spout.engine.chat.console.ConsoleManager; import org.spout.engine.chat.console.FileConsole; import org.spout.engine.chat.console.JLineConsole; import org.spout.engine.command.AdministrationCommands; import org.spout.engine.command.ConnectionCommands; import org.spout.engine.command.InputCommands; import org.spout.engine.command.MessagingCommands; import org.spout.engine.command.RendererCommands; import org.spout.engine.command.SyncedRootCommand; import org.spout.engine.command.TestCommands; import org.spout.engine.entity.EntityManager; import org.spout.engine.entity.SpoutPlayer; import org.spout.engine.filesystem.SharedFileSystem; import org.spout.engine.filesystem.WorldData; import org.spout.engine.filesystem.WorldFiles; import org.spout.engine.input.SpoutInputConfiguration; import org.spout.engine.protocol.SpoutSession; import org.spout.engine.protocol.SpoutSessionRegistry; import org.spout.engine.protocol.builtin.SpoutProtocol; import org.spout.engine.scheduler.SpoutParallelTaskManager; import org.spout.engine.scheduler.SpoutScheduler; import org.spout.engine.util.DeadlockMonitor; import org.spout.engine.util.TicklockMonitor; import org.spout.engine.util.thread.AsyncManager; import org.spout.engine.util.thread.ThreadAsyncExecutor; import org.spout.engine.util.thread.snapshotable.SnapshotManager; import org.spout.engine.util.thread.snapshotable.SnapshotableLinkedHashMap; import org.spout.engine.util.thread.snapshotable.SnapshotableReference; import org.spout.engine.world.MemoryReclamationThread; import org.spout.engine.world.SpoutRegion; import org.spout.engine.world.SpoutWorld; import org.spout.engine.world.WorldSavingThread; import static org.spout.api.lang.Translation.log; import static org.spout.api.lang.Translation.tr; public abstract class SpoutEngine extends AsyncManager implements Engine { private static final Logger logger = Logger.getLogger("Spout"); private final String name = "Spout Engine"; private final File pluginDirectory = SharedFileSystem.PLUGIN_DIRECTORY; private final File configDirectory = SharedFileSystem.CONFIG_DIRECTORY; private final File updateDirectory = SharedFileSystem.UPDATE_DIRECTORY; private final File dataDirectory = SharedFileSystem.DATA_DIRECTORY; private final Random random = new Random(); private final CommonSecurityManager securityManager = new CommonSecurityManager(0); //TODO Need to integrate this/evaluate security in the engine. private final CommonPluginManager pluginManager = new CommonPluginManager(this, securityManager, 0.0); private final ConsoleManager consoleManager; private final EventManager eventManager = new SimpleEventManager(); private final RecipeManager recipeManager = new SimpleRecipeManager(); private final ServiceManager serviceManager = CommonServiceManager.getInstance(); private final SnapshotManager snapshotManager = new SnapshotManager(); protected final SnapshotableLinkedHashMap<String, SpoutPlayer> players = new SnapshotableLinkedHashMap<String, SpoutPlayer>( snapshotManager); private final WorldGenerator defaultGenerator = new EmptyWorldGenerator(); protected final SpoutSessionRegistry sessions = new SpoutSessionRegistry(); protected final SpoutScheduler scheduler = new SpoutScheduler(this); protected final SpoutParallelTaskManager parallelTaskManager = new SpoutParallelTaskManager(this); protected final ChannelGroup group = new DefaultChannelGroup(); private final AtomicBoolean setupComplete = new AtomicBoolean(false); private final SpoutConfiguration config = new SpoutConfiguration(); private final SpoutInputConfiguration inputConfig = new SpoutInputConfiguration(); private final CompletionManager completions = new CompletionManagerImpl(); private final SyncedRootCommand rootCommand = new SyncedRootCommand(this); private final File worldFolder = SharedFileSystem.WORLDS_DIRECTORY; private final SnapshotableLinkedHashMap<String, SpoutWorld> loadedWorlds = new SnapshotableLinkedHashMap<String, SpoutWorld>( snapshotManager); private final SnapshotableReference<World> defaultWorld = new SnapshotableReference<World>(snapshotManager, null); protected final ConcurrentMap<SocketAddress, Protocol> boundProtocols = new ConcurrentHashMap<SocketAddress, Protocol>(); protected final SnapshotableLinkedHashMap<String, SpoutPlayer> onlinePlayers = new SnapshotableLinkedHashMap<String, SpoutPlayer>( snapshotManager); private String logFile; private StringMap engineItemMap = null; private StringMap engineBiomeMap = null; private MultiConsole console; private SpoutApplication arguments; private MemoryReclamationThread reclamation = null; public SpoutEngine() { super(1, new ThreadAsyncExecutor("Engine bootstrap thread")); logFile = "logs" + File.separator + "log-%D.txt"; consoleManager = new ConsoleManager(this); } public void init(SpoutApplication args) { this.arguments = args; try { config.load(); inputConfig.load(); } catch (ConfigurationException e) { log("Error loading config: %0", Level.SEVERE, e.getMessage(), e); } console = new MultiConsole(new FileConsole(this), new JLineConsole(this)); consoleManager.setupConsole(console); registerWithScheduler(scheduler); if (!getExecutor().startExecutor()) { throw new IllegalStateException("SpoutEngine's executor was already started"); } DefaultPermissions.addDefaultPermission(STANDARD_BROADCAST_PERMISSION); if (debugMode()) { new TicklockMonitor().start(); new DeadlockMonitor().start(); } } public abstract void start(); public void start(boolean checkWorlds) { log("Spout is starting in %0-only mode.", getPlatform().name().toLowerCase()); log("Current version is %0 (Implementing SpoutAPI %1).", Spout.getEngine().getVersion(), Spout.getAPIVersion()); log("This software is currently in alpha status so components may"); log("have bugs or not work at all. Please report any issues to"); log("http://issues.spout.org"); if (debugMode()) { log("Debug Mode has been toggled on! This mode is intended for developers only", Level.WARNING); } scheduler.scheduleSyncRepeatingTask(this, new SessionTask(sessions), 50, 50, TaskPriority.CRITICAL); final CommandRegistrationsFactory<Class<?>> commandRegFactory = new AnnotatedCommandRegistrationFactory( new SimpleInjector(this)); // Register commands getRootCommand().addSubCommands(this, AdministrationCommands.class, commandRegFactory); getRootCommand().addSubCommands(this, MessagingCommands.class, commandRegFactory); getRootCommand().addSubCommands(this, ConnectionCommands.class, commandRegFactory); InputCommands.setupInputCommands(this, getRootCommand()); if (arguments.debug) { getRootCommand().addSubCommands(this, TestCommands.class, commandRegFactory); } if (Spout.getPlatform() == Platform.CLIENT) { getRootCommand().addSubCommands(this, RendererCommands.class, commandRegFactory); } Protocol.registerProtocol(new SpoutProtocol()); //Setup the Material Registry engineItemMap = MaterialRegistry.setupRegistry(); //Setup the Biome Registry engineBiomeMap = BiomeRegistry.setupRegistry(); // Start loading plugins loadPlugins(); postPluginLoad(config); enablePlugins(); if (checkWorlds) { //At least one plugin should have registered atleast one world if (loadedWorlds.getLive().size() == 0) { throw new IllegalStateException( "There are no loaded worlds! You must install a plugin that creates a world (Did you forget Vanilla?)"); } //Pick the default world from the configuration World world = this.getWorld(SpoutConfiguration.DEFAULT_WORLD.getString()); if (world != null) { this.setDefaultWorld(world); } //If we don't have a default world set, just grab one. getDefaultWorld(); } if (SpoutConfiguration.RECLAIM_MEMORY.getBoolean()) { reclamation = new MemoryReclamationThread(); reclamation.start(); } scheduler.startMainThread(); WorldSavingThread.startThread(); setupComplete.set(true); } /** * This method is called after {@link #loadPlugins()} but before {@link #enablePlugins()} */ protected void postPluginLoad(SpoutConfiguration config) { } public void loadPlugins() { pluginManager.registerPluginLoader(CommonPluginLoader.class); pluginManager.clearPlugins(); if (!pluginDirectory.exists()) { pluginDirectory.mkdirs(); } List<Plugin> plugins = pluginManager.loadPlugins(pluginDirectory); for (Plugin plugin : plugins) { try { //Technically unsafe. This should call the security manager plugin.onLoad(); } catch (Exception ex) { //TODO: fix //log("Error loading %0: %1", Level.SEVERE, plugin.getDescription().getName(), ex.getMessage(), ex); ex.printStackTrace(); } } } public SpoutApplication getArguments() { return arguments; } private void enablePlugins() { for (Plugin plugin : pluginManager.getPlugins()) { pluginManager.enablePlugin(plugin); } } public Collection<SpoutPlayer> rawGetAllOnlinePlayers() { return players.get().values(); } @Override public String getName() { return name; } @Override public String getVersion() { return getClass().getPackage().getImplementationVersion(); } @Override public Set<PermissionsSubject> getAllWithNode(String permission) { return getEventManager().callEvent(new PermissionGetAllWithNodeEvent(permission)).getAllowedReceivers(); } @Override public PluginManager getPluginManager() { return pluginManager; } @Override public Logger getLogger() { return logger; } @Override public File getUpdateFolder() { if (!updateDirectory.exists()) { updateDirectory.mkdirs(); } return updateDirectory; } @Override public File getConfigFolder() { if (!configDirectory.exists()) { configDirectory.mkdirs(); } return configDirectory; } @Override public File getDataFolder() { if (!dataDirectory.exists()) { dataDirectory.mkdirs(); } File playerDirectory = new File(dataDirectory.toString() + File.separator + "players"); if (!playerDirectory.exists()) { playerDirectory.mkdirs(); } return dataDirectory; } @Override public File getPluginFolder() { if (!pluginDirectory.exists()) { pluginDirectory.mkdirs(); } return pluginDirectory; } @Override public Collection<World> matchWorld(String name) { return StringUtil.matchName(getWorlds(), name); } @Override public Collection<File> matchWorldFolder(String worldName) { return StringUtil.matchFile(getWorldFolders(), worldName); } @Override public SpoutWorld getWorld(String name) { return getWorld(name, true); } @Override public SpoutWorld getWorld(String name, boolean exact) { if (exact) { SpoutWorld world = loadedWorlds.get().get(name); if (world != null) { return world; } return loadedWorlds.getLive().get(name); } else { return StringUtil.getShortest(StringUtil.matchName(loadedWorlds.getValues(), name)); } } @Override public SpoutWorld getWorld(UUID uid) { for (SpoutWorld world : loadedWorlds.getValues()) { if (world.getUID().equals(uid)) { return world; } } return null; } @Override public Collection<World> getWorlds() { Collection<World> w = new ArrayList<World>(); for (SpoutWorld world : loadedWorlds.getValues()) { w.add(world); } return w; } @Override public World loadWorld(String name, WorldGenerator generator) { if (loadedWorlds.get().containsKey((name))) { return loadedWorlds.get().get(name); } if (loadedWorlds.getLive().containsKey(name)) { return loadedWorlds.getLive().get(name); } // TODO - should include generator (and non-zero seed) if (generator == null) { generator = defaultGenerator; } WorldData worldData = WorldData.loadForWorld(name); SpoutWorld world; if (worldData == null) { log("Generating new world named [%0]", name); File itemMapFile = new File(new File(SharedFileSystem.WORLDS_DIRECTORY, name), "materials.dat"); BinaryFileStore itemStore = new BinaryFileStore(itemMapFile); StringMap itemMap = new StringMap(engineItemMap, itemStore, 0, Short.MAX_VALUE, name + "ItemMap"); world = new SpoutWorld(name, this, random.nextLong(), 0L, generator, UUID.randomUUID(), itemMap); new WorldData(world).saveToFile(); } else { world = worldData.toWorld(generator, engineItemMap); } World oldWorld = loadedWorlds.putIfAbsent(name, world); if (oldWorld != null) { return oldWorld; } if (!world.getExecutor().startExecutor()) { throw new IllegalStateException("Unable to start executor for new world"); } getEventManager().callDelayedEvent(new WorldLoadEvent(world)); return world; } @Override public void save(boolean worlds, boolean players) { // TODO Auto-generated method stub } @Override public boolean stop() { return stop(tr("Spout shutting down", getCommandSource())); // TODO distribute the message differently } private final AtomicBoolean stopping = new AtomicBoolean(); @Override public boolean stop(final String message) { return stop(message, true); } /** * Used to allow subclasses submit final tasks before stopping the scheduler * @param message * @param stopScheduler * @return */ protected boolean stop(final String message, boolean stopScheduler) { final SpoutEngine engine = this; if (!stopping.compareAndSet(false, true)) { return false; } getPluginManager().clearPlugins(); Runnable lastTickTask = new Runnable() { @Override public void run() { setupComplete.set(false); for (SpoutWorld world : engine.getLiveWorlds()) { world.unload(true); } console.close(); } }; Runnable finalTask = new Runnable() { @Override public void run() { ChannelGroupFuture f = group.close(); try { f.await(); } catch (InterruptedException ie) { Spout.getLogger().info("Thread interrupted when waiting for network shutdown"); } WorldSavingThread.finish(); WorldSavingThread.staticJoin(); } }; scheduler.submitLastTickTask(lastTickTask); scheduler.submitFinalTask(finalTask, true); if (stopScheduler) { scheduler.stop(); } return true; } @Override public List<File> getWorldFolders() { File[] folders = this.getWorldFolder().listFiles((FilenameFilter) DirectoryFileFilter.INSTANCE); if (folders == null || folders.length == 0) { return new ArrayList<File>(); } List<File> worlds = new ArrayList<File>(folders.length); // Are they really world folders? for (File world : folders) { if (new File(world, "world.dat").exists()) { worlds.add(world); } } return worlds; } @Override public File getWorldFolder() { return worldFolder; } @Override public SyncedRootCommand getRootCommand() { return rootCommand; } @Override public EventManager getEventManager() { return eventManager; } @Override public ChannelGroup getChannelGroup() { return group; } @Override public SessionRegistry getSessionRegistry() { return sessions; } @Override public SpoutScheduler getScheduler() { return scheduler; } @Override public TaskManager getParallelTaskManager() { return parallelTaskManager; } @Override public WorldGenerator getDefaultGenerator() { return defaultGenerator; } @Override public Protocol getProtocol(SocketAddress socketAddress) { Protocol proto = boundProtocols.get(socketAddress); if (proto == null) { for (Map.Entry<SocketAddress, Protocol> entry : boundProtocols.entrySet()) { if (entry.getKey() instanceof InetSocketAddress && socketAddress instanceof InetSocketAddress) { InetSocketAddress key = (InetSocketAddress) entry.getKey(), given = (InetSocketAddress) socketAddress; if (key.getPort() == given.getPort() && ((given.getAddress() instanceof Inet4Address && key.getAddress().getHostAddress().equals("0.0.0.0")) || (given.getAddress() instanceof Inet6Address && key.getAddress().getHostAddress().equals("::")))) { // TODO: Make sure IPV6 works proto = entry.getValue(); break; } } } } if (proto == null) { throw new SpoutRuntimeException("No protocol for bound address!"); } return proto; } @Override public ServiceManager getServiceManager() { return serviceManager; } @Override public RecipeManager getRecipeManager() { return recipeManager; } @Override public boolean debugMode() { return arguments.debug; } @Override public Thread getMainThread() { return scheduler.getMainThread(); } @Override public void finalizeRun() throws InterruptedException { // TODO Auto-generated method stub } @Override public void preSnapshotRun() throws InterruptedException { // TODO Auto-generated method stub } @Override public void copySnapshotRun() throws InterruptedException { snapshotManager.copyAllSnapshots(); for (Player player : players.get().values()) { ((SpoutPlayer) player).copySnapshot(); } } @Override public void startTickRun(int stage, long delta) throws InterruptedException { switch (stage) { case 0: engineItemMap.save(); engineBiomeMap.save(); break; } } @Override public void haltRun() throws InterruptedException { log("Server halting"); } @Override public boolean setDefaultWorld(World world) { if (world == null) { return false; } defaultWorld.set(world); return true; } @Override public World getDefaultWorld() { final Map<String, SpoutWorld> loadedWorlds = this.loadedWorlds.get(); final World defaultWorld = this.defaultWorld.get(); if (defaultWorld != null && loadedWorlds.containsKey(defaultWorld.getName())) { return defaultWorld; } if (loadedWorlds.isEmpty()) { return null; } World first = loadedWorlds.values().iterator().next(); return first; } @Override public String getLogFile() { return logFile; } @Override public boolean unloadWorld(String name, boolean save) { return unloadWorld(loadedWorlds.getLive().get(name), save); } @Override public boolean unloadWorld(World world, boolean save) { if (world == null) { return false; } boolean success = loadedWorlds.remove(world.getName(), (SpoutWorld) world); if (success) { if (save) { SpoutWorld w = (SpoutWorld) world; if (!w.getExecutor().haltExecutor()) { throw new IllegalStateException("Executor was already halted when halting was attempted"); } getEventManager().callDelayedEvent(new WorldUnloadEvent(world)); w.unload(save); } //Note: Worlds should not allow being saved twice and/or throw exceptions if accessed after unloading // Also, should blank out as much internal world data as possible, in case plugins retain references to unloaded worlds } return success; } public EntityManager getExpectedEntityManager(Point point) { Region region = point.getWorld().getRegionFromBlock(point); return ((SpoutRegion) region).getEntityManager(); } @Override public Entity getEntity(UUID uid) { for (World w : loadedWorlds.get().values()) { Entity e = w.getEntity(uid); if (e != null) { return e; } } return null; } @Override public List<String> getAllPlayers() { ArrayList<String> names = new ArrayList<String>(); for (Player player : players.getValues()) { names.add(player.getName()); } return Collections.unmodifiableList(names); } @Override public Player getPlayer(String name, boolean exact) { name = name.toLowerCase(); if (exact) { for (Player player : players.getValues()) { if (player.getName().equalsIgnoreCase(name)) { return player; } } return null; } else { return StringUtil.getShortest(StringUtil.matchName(players.getValues(), name)); } } @Override public Collection<Player> matchPlayer(String name) { //TODO Can someone redo this or make it better? return StringUtil.matchName( Arrays.<Player>asList(players.getValues().toArray(new Player[players.getValues().size()])), name); } // Players should use weak map? public Player addPlayer(String playerName, SpoutSession<?> session, int viewDistance) { SpoutPlayer player = WorldFiles.loadPlayerData(playerName); boolean created = false; if (player == null) { player = new SpoutPlayer(playerName, null, viewDistance); created = true; } SpoutPlayer oldPlayer = players.put(playerName, player); if (reclamation != null) { reclamation.addPlayer(); } if (oldPlayer != null) { oldPlayer.kick("Login occured from another client"); } //Connect the player and set their transform to the default world's spawn. player.connect(session, created ? getDefaultWorld().getSpawnPoint() : player.getTransform().getTransformLive()); //Spawn the player in the world World world = player.getTransform().getTransformLive().getPosition().getWorld(); world.spawnEntity(player); ((SpoutWorld) world).addPlayer(player); //Set the player to the session session.setPlayer(player); //Initialize the session session.getProtocol().initializeSession(session); return player; } public boolean removePlayer(SpoutPlayer player) { boolean remove = players.remove(player.getName(), player); if (remove) { if (reclamation != null) { reclamation.removePlayer(); } return true; } return false; } protected Collection<SpoutWorld> getLiveWorlds() { return loadedWorlds.getLive().values(); } @Override public CommandSource getCommandSource() { return consoleManager.getCommandSource(); } /** * Gets the item map used across all worlds on the engine * @return engine map */ public StringMap getEngineItemMap() { return engineItemMap; } /** * Gets the biome map used accorss all worlds on the engine * @return biome map */ public StringMap getBiomeMap() { return engineBiomeMap; } public boolean isSetupComplete() { return setupComplete.get(); } @Override public MultiConsole getConsoles() { return console; } // The engine doesn't do any of these @Override public void runPhysics(int sequence) throws InterruptedException { } @Override public long getFirstDynamicUpdateTime() { return SpoutScheduler.END_OF_THE_WORLD; } @Override public void runDynamicUpdates(long time, int sequence) throws InterruptedException { } @Override public void runLighting(int sequence) throws InterruptedException { } @Override public CompletionManager getCompletionManager() { return completions; } private class SessionTask implements Runnable { final SpoutSessionRegistry registry; SessionTask(SpoutSessionRegistry registry) { this.registry = registry; } @Override public void run() { registry.pulse(); } } }