/* * Copyright 2015 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.Joiner; import com.google.common.collect.ImmutableList; import org.terasology.config.Config; import org.terasology.config.SystemConfig; import org.terasology.crashreporter.CrashReporter; import org.terasology.engine.modes.StateLoading; import org.terasology.engine.modes.StateMainMenu; import org.terasology.engine.paths.PathManager; import org.terasology.engine.subsystem.EngineSubsystem; import org.terasology.engine.subsystem.common.ConfigurationSubsystem; import org.terasology.engine.subsystem.common.ThreadManager; import org.terasology.engine.subsystem.common.hibernation.HibernationSubsystem; import org.terasology.engine.subsystem.headless.HeadlessAudio; import org.terasology.engine.subsystem.headless.HeadlessGraphics; import org.terasology.engine.subsystem.headless.HeadlessInput; import org.terasology.engine.subsystem.headless.HeadlessTimer; import org.terasology.engine.subsystem.headless.mode.HeadlessStateChangeListener; import org.terasology.engine.subsystem.headless.mode.StateHeadlessSetup; import org.terasology.engine.subsystem.lwjgl.LwjglAudio; import org.terasology.engine.subsystem.lwjgl.LwjglGraphics; import org.terasology.engine.subsystem.lwjgl.LwjglInput; import org.terasology.engine.subsystem.lwjgl.LwjglTimer; import org.terasology.game.GameManifest; import org.terasology.network.NetworkMode; import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameInfo; import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameProvider; import org.terasology.splash.SplashScreen; import org.terasology.splash.SplashScreenBuilder; import org.terasology.splash.overlay.AnimatedBoxRowOverlay; import org.terasology.splash.overlay.ImageOverlay; import org.terasology.splash.overlay.RectOverlay; import org.terasology.splash.overlay.TextOverlay; import org.terasology.splash.overlay.TriggerImageOverlay; import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.Rectangle; import java.io.IOException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; /** * Class providing the main() method for launching Terasology as a PC app. * <br><br> * Through the following launch arguments default locations to store logs and * game saves can be overridden, by using the current directory or a specified * one as the home directory. Furthermore, Terasology can be launched headless, * to save resources while acting as a server or to run in an environment with * no graphics, audio or input support. Additional arguments are available to * reload the latest game on startup and to disable crash reporting. * <br><br> * Available launch arguments: * <br><br> * <table summary="Launch arguments"> * <tbody> * <tr><td>-homedir</td><td>Use the current directory as the home directory.</td></tr> * <tr><td>-homedir=path</td><td>Use the specified path as the home directory.</td></tr> * <tr><td>-headless</td><td>Start headless.</td></tr> * <tr><td>-loadlastgame</td><td>Load the latest game on startup.</td></tr> * <tr><td>-noSaveGames</td><td>Disable writing of save games.</td></tr> * <tr><td>-noCrashReport</td><td>Disable crash reporting.</td></tr> * <tr><td>-noSound</td><td>Disable sound.</td></tr> * <tr><td>-noSplash</td><td>Disable splash screen.</td></tr> * <tr><td>-serverPort=xxxxx</td><td>Change the server port.</td></tr> * </tbody> * </table> * <br><br> * When used via command line an usage help and some examples can be obtained via: * <br><br> * terasology -help or terasology /? * <br><br> */ public final class Terasology { private static final String[] PRINT_USAGE_FLAGS = {"--help", "-help", "/help", "-h", "/h", "-?", "/?"}; private static final String USE_CURRENT_DIR_AS_HOME = "-homedir"; private static final String USE_SPECIFIED_DIR_AS_HOME = "-homedir="; private static final String START_HEADLESS = "-headless"; private static final String LOAD_LAST_GAME = "-loadlastgame"; private static final String NO_CRASH_REPORT = "-noCrashReport"; private static final String NO_SAVE_GAMES = "-noSaveGames"; private static final String NO_SOUND = "-noSound"; private static final String NO_SPLASH = "-noSplash"; private static final String SERVER_PORT = "-serverPort="; private static final String OVERRIDE_DEFAULT_CONFIG = "-overrideDefaultConfig="; private static boolean isHeadless; private static boolean crashReportEnabled = true; private static boolean soundEnabled = true; private static boolean splashEnabled = true; private static boolean loadLastGame; private Terasology() { } public static void main(String[] args) { handlePrintUsageRequest(args); handleLaunchArguments(args); SplashScreen splashScreen = splashEnabled ? configureSplashScreen() : SplashScreenBuilder.createStub(); splashScreen.post("Java Runtime " + System.getProperty("java.version") + " loaded"); setupLogging(); try { TerasologyEngineBuilder builder = new TerasologyEngineBuilder(); populateSubsystems(builder); TerasologyEngine engine = builder.build(); engine.subscribe(newStatus -> { if (newStatus == StandardGameStatus.RUNNING) { splashScreen.close(); } else { splashScreen.post(newStatus.getDescription()); } }); if (isHeadless) { engine.subscribeToStateChange(new HeadlessStateChangeListener(engine)); engine.run(new StateHeadlessSetup()); } else { if (loadLastGame) { engine.getFromEngineContext(ThreadManager.class).submitTask("loadGame", () -> { GameManifest gameManifest = getLatestGameManifest(); if (gameManifest != null) { engine.changeState(new StateLoading(gameManifest, NetworkMode.NONE)); } }); } engine.run(new StateMainMenu()); } } catch (Throwable e) { // also catch Errors such as UnsatisfiedLink, NoSuchMethodError, etc. splashScreen.close(); reportException(e); } } private static SplashScreen configureSplashScreen() { int imageHeight = 283; int maxTextWidth = 450; int width = 600; int height = 30; int left = 20; int top = imageHeight - height - 20; Rectangle rectRc = new Rectangle(left, top, width, height); Rectangle textRc = new Rectangle(left + 10, top + 5, maxTextWidth, height); Rectangle boxRc = new Rectangle(left + maxTextWidth + 10, top, width - maxTextWidth - 20, height); SplashScreenBuilder builder = new SplashScreenBuilder(); String[] imgFiles = new String[] { "splash_1.png", "splash_2.png", "splash_3.png", "splash_4.png", "splash_5.png" }; Point[] imgOffsets = new Point[] { new Point(0, 0), new Point(150, 0), new Point(300, 0), new Point(450, 0), new Point(630, 0) }; EngineStatus[] trigger = new EngineStatus[] { TerasologyEngineStatus.PREPARING_SUBSYSTEMS, TerasologyEngineStatus.INITIALIZING_MODULE_MANAGER, TerasologyEngineStatus.INITIALIZING_ASSET_TYPES, TerasologyEngineStatus.INITIALIZING_SUBSYSTEMS, TerasologyEngineStatus.INITIALIZING_ASSET_MANAGEMENT, }; try { for (int index = 0; index < 5; index++) { URL resource = Terasology.class.getResource("/splash/" + imgFiles[index]); builder.add(new TriggerImageOverlay(resource) .setTrigger(trigger[index].getDescription()) .setPosition(imgOffsets[index].x, imgOffsets[index].y)); } builder.add(new ImageOverlay(Terasology.class.getResource("/splash/splash_text.png"))); } catch (IOException e) { e.printStackTrace(); } SplashScreen instance = builder .add(new RectOverlay(rectRc)) .add(new TextOverlay(textRc)) .add(new AnimatedBoxRowOverlay(boxRc)) .build(); return instance; } private static void setupLogging() { Path path = PathManager.getInstance().getLogPath(); if (path == null) { path = Paths.get("logs"); } LoggingContext.initialize(path); } private static void handlePrintUsageRequest(String[] args) { for (String arg : args) { for (String usageArg : PRINT_USAGE_FLAGS) { if (usageArg.equals(arg.toLowerCase())) { printUsageAndExit(); } } } } private static void printUsageAndExit() { String printUsageFlags = Joiner.on("|").join(PRINT_USAGE_FLAGS); List<String> opts = ImmutableList.of( printUsageFlags, USE_CURRENT_DIR_AS_HOME + "|" + USE_SPECIFIED_DIR_AS_HOME + "<path>", START_HEADLESS, LOAD_LAST_GAME, NO_CRASH_REPORT, NO_SAVE_GAMES, NO_SOUND, NO_SPLASH, OVERRIDE_DEFAULT_CONFIG + "<path>", SERVER_PORT + "<port>"); StringBuilder optText = new StringBuilder(); for (String opt : opts) { optText.append(" [").append(opt).append("]"); } System.out.println("Usage:"); System.out.println(); System.out.println(" terasology" + optText.toString()); System.out.println(); System.out.println("By default Terasology saves data such as game saves and logs into subfolders of a platform-specific \"home directory\"."); System.out.println("Saving can be explicitly disabled using the \"" + NO_SAVE_GAMES + "\" flag."); System.out.println("Optionally, the user can override the default by using one of the following launch arguments:"); System.out.println(); System.out.println(" " + USE_CURRENT_DIR_AS_HOME + " Use the current directory as the home directory."); System.out.println(" " + USE_SPECIFIED_DIR_AS_HOME + "<path> Use the specified directory as the home directory."); System.out.println(); System.out.println("It is also possible to start Terasology in headless mode (no graphics), i.e. to act as a server."); System.out.println("For this purpose use the " + START_HEADLESS + " launch argument."); System.out.println(); System.out.println("To automatically load the latest game on startup,"); System.out.println("use the " + LOAD_LAST_GAME + " launch argument."); System.out.println(); System.out.println("By default Crash Reporting is enabled."); System.out.println("To disable this feature use the " + NO_CRASH_REPORT + " launch argument."); System.out.println(); System.out.println("To disable sound use the " + NO_SOUND + " launch argument (default in headless mode)."); System.out.println(); System.out.println("To disable the splash screen use the " + NO_SPLASH + " launch argument."); System.out.println(); System.out.println("To change the port the server is hosted on use the " + SERVER_PORT + " launch argument."); System.out.println(); System.out.println("To override the default generated config (useful for headless server) use the " + OVERRIDE_DEFAULT_CONFIG + " launch argument"); System.out.println(); System.out.println("Examples:"); System.out.println(); System.out.println(" Use the current directory as the home directory:"); System.out.println(" terasology " + USE_CURRENT_DIR_AS_HOME); System.out.println(); System.out.println(" Use \"myPath\" as the home directory:"); System.out.println(" terasology " + USE_SPECIFIED_DIR_AS_HOME + "myPath"); System.out.println(); System.out.println(" Start terasology in headless mode (no graphics) and enforce using the default port:"); System.out.println(" terasology " + START_HEADLESS + " " + SERVER_PORT + TerasologyConstants.DEFAULT_PORT); System.out.println(); System.out.println(" Load the latest game on startup and disable crash reporting"); System.out.println(" terasology " + LOAD_LAST_GAME + " " + NO_CRASH_REPORT); System.out.println(); System.out.println(" Don't start Terasology, just print this help:"); System.out.println(" terasology " + PRINT_USAGE_FLAGS[1]); System.out.println(); System.out.println("Alternatively use our standalone Launcher from: https://github.com/MovingBlocks/TerasologyLauncher/releases"); System.out.println(); System.exit(0); } private static void handleLaunchArguments(String[] args) { Path homePath = null; for (String arg : args) { boolean recognized = true; if (arg.startsWith(USE_SPECIFIED_DIR_AS_HOME)) { homePath = Paths.get(arg.substring(USE_SPECIFIED_DIR_AS_HOME.length())); } else if (arg.equals(USE_CURRENT_DIR_AS_HOME)) { homePath = Paths.get(""); } else if (arg.equals(START_HEADLESS)) { isHeadless = true; crashReportEnabled = false; splashEnabled = false; } else if (arg.equals(NO_SAVE_GAMES)) { System.setProperty(SystemConfig.SAVED_GAMES_ENABLED_PROPERTY, "false"); } else if (arg.equals(NO_CRASH_REPORT)) { crashReportEnabled = false; } else if (arg.equals(NO_SOUND)) { soundEnabled = false; } else if (arg.equals(NO_SPLASH)) { splashEnabled = false; } else if (arg.equals(LOAD_LAST_GAME)) { loadLastGame = true; } else if (arg.startsWith(SERVER_PORT)) { System.setProperty(ConfigurationSubsystem.SERVER_PORT_PROPERTY, arg.substring(SERVER_PORT.length())); } else if (arg.startsWith(OVERRIDE_DEFAULT_CONFIG)) { System.setProperty(Config.PROPERTY_OVERRIDE_DEFAULT_CONFIG, arg.substring(OVERRIDE_DEFAULT_CONFIG.length())); } else { recognized = false; } System.out.println((recognized ? "Recognized" : "Invalid") + " argument: " + arg); } try { if (homePath != null) { PathManager.getInstance().useOverrideHomePath(homePath); } else { PathManager.getInstance().useDefaultHomePath(); } } catch (IOException e) { reportException(e); System.exit(0); } } private static void populateSubsystems(TerasologyEngineBuilder builder) { if (isHeadless) { builder.add(new HeadlessGraphics()) .add(new HeadlessTimer()) .add(new HeadlessAudio()) .add(new HeadlessInput()); } else { EngineSubsystem audio = soundEnabled ? new LwjglAudio() : new HeadlessAudio(); builder.add(audio) .add(new LwjglGraphics()) .add(new LwjglTimer()) .add(new LwjglInput()); } builder.add(new HibernationSubsystem()); } private static void reportException(Throwable throwable) { Path logPath = LoggingContext.getLoggingPath(); if (!GraphicsEnvironment.isHeadless() && crashReportEnabled) { CrashReporter.report(throwable, logPath); } else { throwable.printStackTrace(); System.err.println("For more details, see the log files in " + logPath.toAbsolutePath().normalize()); } } private static GameManifest getLatestGameManifest() { GameInfo latestGame = null; List<GameInfo> savedGames = GameProvider.getSavedGames(); for (GameInfo savedGame : savedGames) { if (latestGame == null || savedGame.getTimestamp().after(latestGame.getTimestamp())) { latestGame = savedGame; } } if (latestGame == null) { return null; } return latestGame.getManifest(); } }