/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.engine; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.collect.Queues; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.assets.AssetFactory; import org.terasology.assets.management.AssetManager; import org.terasology.assets.module.ModuleAwareAssetTypeManager; import org.terasology.context.Context; import org.terasology.context.internal.ContextImpl; import org.terasology.engine.bootstrap.EnvironmentSwitchHandler; import org.terasology.engine.modes.GameState; import org.terasology.engine.module.ModuleManager; import org.terasology.engine.module.ModuleManagerImpl; import org.terasology.engine.paths.PathManager; import org.terasology.engine.subsystem.DisplayDevice; import org.terasology.engine.subsystem.EngineSubsystem; import org.terasology.engine.subsystem.RenderingSubsystemFactory; import org.terasology.engine.subsystem.common.CommandSubsystem; import org.terasology.engine.subsystem.common.ConfigurationSubsystem; import org.terasology.engine.subsystem.common.GameSubsystem; import org.terasology.engine.subsystem.common.MonitoringSubsystem; import org.terasology.engine.subsystem.common.NetworkSubsystem; import org.terasology.engine.subsystem.common.PhysicsSubsystem; import org.terasology.engine.subsystem.common.ThreadManagerSubsystem; import org.terasology.engine.subsystem.common.TimeSubsystem; import org.terasology.engine.subsystem.common.WorldGenerationSubsystem; import org.terasology.entitySystem.prefab.Prefab; import org.terasology.entitySystem.prefab.PrefabData; import org.terasology.entitySystem.prefab.internal.PojoPrefab; import org.terasology.i18n.I18nSubsystem; import org.terasology.input.InputSystem; import org.terasology.logic.behavior.asset.BehaviorTree; import org.terasology.logic.behavior.asset.BehaviorTreeData; import org.terasology.monitoring.Activity; import org.terasology.monitoring.PerformanceMonitor; import org.terasology.persistence.typeHandling.TypeSerializationLibrary; import org.terasology.reflection.copy.CopyStrategyLibrary; import org.terasology.reflection.reflect.ReflectFactory; import org.terasology.reflection.reflect.ReflectionReflectFactory; import org.terasology.registry.CoreRegistry; import org.terasology.rendering.nui.asset.UIData; import org.terasology.rendering.nui.asset.UIElement; import org.terasology.rendering.nui.skin.UISkin; import org.terasology.rendering.nui.skin.UISkinData; import org.terasology.version.TerasologyVersion; import org.terasology.world.block.family.BlockFamilyFactoryRegistry; import org.terasology.world.block.family.DefaultBlockFamilyFactoryRegistry; import org.terasology.world.block.loader.BlockFamilyDefinition; import org.terasology.world.block.loader.BlockFamilyDefinitionData; import org.terasology.world.block.loader.BlockFamilyDefinitionFormat; import org.terasology.world.block.shapes.BlockShape; import org.terasology.world.block.shapes.BlockShapeData; import org.terasology.world.block.shapes.BlockShapeImpl; import org.terasology.world.block.sounds.BlockSounds; import org.terasology.world.block.sounds.BlockSoundsData; import org.terasology.world.block.tiles.BlockTile; import org.terasology.world.block.tiles.TileData; import java.util.Collection; import java.util.Deque; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; /** * <p> * This GameEngine implementation is the heart of Terasology. * </p> * <p> * It first takes care of making a number of application-wide initializations (see init() * method). It then provides a main game loop (see run() method) characterized by a number * of mutually exclusive {@link GameState}s. The current GameState is updated each * frame, and a change of state (see changeState() method) can be requested at any time - the * switch will occur cleanly between frames. Interested parties can be notified of GameState * changes by using the subscribeToStateChange() method. * </p> * <p> * At this stage the engine also provides a number of utility methods (see submitTask() and * hasMouseFocus() to name a few) but they might be moved elsewhere. * </p> * <p> * Special mention must be made in regard to EngineSubsystems. An {@link EngineSubsystem} * is a pluggable low-level component of the engine, that is processed every frame - like * rendering or audio. A list of EngineSubsystems is provided in input to the engine's * constructor. Different sets of Subsystems can significantly change the behaviour of * the engine, i.e. providing a "no-frills" server in one case or a full-graphics client * in another. * </p> */ public class TerasologyEngine implements GameEngine { private static final Logger logger = LoggerFactory.getLogger(TerasologyEngine.class); private static final int ONE_MEBIBYTE = 1024 * 1024; private GameState currentState; private GameState pendingState; private Set<StateChangeSubscriber> stateChangeSubscribers = Sets.newLinkedHashSet(); private EngineStatus status = StandardGameStatus.UNSTARTED; private final List<EngineStatusSubscriber> statusSubscriberList = new CopyOnWriteArrayList<>(); private volatile boolean shutdownRequested; private volatile boolean running; private TimeSubsystem timeSubsystem; private Deque<EngineSubsystem> allSubsystems; private ModuleAwareAssetTypeManager assetTypeManager; /** * Contains objects that life for the duration of this engine. */ private Context rootContext; /** * This constructor initializes the engine by initializing its systems, * subsystems and managers. It also verifies that some required systems * are up and running after they have been initialized. * * @param subsystems Typical subsystems lists contain graphics, timer, * audio and input subsystems. */ public TerasologyEngine(TimeSubsystem timeSubsystem, Collection<EngineSubsystem> subsystems) { this.rootContext = new ContextImpl(); this.timeSubsystem = timeSubsystem; /* * We can't load the engine without core registry yet. * e.g. the statically created MaterialLoader needs the CoreRegistry to get the AssetManager. * And the engine loads assets while it gets created. */ // TODO: Remove CoreRegistry.setContext(rootContext); this.allSubsystems = Queues.newArrayDeque(); this.allSubsystems.add(new ConfigurationSubsystem()); this.allSubsystems.add(timeSubsystem); this.allSubsystems.addAll(subsystems); this.allSubsystems.add(new ThreadManagerSubsystem()); this.allSubsystems.add(new MonitoringSubsystem()); this.allSubsystems.add(new PhysicsSubsystem()); this.allSubsystems.add(new CommandSubsystem()); this.allSubsystems.add(new NetworkSubsystem()); this.allSubsystems.add(new WorldGenerationSubsystem()); this.allSubsystems.add(new GameSubsystem()); this.allSubsystems.add(new I18nSubsystem()); } private void initialize() { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); Stopwatch totalInitTime = Stopwatch.createStarted(); try { logger.info("Initializing Terasology..."); logEnvironmentInfo(); // TODO: Need to get everything thread safe and get rid of the concept of "GameThread" as much as possible. GameThread.setToCurrentThread(); preInitSubsystems(); initManagers(); initSubsystems(); changeStatus(TerasologyEngineStatus.INITIALIZING_ASSET_MANAGEMENT); initAssets(); EnvironmentSwitchHandler environmentSwitcher = new EnvironmentSwitchHandler(); rootContext.put(EnvironmentSwitchHandler.class, environmentSwitcher); environmentSwitcher.handleSwitchToGameEnvironment(rootContext); postInitSubsystems(); verifyInitialisation(); /** * Prevent objects being put in engine context after init phase. Engine states should use/create a * child context. */ CoreRegistry.setContext(null); } catch (RuntimeException e) { logger.error("Failed to initialise Terasology", e); cleanup(); throw e; } double seconds = 0.001 * totalInitTime.elapsed(TimeUnit.MILLISECONDS); logger.info("Initialization completed in {}sec.", String.format("%.2f", seconds)); } private void verifyInitialisation() { verifyRequiredSystemIsRegistered(Time.class); verifyRequiredSystemIsRegistered(DisplayDevice.class); verifyRequiredSystemIsRegistered(RenderingSubsystemFactory.class); verifyRequiredSystemIsRegistered(InputSystem.class); } /** * Logs software, environment and hardware information. */ private void logEnvironmentInfo() { logger.info(TerasologyVersion.getInstance().toString()); logger.info("Home path: {}", PathManager.getInstance().getHomePath()); logger.info("Install path: {}", PathManager.getInstance().getInstallPath()); logger.info("Java: {} in {}", System.getProperty("java.version"), System.getProperty("java.home")); logger.info("Java VM: {}, version: {}", System.getProperty("java.vm.name"), System.getProperty("java.vm.version")); logger.info("OS: {}, arch: {}, version: {}", System.getProperty("os.name"), System.getProperty("os.arch"), System.getProperty("os.version")); logger.info("Max. Memory: {} MiB", Runtime.getRuntime().maxMemory() / ONE_MEBIBYTE); logger.info("Processors: {}", Runtime.getRuntime().availableProcessors()); } /** * Gives a chance to subsystems to do something BEFORE managers and Time are initialized. */ private void preInitSubsystems() { changeStatus(TerasologyEngineStatus.PREPARING_SUBSYSTEMS); for (EngineSubsystem subsystem : getSubsystems()) { changeStatus(() -> "Pre-initialising " + subsystem.getName() + " subsystem"); subsystem.preInitialise(rootContext); } } private void initSubsystems() { changeStatus(TerasologyEngineStatus.INITIALIZING_SUBSYSTEMS); for (EngineSubsystem subsystem : getSubsystems()) { changeStatus(() -> "Initialising " + subsystem.getName() + " subsystem"); subsystem.initialise(this, rootContext); } } /** * Gives a chance to subsystems to do something AFTER managers and Time are initialized. */ private void postInitSubsystems() { for (EngineSubsystem subsystem : getSubsystems()) { subsystem.postInitialise(rootContext); } } /** * Verifies that a required class is available through the core registry. * * @param clazz The required type, i.e. Time.class * @throws IllegalStateException Details the required system that has not been registered. */ private void verifyRequiredSystemIsRegistered(Class<?> clazz) { if (rootContext.get(clazz) == null) { throw new IllegalStateException(clazz.getSimpleName() + " not registered as a core system."); } } private void initManagers() { changeStatus(TerasologyEngineStatus.INITIALIZING_MODULE_MANAGER); ModuleManager moduleManager = new ModuleManagerImpl(); rootContext.put(ModuleManager.class, moduleManager); changeStatus(TerasologyEngineStatus.INITIALIZING_LOWLEVEL_OBJECT_MANIPULATION); ReflectFactory reflectFactory = new ReflectionReflectFactory(); rootContext.put(ReflectFactory.class, reflectFactory); CopyStrategyLibrary copyStrategyLibrary = new CopyStrategyLibrary(reflectFactory); rootContext.put(CopyStrategyLibrary.class, copyStrategyLibrary); rootContext.put(TypeSerializationLibrary.class, new TypeSerializationLibrary(reflectFactory, copyStrategyLibrary)); changeStatus(TerasologyEngineStatus.INITIALIZING_ASSET_TYPES); assetTypeManager = new ModuleAwareAssetTypeManager(); rootContext.put(ModuleAwareAssetTypeManager.class, assetTypeManager); rootContext.put(AssetManager.class, assetTypeManager.getAssetManager()); } private void initAssets() { DefaultBlockFamilyFactoryRegistry familyFactoryRegistry = new DefaultBlockFamilyFactoryRegistry(); rootContext.put(BlockFamilyFactoryRegistry.class, familyFactoryRegistry); // cast lambdas explicitly to avoid inconsistent compiler behavior wrt. type inference assetTypeManager.registerCoreAssetType(Prefab.class, (AssetFactory<Prefab, PrefabData>) PojoPrefab::new, false, "prefabs"); assetTypeManager.registerCoreAssetType(BlockShape.class, (AssetFactory<BlockShape, BlockShapeData>) BlockShapeImpl::new, "shapes"); assetTypeManager.registerCoreAssetType(BlockSounds.class, (AssetFactory<BlockSounds, BlockSoundsData>) BlockSounds::new, "blockSounds"); assetTypeManager.registerCoreAssetType(BlockTile.class, (AssetFactory<BlockTile, TileData>) BlockTile::new, "blockTiles"); assetTypeManager.registerCoreAssetType(BlockFamilyDefinition.class, (AssetFactory<BlockFamilyDefinition, BlockFamilyDefinitionData>) BlockFamilyDefinition::new, "blocks"); assetTypeManager.registerCoreFormat(BlockFamilyDefinition.class, new BlockFamilyDefinitionFormat(assetTypeManager.getAssetManager(), familyFactoryRegistry)); assetTypeManager.registerCoreAssetType(UISkin.class, (AssetFactory<UISkin, UISkinData>) UISkin::new, "skins"); assetTypeManager.registerCoreAssetType(BehaviorTree.class, (AssetFactory<BehaviorTree, BehaviorTreeData>) BehaviorTree::new, false, "behaviors"); assetTypeManager.registerCoreAssetType(UIElement.class, (AssetFactory<UIElement, UIData>) UIElement::new, "ui"); for (EngineSubsystem subsystem : allSubsystems) { subsystem.registerCoreAssetTypes(assetTypeManager); } } @Override public EngineStatus getStatus() { return status; } @Override public void subscribe(EngineStatusSubscriber subscriber) { statusSubscriberList.add(subscriber); } @Override public void unsubscribe(EngineStatusSubscriber subscriber) { statusSubscriberList.remove(subscriber); } private void changeStatus(EngineStatus newStatus) { status = newStatus; for (EngineStatusSubscriber subscriber : statusSubscriberList) { subscriber.onEngineStatusChanged(newStatus); } } /** * Runs the engine, including its main loop. This method is called only once per * application startup, which is the reason the GameState provided is the -initial- * state rather than a generic game state. * * @param initialState In at least one context (the PC facade) the GameState * implementation provided as input may vary, depending if * the application has or hasn't been started headless. */ @Override public synchronized void run(GameState initialState) { Preconditions.checkState(!running); running = true; initialize(); changeStatus(StandardGameStatus.RUNNING); try { rootContext.put(GameEngine.class, this); changeState(initialState); mainLoop(); // -THE- MAIN LOOP. Most of the application time and resources are spent here. } catch (Throwable e) { logger.error("Uncaught exception, attempting clean game shutdown", e); throw e; } finally { try { cleanup(); } catch (RuntimeException t) { logger.error("Clean game shutdown after an uncaught exception failed", t); } running = false; shutdownRequested = false; changeStatus(StandardGameStatus.UNSTARTED); } } /** * The main loop runs until the EngineState is set back to INITIALIZED by shutdown() * or until the OS requests the application's window to be closed. Engine cleanup * and disposal occur afterwards. */ private void mainLoop() { PerformanceMonitor.startActivity("Other"); // MAIN GAME LOOP while (!shutdownRequested) { assetTypeManager.reloadChangedOnDisk(); processPendingState(); if (currentState == null) { shutdown(); break; } Iterator<Float> updateCycles = timeSubsystem.getEngineTime().tick(); for (EngineSubsystem subsystem : allSubsystems) { try (Activity ignored = PerformanceMonitor.startActivity(subsystem.getName() + " PreUpdate")) { subsystem.preUpdate(currentState, timeSubsystem.getEngineTime().getRealDelta()); } } while (updateCycles.hasNext()) { float updateDelta = updateCycles.next(); // gameTime gets updated here! try (Activity ignored = PerformanceMonitor.startActivity("Main Update")) { currentState.update(updateDelta); } } // Waiting processes are set by modules via GameThread.a/synch() methods. GameThread.processWaitingProcesses(); for (EngineSubsystem subsystem : getSubsystems()) { try (Activity ignored = PerformanceMonitor.startActivity(subsystem.getName() + " Subsystem postUpdate")) { subsystem.postUpdate(currentState, timeSubsystem.getEngineTime().getRealDelta()); } } assetTypeManager.disposedUnusedAssets(); PerformanceMonitor.rollCycle(); PerformanceMonitor.startActivity("Other"); } PerformanceMonitor.endActivity(); } private void cleanup() { logger.info("Shutting down Terasology..."); changeStatus(StandardGameStatus.SHUTTING_DOWN); if (currentState != null) { currentState.dispose(); currentState = null; } Iterator<EngineSubsystem> preshutdownIter = allSubsystems.descendingIterator(); while (preshutdownIter.hasNext()) { EngineSubsystem subsystem = preshutdownIter.next(); try { subsystem.preShutdown(); } catch (RuntimeException e) { logger.error("Error preparing to shutdown {} subsystem", subsystem.getName(), e); } } Iterator<EngineSubsystem> shutdownIter = allSubsystems.descendingIterator(); while (shutdownIter.hasNext()) { EngineSubsystem subsystem = shutdownIter.next(); try { subsystem.shutdown(); } catch (RuntimeException e) { logger.error("Error shutting down {} subsystem", subsystem.getName(), e); } } } /** * Causes the main loop to stop at the end of the current frame, cleanly ending * the current GameState, all running task threads and disposing subsystems. */ @Override public void shutdown() { shutdownRequested = true; } /** * Changes the game state, i.e. to switch from the MainMenu to Ingame via Loading screen * (each is a GameState). The change can be immediate, if there is no current game * state set, or scheduled, when a current state exists and the new state is stored as * pending. That been said, scheduled changes occurs in the main loop through the call * processStateChanges(). As such, from a user perspective in normal circumstances, * scheduled changes are likely to be perceived as immediate. */ @Override public void changeState(GameState newState) { if (currentState != null) { pendingState = newState; // scheduled change } else { switchState(newState); // immediate change } } private void processPendingState() { if (pendingState != null) { switchState(pendingState); pendingState = null; } } private void switchState(GameState newState) { if (currentState != null) { currentState.dispose(); } currentState = newState; LoggingContext.setGameState(newState); newState.init(this); stateChangeSubscribers.forEach(StateChangeSubscriber::onStateChange); InputSystem inputSystem = rootContext.get(InputSystem.class); inputSystem.drainQueues(); } @Override public boolean hasPendingState() { return pendingState != null; } @Override public GameState getState() { return currentState; } @Override public boolean isRunning() { return running; } public Iterable<EngineSubsystem> getSubsystems() { return allSubsystems; } @Override public void subscribeToStateChange(StateChangeSubscriber subscriber) { stateChangeSubscribers.add(subscriber); } @Override public void unsubscribeToStateChange(StateChangeSubscriber subscriber) { stateChangeSubscribers.remove(subscriber); } @Override public Context createChildContext() { return new ContextImpl(rootContext); } /** * Allows it to obtain objects directly from the context of the game engine. It exists only for situations in * which no child context exists yet. If there is a child context then it automatically contains the objects of * the engine context. Thus normal code should just work with the (child) context that is available to it * instead of using this method. * * @return a object directly from the context of the game engine */ public <T> T getFromEngineContext(Class<? extends T> type) { return rootContext.get(type); } }