diff --git a/.gitignore b/.gitignore index 2e3ce01cc..cab7d48b0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ config/metrics ## Ignored sub-projects with nested Git roots. Core module is bundled with the main repo. android gwt +steam modules/* modules/**/build.gradle !modules/core @@ -27,6 +28,9 @@ gwt-unitCache/ www-test/ .gwt-tmp/ +# Steam +steam_appid.txt + ## Intellij .idea/ *.ipr diff --git a/Jenkinsfile b/Jenkinsfile index 393ce7e0c..0e43b8da2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -89,6 +89,42 @@ pipeline { discordSend title: env.BRANCH_NAME, link: env.BUILD_URL, result: currentBuild.currentResult, webhookURL: env.WEBHOOK } } + stage('Build Steam') { + when { + // Example: v2.1.0 + tag pattern: 'v\\d+\\.\\d+\\.\\d+.*', comparator: "REGEXP" + branch pattern: 'steam/*' + } + steps { + dir('steam') { + script { + // Allow varying from the default Steam repo path for easier development. Assume same Steam branch as engine branch. + def steamGitPath = "https://github.com/MovingBlocks/DestSolSteam.git" + if (env.PUBLISH_ORG) { + steamGitPath = steamGitPath.replace("MovingBlocks", env.PUBLISH_ORG) + println "Updated target Steam Git path to: " + steamGitPath + } else { + println "Not varying the Steam path from default " + steamGitPath + } + // Figure out a suitable target branch in the Steam repo, default is the develop branch + def steamBranch = "develop" + // Check to see if Jenkins is building a tag, branch, or other (including PRs) + if (env.TAG_NAME != null && env.TAG_NAME ==~ /v\d+\.\d+\.\d+.*/) { + println "Going to use target Steam tag " + env.TAG_NAME + steamBranch = "refs/tags/" + env.TAG_NAME + } else if (env.BRANCH_NAME.equalsIgnoreCase("master") || env.BRANCH_NAME.startsWith("steam/")) { + println "Going to use target unusual Steam branch " + env.BRANCH_NAME + steamBranch = env.BRANCH_NAME + } else { + println "Going to use target Steam branch 'develop' - not building 'master' nor anything starting with 'android/'" + } + checkout scm: [$class: 'GitSCM', branches: [[name: steamBranch]], extensions: [], userRemoteConfigs: [[credentialsId: 'GooeyHub', url: steamGitPath]]] + } + } + sh './gradlew distSteam' + zip dir: 'steam/build/distributions/app', zipFile: 'DestinationSolSteam.zip' + } + } stage('Publish to Play Store') { when { // Example: v2.1.0 diff --git a/build.gradle b/build.gradle index aab26bd80..f23b9c425 100644 --- a/build.gradle +++ b/build.gradle @@ -150,3 +150,28 @@ tasks.register('fetchAndroid') { ) } } + +tasks.register('fetchSteam') { + description = 'Git clones the Steam facade source from GitHub' + + // Repo name is the dynamic part of the task name + def repo = 'DestSolSteam' + + // Default GitHub account to use. Supply with -PgithubAccount="TargetAccountName" or via gradle.properties + def githubHome = 'MovingBlocks' + + def destination = file('steam') + + // Don't clone this repo if we already have a directory by that name (also determines Gradle UP-TO-DATE) + enabled = !destination.exists() + + doLast { + Grgit.clone( + // Do the actual clone if we don't have the directory already + uri: "https://github.com/$githubHome/" + repo + ".git", + //println "Fetching $repo from $uri" + dir: destination, + bare: false + ) + } +} \ No newline at end of file diff --git a/desktop/build.gradle b/desktop/build.gradle index 71c6ab12e..88cd9bc68 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25' implementation group: 'org.terasology.crashreporter', name: 'cr-destsol', version: '4.0.0' + annotationProcessor "org.terasology.gestalt:gestalt-inject-java:$gestaltVersion" } tasks.register('run', JavaExec) { diff --git a/desktop/src/main/java/org/destinationsol/desktop/DesktopLauncher.java b/desktop/src/main/java/org/destinationsol/desktop/DesktopLauncher.java new file mode 100644 index 000000000..4b1707853 --- /dev/null +++ b/desktop/src/main/java/org/destinationsol/desktop/DesktopLauncher.java @@ -0,0 +1,289 @@ +/* + * Copyright 2026 The Terasology Foundation + * + * 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 + * + * https://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.destinationsol.desktop; + +import com.badlogic.gdx.Files; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Graphics; +import org.destinationsol.GameOptions; +import org.destinationsol.SolApplication; +import org.destinationsol.SolFileReader; +import org.destinationsol.game.DebugOptions; +import org.destinationsol.modules.FacadeModuleConfig; +import org.destinationsol.ui.ResizeSubscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.context.Lifetime; +import org.terasology.crashreporter.CrashReporter; +import org.terasology.gestalt.di.ServiceRegistry; +import org.terasology.gestalt.module.ModulePathScanner; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Stream; + +public final class DesktopLauncher { + private static Logger logger = LoggerFactory.getLogger(DesktopLauncher.class); + + /** + * Specifies the commandline option to pass to the application for it to generate no crash reports. + */ + private static final String NO_CRASH_REPORT = "-noCrashReport"; + + /** + * Specifies the commandline option to pass to the application for it to not show a splash-screen. + */ + private static final String NO_SPLASH_SCREEN = "-noSplash"; + + /** + * The colour for the splash-screen logo to be shown in. + */ + private static final Color LOGO_COLOUR = Color.LIGHT_GRAY; + + /** + * This class is basically only a holder for the Java's {@code main(String[])} method, thus needs not to be + * instantiated. + */ + private DesktopLauncher() { + } + + public static void launchGame(String[] argv, Class facadeModuleConfigClass, + Class moduleScannerClass) { + launchGame(argv, facadeModuleConfigClass, moduleScannerClass, new ServiceRegistry()); + } + + public static void launchGame(String[] argv, Class facadeModuleConfigClass, + Class moduleScannerClass, ServiceRegistry extraRegistrations) { + SplashScreen splash = null; + try { + splash = SplashScreen.getSplashScreen(); + } catch (Exception e) { + e.printStackTrace(); + } + + boolean useSplash = (splash != null) && Stream.of(argv).noneMatch(s -> s.equals(NO_SPLASH_SCREEN)); + if (useSplash) { + Graphics2D splashScreenGraphics = splash.createGraphics(); + Rectangle splashBounds = splash.getBounds(); + splashScreenGraphics.setColor(LOGO_COLOUR); + splashScreenGraphics.setPaintMode(); + splashScreenGraphics.fillRect(0, 0, splashBounds.width, splashBounds.height); + splash.update(); + } + + Lwjgl3ApplicationConfiguration applicationConfig = new Lwjgl3ApplicationConfiguration(); + //TODO: Is checking for a presence of the file really the way we want to determine if it is a debug build? + handleDevBuild(applicationConfig); + DesktopLauncher.MyReader reader = new DesktopLauncher.MyReader(); + DebugOptions.read(reader); + + GameOptions options = new GameOptions(false, reader); + // Set screen width, height... + setScreenDimensions(applicationConfig, options); + + // Set the application's title, icon... + applicationConfig.setTitle("Destination Sol"); + if (DebugOptions.DEV_ROOT_PATH == null) { + applicationConfig.setWindowIcon(Files.FileType.Internal, "icon.png"); + } else { + applicationConfig.setWindowIcon(Files.FileType.Absolute, DebugOptions.DEV_ROOT_PATH + "/icon.png"); + } + + handleCrashReporting(argv); + + + if (useSplash) { + splash.close(); + } + // Everything is set up correctly, launch the application + DesktopLauncher.DesktopServices desktopServiceRegistry = new DesktopLauncher.DesktopServices(facadeModuleConfigClass, moduleScannerClass); + desktopServiceRegistry.includeRegistry(extraRegistrations); + SolApplication application = new SolApplication(100, desktopServiceRegistry); + SolApplication.addResizeSubscriber(new DesktopLauncher.FullScreenWindowPositionAdjustment(!options.fullscreen)); + // Everything is set up correctly, launch the application + new Lwjgl3Application(application, applicationConfig); + } + + /** + * When on dev build, use specific settings for vSync and FPS throttling. + * + * Whether a build is a dev build is found out by checking of a file "devBuild" in the root directory of DestSol. + * Those specific option means disabling vSync, and increasing foreground FPS throttling to allow for a swifter + * game, while lowering it for the background to not eat as much resources. Since game time flow is dependent on + * FPS, this also means that on dev build, the game may run faster in foreground than background, which is not + * something we exactly want. Also, since the default FPS for non-dev builds is 60, it ensures that the game will + * run at the same sane speed in production and the same speed in foreground as well as background. + * + * @param applicationConfig App config to configure. + */ + private static void handleDevBuild(Lwjgl3ApplicationConfiguration applicationConfig) { + boolean devBuild = java.nio.file.Files.exists(Paths.get("devBuild")); + if (devBuild) { + DebugOptions.DEV_ROOT_PATH = "engine/src/main/resources/"; // Lets the game run from source without a tweaked working directory + applicationConfig.useVsync(false); // Setting to false disables vertical sync + //The LWJGL3 backend does not support FPS throttling in the foreground + //applicationConfig.foregroundFPS = 100; // Use 0 to disable foreground fps throttling + //applicationConfig.backgroundFPS = 10; // Use 0 to disable background fps throttling + applicationConfig.setIdleFPS(10); + } + } + + /** + * When flag {@link #NO_CRASH_REPORT} is NOT passed in, overload the uncaught exception behaviour to create a crash + * dump and report the crash. + * + * @param argv App's cmdline args. + */ + private static void handleCrashReporting(String[] argv) { + if (Stream.of(argv).noneMatch(s -> s.equals(NO_CRASH_REPORT))) { + Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { + // Get the exception stack trace string + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + ex.printStackTrace(printWriter); + String exceptionString = stringWriter.getBuffer().toString(); + logger.error("This exception was not caught:", ex); + + // Create a crash dump file + String fileName = "crash-" + new SimpleDateFormat("yyyy-dd-MM_HH-mm-ss").format(new Date()) + ".log"; + java.util.List lines = Collections.singletonList(exceptionString); + Path logPath = Paths.get(new DesktopLauncher.MyReader().create(fileName, lines)).getParent(); + + // Run asynchronously so that the error message view is not blocked + new Thread(() -> CrashReporter.report(ex, logPath)).start(); + }); + } + } + + /** + * Set up window resolution. + * + * When flag {@link DebugOptions#EMULATE_MOBILE} is set, make the app window the size of mobile screen. Otherwise, + * load the window resolution from game options. + * + * @param applicationConfig App config to configure + * @param options {@link GameOptions} the configuration to read from. + */ + private static void setScreenDimensions(Lwjgl3ApplicationConfiguration applicationConfig, GameOptions options) { + if (DebugOptions.EMULATE_MOBILE) { + applicationConfig.setWindowedMode(640, 480); + } else { + if (options.fullscreen) { + com.badlogic.gdx.Graphics.DisplayMode mode = null; + for (com.badlogic.gdx.Graphics.DisplayMode displayMode : Lwjgl3ApplicationConfiguration.getDisplayModes()) { + if (displayMode.width == options.x && displayMode.height == options.y) { + mode = displayMode; + } + } + if (mode != null) { + applicationConfig.setFullscreenMode(mode); + } else { + logger.warn("The resolution {}x{} is not supported in fullscreen mode!", options.x, options.y); + } + } else { + applicationConfig.setWindowedMode(options.x, options.y); + } + } + } + + private static class DesktopServices extends ServiceRegistry { + public DesktopServices(Class facadeModuleConfigClass, Class moduleScannerClass) { + this.with(FacadeModuleConfig.class).lifetime(Lifetime.Singleton).use(facadeModuleConfigClass); + this.with(ModulePathScanner.class).lifetime(Lifetime.Singleton).use(moduleScannerClass); + } + } + + /** + * Provides the implementation of SolFileReader used by this class. + */ + //TODO Since this is currently the only implementation of SolFileReader, consider making this into a self-standing class with static methods. Also, consider uniting SolFileReader and IniReader. + private static class MyReader implements SolFileReader { + @Override + public String create(String fileName, java.util.List lines) { + String path = ""; + if (DebugOptions.DEV_ROOT_PATH != null) { + path = DebugOptions.DEV_ROOT_PATH; + } + path += fileName; + + Path file = Paths.get(path); + try { + java.nio.file.Files.write(file, lines, Charset.forName("UTF-8")); + } catch (IOException e) { + logger.error("Failed to write to file", e); + } + return file.toAbsolutePath().toString(); + } + + @Override + public List read(String fileName) { + String path = ""; + if (DebugOptions.DEV_ROOT_PATH != null) { + path = DebugOptions.DEV_ROOT_PATH; + } + path += fileName; + + ArrayList lines = new ArrayList<>(); + + try { + BufferedReader br = new BufferedReader(new FileReader(path)); + String line; + while ((line = br.readLine()) != null) { + lines.add(line); + } + br.close(); + } catch (IOException ignore) { + } + + return lines; + } + } + + private static final class FullScreenWindowPositionAdjustment implements ResizeSubscriber { + private boolean lastFullScreenState; + + public FullScreenWindowPositionAdjustment(boolean lastFullScreenState) { + this.lastFullScreenState = lastFullScreenState; + } + + @Override + public void resize() { + //If the game has gone from full-screen to windowed + if (lastFullScreenState && !Gdx.graphics.isFullscreen()) { + Graphics.DisplayMode mode = Gdx.graphics.getDisplayMode(); + ((Lwjgl3Graphics) Gdx.graphics).getWindow().setPosition(mode.width / 4, mode.height / 4); + } + + lastFullScreenState = Gdx.graphics.isFullscreen(); + } + } +} diff --git a/desktop/src/main/java/org/destinationsol/desktop/SolDesktop.java b/desktop/src/main/java/org/destinationsol/desktop/SolDesktop.java index 97ce2e947..e83f64b2c 100644 --- a/desktop/src/main/java/org/destinationsol/desktop/SolDesktop.java +++ b/desktop/src/main/java/org/destinationsol/desktop/SolDesktop.java @@ -15,73 +15,25 @@ */ package org.destinationsol.desktop; -import com.badlogic.gdx.Files; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Graphics; -import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; -import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; -import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Graphics; -import org.destinationsol.GameOptions; import org.destinationsol.modules.FacadeModuleConfig; -import org.destinationsol.modules.ModuleManager; import org.destinationsol.SolApplication; -import org.destinationsol.SolFileReader; -import org.destinationsol.game.DebugOptions; -import org.destinationsol.ui.ResizeSubscriber; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.terasology.context.Lifetime; -import org.terasology.crashreporter.CrashReporter; -import org.terasology.gestalt.di.ServiceRegistry; import org.terasology.gestalt.module.Module; import org.terasology.gestalt.module.ModuleEnvironment; import org.terasology.gestalt.module.ModuleFactory; import org.terasology.gestalt.module.ModulePathScanner; import org.terasology.gestalt.module.sandbox.JavaModuleClassLoader; -import java.awt.Graphics2D; -import java.awt.Color; -import java.awt.SplashScreen; -import java.awt.Rectangle; -import java.io.BufferedReader; +import javax.inject.Inject; import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.charset.Charset; -import java.nio.file.Path; import java.nio.file.Paths; -import java.text.SimpleDateFormat; -import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.stream.Stream; /** * This class is the desktop (PC) entry point for the whole DestinationSol application. It handles the creation and * launching of LwjglApplication from {@link SolApplication}. */ public final class SolDesktop { - - private static Logger logger = LoggerFactory.getLogger(SolDesktop.class); - - /** - * Specifies the commandline option to pass to the application for it to generate no crash reports. - */ - private static final String NO_CRASH_REPORT = "-noCrashReport"; - - /** - * Specifies the commandline option to pass to the application for it to not show a splash-screen. - */ - private static final String NO_SPLASH_SCREEN = "-noSplash"; - - /** - * The colour for the splash-screen logo to be shown in. - */ - private static final Color LOGO_COLOUR = Color.LIGHT_GRAY; - /** * This class is basically only a holder for the Java's {@code main(String[])} method, thus needs not to be * instantiated. @@ -90,147 +42,17 @@ private SolDesktop() { } public static void main(String[] argv) { - SplashScreen splash = null; - try { - splash = SplashScreen.getSplashScreen(); - } catch (Exception e) { - e.printStackTrace(); - } - - boolean useSplash = (splash != null) && Stream.of(argv).noneMatch(s -> s.equals(NO_SPLASH_SCREEN)); - if (useSplash) { - Graphics2D splashScreenGraphics = splash.createGraphics(); - Rectangle splashBounds = splash.getBounds(); - splashScreenGraphics.setColor(LOGO_COLOUR); - splashScreenGraphics.setPaintMode(); - splashScreenGraphics.fillRect(0, 0, splashBounds.width, splashBounds.height); - splash.update(); - } - - Lwjgl3ApplicationConfiguration applicationConfig = new Lwjgl3ApplicationConfiguration(); - //TODO: Is checking for a presence of the file really the way we want to determine if it is a debug build? - handleDevBuild(applicationConfig); - MyReader reader = new MyReader(); - DebugOptions.read(reader); - - GameOptions options = new GameOptions(false, reader); - // Set screen width, height... - setScreenDimensions(applicationConfig, options); - - // Set the application's title, icon... - applicationConfig.setTitle("Destination Sol"); - if (DebugOptions.DEV_ROOT_PATH == null) { - applicationConfig.setWindowIcon(Files.FileType.Internal, "icon.png"); - } else { - applicationConfig.setWindowIcon(Files.FileType.Absolute, DebugOptions.DEV_ROOT_PATH + "/icon.png"); - } - - handleCrashReporting(argv); - - - if (useSplash) { - splash.close(); - } - // Everything is set up correctly, launch the application - SolApplication application = new SolApplication(100, new DesktopServices()); - SolApplication.addResizeSubscriber(new SolDesktop.FullScreenWindowPositionAdjustment(!options.fullscreen)); - // Everything is set up correctly, launch the application - new Lwjgl3Application(application, applicationConfig); + DesktopLauncher.launchGame(argv, DesktopModuleConfig.class, ModulePathScanner.class); } - /** - * When on dev build, use specific settings for vSync and FPS throttling. - * - * Whether a build is a dev build is found out by checking of a file "devBuild" in the root directory of DestSol. - * Those specific option means disabling vSync, and increasing foreground FPS throttling to allow for a swifter - * game, while lowering it for the background to not eat as much resources. Since game time flow is dependent on - * FPS, this also means that on dev build, the game may run faster in foreground than background, which is not - * something we exactly want. Also, since the default FPS for non-dev builds is 60, it ensures that the game will - * run at the same sane speed in production and the same speed in foreground as well as background. - * - * @param applicationConfig App config to configure. - */ - private static void handleDevBuild(Lwjgl3ApplicationConfiguration applicationConfig) { - boolean devBuild = java.nio.file.Files.exists(Paths.get("devBuild")); - if (devBuild) { - DebugOptions.DEV_ROOT_PATH = "engine/src/main/resources/"; // Lets the game run from source without a tweaked working directory - applicationConfig.useVsync(false); // Setting to false disables vertical sync - //The LWJGL3 backend does not support FPS throttling in the foreground - //applicationConfig.foregroundFPS = 100; // Use 0 to disable foreground fps throttling - //applicationConfig.backgroundFPS = 10; // Use 0 to disable background fps throttling - applicationConfig.setIdleFPS(10); + public static class DesktopModuleConfig implements FacadeModuleConfig { + @Inject + public DesktopModuleConfig() { } - } - - /** - * When flag {@link #NO_CRASH_REPORT} is NOT passed in, overload the uncaught exception behaviour to create a crash - * dump and report the crash. - * - * @param argv App's cmdline args. - */ - private static void handleCrashReporting(String[] argv) { - if (Stream.of(argv).noneMatch(s -> s.equals(NO_CRASH_REPORT))) { - Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { - // Get the exception stack trace string - StringWriter stringWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(stringWriter); - ex.printStackTrace(printWriter); - String exceptionString = stringWriter.getBuffer().toString(); - logger.error("This exception was not caught:", ex); - // Create a crash dump file - String fileName = "crash-" + new SimpleDateFormat("yyyy-dd-MM_HH-mm-ss").format(new Date()) + ".log"; - List lines = Collections.singletonList(exceptionString); - Path logPath = Paths.get(new MyReader().create(fileName, lines)).getParent(); - - // Run asynchronously so that the error message view is not blocked - new Thread(() -> CrashReporter.report(ex, logPath)).start(); - }); - } - } - - /** - * Set up window resolution. - * - * When flag {@link DebugOptions#EMULATE_MOBILE} is set, make the app window the size of mobile screen. Otherwise, - * load the window resolution from game options. - * - * @param applicationConfig App config to configure - * @param options {@link GameOptions} the configuration to read from. - */ - private static void setScreenDimensions(Lwjgl3ApplicationConfiguration applicationConfig, GameOptions options) { - if (DebugOptions.EMULATE_MOBILE) { - applicationConfig.setWindowedMode(640, 480); - } else { - if (options.fullscreen) { - Graphics.DisplayMode mode = null; - for (Graphics.DisplayMode displayMode : Lwjgl3ApplicationConfiguration.getDisplayModes()) { - if (displayMode.width == options.x && displayMode.height == options.y) { - mode = displayMode; - } - } - if (mode != null) { - applicationConfig.setFullscreenMode(mode); - } else { - logger.warn("The resolution {}x{} is not supported in fullscreen mode!", options.x, options.y); - } - } else { - applicationConfig.setWindowedMode(options.x, options.y); - } - } - } - - private static class DesktopServices extends ServiceRegistry { - public DesktopServices() { - this.with(FacadeModuleConfig.class).lifetime(Lifetime.Singleton).use(DesktopModuleConfig::new); - this.with(ModulePathScanner.class).lifetime(Lifetime.Singleton); - } - } - - private static class DesktopModuleConfig implements FacadeModuleConfig { @Override - public File getModulesPath() { - return Paths.get(".").resolve("modules").toFile(); + public Collection getModulePaths() { + return Collections.singletonList(Paths.get(".").resolve("modules").toFile()); } @Override @@ -253,69 +75,4 @@ public Class[] getAPIClasses() { return new Class[0]; } } - - /** - * Provides the implementation of SolFileReader used by this class. - */ - //TODO Since this is currently the only implementation of SolFileReader, consider making this into a self-standing class with static methods. Also, consider uniting SolFileReader and IniReader. - private static class MyReader implements SolFileReader { - @Override - public String create(String fileName, List lines) { - String path = ""; - if (DebugOptions.DEV_ROOT_PATH != null) { - path = DebugOptions.DEV_ROOT_PATH; - } - path += fileName; - - Path file = Paths.get(path); - try { - java.nio.file.Files.write(file, lines, Charset.forName("UTF-8")); - } catch (IOException e) { - logger.error("Failed to write to file", e); - } - return file.toAbsolutePath().toString(); - } - - @Override - public List read(String fileName) { - String path = ""; - if (DebugOptions.DEV_ROOT_PATH != null) { - path = DebugOptions.DEV_ROOT_PATH; - } - path += fileName; - - ArrayList lines = new ArrayList<>(); - - try { - BufferedReader br = new BufferedReader(new FileReader(path)); - String line; - while ((line = br.readLine()) != null) { - lines.add(line); - } - br.close(); - } catch (IOException ignore) { - } - - return lines; - } - } - - private static final class FullScreenWindowPositionAdjustment implements ResizeSubscriber { - private boolean lastFullScreenState; - - public FullScreenWindowPositionAdjustment(boolean lastFullScreenState) { - this.lastFullScreenState = lastFullScreenState; - } - - @Override - public void resize() { - //If the game has gone from full-screen to windowed - if (lastFullScreenState && !Gdx.graphics.isFullscreen()) { - Graphics.DisplayMode mode = Gdx.graphics.getDisplayMode(); - ((Lwjgl3Graphics) Gdx.graphics).getWindow().setPosition(mode.width / 4, mode.height / 4); - } - - lastFullScreenState = Gdx.graphics.isFullscreen(); - } - } } diff --git a/engine/src/main/java/org/destinationsol/assets/AssetHelper.java b/engine/src/main/java/org/destinationsol/assets/AssetHelper.java index 79af1120d..e3b803ca4 100644 --- a/engine/src/main/java/org/destinationsol/assets/AssetHelper.java +++ b/engine/src/main/java/org/destinationsol/assets/AssetHelper.java @@ -21,6 +21,7 @@ import org.destinationsol.assets.sound.AndroidOggSoundFileFormat; import org.destinationsol.assets.sound.OggSound; import org.destinationsol.assets.sound.OggSoundData; +import org.destinationsol.assets.ui.UIDeltaFormat; import org.destinationsol.assets.ui.UIFormat; import org.destinationsol.assets.ui.UISkinFormat; import org.slf4j.Logger; @@ -99,7 +100,9 @@ public void init(ModuleEnvironment environment, // TODO inject this assetTypeManager.createAssetType(UIElement.class, UIElement::new, "ui"); - ((AssetFileDataProducer) assetTypeManager.getAssetType(UIElement.class).get().getProducers().get(0)).addAssetFormat(new UIFormat(widgetLibrary,beanContext)); + UIFormat uiFormat = new UIFormat(widgetLibrary,beanContext); + ((AssetFileDataProducer) assetTypeManager.getAssetType(UIElement.class).get().getProducers().get(0)).addAssetFormat(uiFormat); + ((AssetFileDataProducer) assetTypeManager.getAssetType(UIElement.class).get().getProducers().get(0)).addDeltaFormat(new UIDeltaFormat(uiFormat)); assetTypeManager.switchEnvironment(environment); Assets.initialize(this); diff --git a/engine/src/main/java/org/destinationsol/assets/json/JsonDeltaFileFormat.java b/engine/src/main/java/org/destinationsol/assets/json/JsonDeltaFileFormat.java index 8aa060c66..214693540 100644 --- a/engine/src/main/java/org/destinationsol/assets/json/JsonDeltaFileFormat.java +++ b/engine/src/main/java/org/destinationsol/assets/json/JsonDeltaFileFormat.java @@ -17,8 +17,7 @@ import com.badlogic.gdx.files.FileHandle; import org.destinationsol.assets.AssetDataFileHandle; -import org.json.JSONArray; -import org.json.JSONException; +import org.destinationsol.util.JSONMerger; import org.json.JSONObject; import org.terasology.gestalt.assets.format.AbstractAssetAlterationFileFormat; import org.terasology.gestalt.assets.format.AssetDataFile; @@ -47,61 +46,6 @@ public void apply(AssetDataFile input, JsonData assetData) throws IOException { JSONObject deltaJsonValue = new JSONObject(handle.readString()); JSONObject jsonValue = assetData.getJsonValue(); - mergeObjects(jsonValue, deltaJsonValue); - } - - /** - * This method merges the JSONObject input with its delta by recursively checking for differing values. - * - * If a value does not exist in the delta, then the original input value is preserved. Otherwise, if the value is - * a primitive type (excluding array), then the delta value will override the input value. For JSONObject values, - * this method is called recursively to merge the sub-objects together. In the case of arrays, all of the values - * in the delta array are appended to the input array. - * - * @param input the JSONObject to merge into - * @param delta the JSONObject to merge with - */ - private void mergeObjects(JSONObject input, JSONObject delta) { - for (String key : input.keySet()) { - Object subObject = input.get(key); - if (!delta.has(key)) { - // Value is not modified - continue; - } - - if (subObject instanceof JSONObject) { - Object deltaObject = delta.get(key); - if (deltaObject instanceof JSONObject) { - mergeObjects((JSONObject) subObject, (JSONObject) deltaObject); - } else { - throw new JSONException("Error when parsing delta: Type " + deltaObject.getClass().getSimpleName() + " does not equal JSONObject"); - } - - continue; - } - - if (subObject instanceof JSONArray) { - Object deltaObject = delta.get(key); - if (deltaObject instanceof JSONArray) { - mergeArray((JSONArray) subObject, (JSONArray) deltaObject); - } else { - throw new JSONException("Error when parsing delta: Type " + deltaObject.getClass().getSimpleName() + " does not equal JSONArray"); - } - - continue; - } - - // Assume that a primitive type is used (primitive types cannot be merged, only overridden) - input.put(key, delta.get(key)); - } - } - - /** - * Merges the input with its delta by adding all values from the delta to the input. - */ - private void mergeArray(JSONArray input, JSONArray delta) { - for (int index = 0; index < delta.length(); index++) { - input.put(delta.get(index)); - } + JSONMerger.merge(jsonValue, deltaJsonValue); } } diff --git a/engine/src/main/java/org/destinationsol/assets/ui/UIDeltaFormat.java b/engine/src/main/java/org/destinationsol/assets/ui/UIDeltaFormat.java new file mode 100644 index 000000000..e247c1c1f --- /dev/null +++ b/engine/src/main/java/org/destinationsol/assets/ui/UIDeltaFormat.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 The Terasology Foundation + * + * 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 + * + * https://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.destinationsol.assets.ui; + + +import com.badlogic.gdx.files.FileHandle; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; +import org.destinationsol.assets.AssetDataFileHandle; +import org.destinationsol.util.JSONMerger; +import org.json.JSONObject; +import org.terasology.gestalt.assets.format.AbstractAssetAlterationFileFormat; +import org.terasology.gestalt.assets.format.AssetDataFile; +import org.terasology.gestalt.assets.module.annotations.RegisterAssetDeltaFileFormat; +import org.terasology.nui.asset.UIData; + +import javax.inject.Inject; +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Field; +import java.security.AccessController; +import java.security.PrivilegedAction; + +@RegisterAssetDeltaFileFormat +public class UIDeltaFormat extends AbstractAssetAlterationFileFormat { + private final UIFormat uiFormat; + + @Inject + public UIDeltaFormat(UIFormat uiFormat) { + super("ui"); + this.uiFormat = uiFormat; + } + + @Override + public void apply(AssetDataFile input, UIData assetData) throws IOException { + FileHandle handle = new AssetDataFileHandle(input); + JSONObject deltaJsonValue = new JSONObject(handle.readString()); + + JSONObject jsonValue = new JSONObject(new AssetDataFileHandle(assetData.getSource()).readString()); + JSONMerger.merge(jsonValue, deltaJsonValue); + + JsonReader jsonReader = new JsonReader(new StringReader(jsonValue.toString())); + jsonReader.setLenient(true); + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + Field widgetField = assetData.getClass().getDeclaredField("rootWidget"); + widgetField.setAccessible(true); + widgetField.set(assetData, uiFormat.load(new JsonParser().parse(jsonReader)).getRootWidget()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + }); + } +} diff --git a/engine/src/main/java/org/destinationsol/modules/FacadeModuleConfig.java b/engine/src/main/java/org/destinationsol/modules/FacadeModuleConfig.java index c1cc24770..018611ee1 100644 --- a/engine/src/main/java/org/destinationsol/modules/FacadeModuleConfig.java +++ b/engine/src/main/java/org/destinationsol/modules/FacadeModuleConfig.java @@ -2,8 +2,11 @@ import org.terasology.gestalt.module.Module; import org.terasology.gestalt.module.ModuleEnvironment; +import org.terasology.gestalt.module.ModuleFactory; import java.io.File; +import java.util.Collection; +import java.util.Collections; /** * This interface defines the module configuration for a given facade. Different facades will have different implementations @@ -11,10 +14,10 @@ */ public interface FacadeModuleConfig { /** - * Returns the root folder to search for modules in. All modules should be located within this root folder. - * @return the root module path + * Returns a collection of root folders to search for modules in. All modules should be located within these folders. + * @return the root module paths */ - File getModulesPath(); + Collection getModulePaths(); /** * Determines if the game uses SecurityManager for gestalt sandboxing. @@ -35,6 +38,18 @@ public interface FacadeModuleConfig { */ Module createEngineModule(); + /** + * Constructs facade-specific modules from the base classpath and returns them. + * @return the constructed facade-specific modules + */ + default Collection createFacadeModules() { + return Collections.emptyList(); + } + + default ModuleFactory createModuleFactory() { + return new ModuleFactory(); + } + /** * Returns a list of classes that should be accessible from within the sandbox. * Any classes not part of the built-in list or this one cannot be used in module code. diff --git a/engine/src/main/java/org/destinationsol/modules/ModuleManager.java b/engine/src/main/java/org/destinationsol/modules/ModuleManager.java index 741c770e4..34b3936a4 100644 --- a/engine/src/main/java/org/destinationsol/modules/ModuleManager.java +++ b/engine/src/main/java/org/destinationsol/modules/ModuleManager.java @@ -223,9 +223,9 @@ public class ModuleManager implements AutoCloseable { private Set builtInModules; @Inject - public ModuleManager(BeanContext beanContext, ModuleFactory moduleFactory, ModuleRegistry moduleRegistry, + public ModuleManager(BeanContext beanContext, ModuleRegistry moduleRegistry, ModulePathScanner scanner, FacadeModuleConfig moduleConfig) { - this.moduleFactory = moduleFactory; + this.moduleFactory = moduleConfig.createModuleFactory(); this.registry = moduleRegistry; this.scanner = scanner; this.beanContext = beanContext; @@ -238,11 +238,13 @@ public void init() throws Exception { Module nuiModule = moduleFactory.createPackageModule(new ModuleMetadata(new Name("nui"), new Version("2.0.0")),"org.terasology.nui"); // scan for all standard modules - File modulesRoot = moduleConfig.getModulesPath(); - scanner.scan(registry, modulesRoot); + for (File modulesRoot : moduleConfig.getModulePaths()) { + scanner.scan(registry, modulesRoot); + } builtInModules = Sets.newHashSet(); builtInModules.add(engineModule); + builtInModules.addAll(moduleConfig.createFacadeModules()); builtInModules.add(nuiModule); registry.addAll(builtInModules); diff --git a/engine/src/main/java/org/destinationsol/ui/nui/NUIManager.java b/engine/src/main/java/org/destinationsol/ui/nui/NUIManager.java index 922379545..a8c161edc 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/NUIManager.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/NUIManager.java @@ -22,9 +22,9 @@ import org.destinationsol.SolApplication; import org.destinationsol.assets.Assets; import org.destinationsol.assets.sound.OggSound; -import org.destinationsol.game.context.Context; import org.destinationsol.ui.UiDrawer; import org.joml.Vector2i; +import org.terasology.gestalt.di.BeanContext; import org.terasology.input.InputType; import org.terasology.input.Keyboard; import org.terasology.input.MouseInput; @@ -100,7 +100,7 @@ public class NUIManager { /** * The current game context used to initialise UI screens. */ - private Context context; + private BeanContext context; /** * The baseline UI scale used on Android. */ @@ -136,7 +136,7 @@ public class NUIManager { */ @Inject public NUIManager(SolApplication solApplication, - Context context, + BeanContext context, CommonDrawer commonDrawer, GameOptions options, UiDrawer uiDrawer, @@ -321,6 +321,7 @@ public NUIScreenLayer createScreen(String uri) { if (rootWidget instanceof NUIScreenLayer) { NUIScreenLayer screen = (NUIScreenLayer) rootWidget; if (!alreadyLoaded) { + context.inject(screen); screen.initialise(); } return screen; @@ -431,7 +432,7 @@ public UISkin getDefaultSkin() { * Sets the game context to be used by all UI screens. Newly-added screens will the use this context. * @param context the new context to use */ - public void setContext(Context context) { + public void setContext(BeanContext context) { this.context = context; } diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ModulesScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ModulesScreen.java index 7c0119f04..4cbf0f8e2 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ModulesScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/ModulesScreen.java @@ -19,7 +19,9 @@ import org.destinationsol.SolApplication; import org.destinationsol.modules.ModuleManager; import org.destinationsol.ui.nui.NUIScreenLayer; +import org.destinationsol.ui.nui.widgets.KeyActivatedButton; import org.terasology.gestalt.module.Module; +import org.terasology.nui.backends.libgdx.GDXInputUtil; import org.terasology.nui.databinding.ReadOnlyBinding; import org.terasology.nui.itemRendering.StringTextRenderer; import org.terasology.nui.widgets.UIButton; @@ -30,6 +32,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; /** * This screen is used to select the modules that should be active when playing a particular save. @@ -39,6 +42,7 @@ public class ModulesScreen extends NUIScreenLayer { private final SolApplication solApplication; private final ModuleManager moduleManager; + private UIList moduleList; private Set selectedModules; @Inject @@ -51,10 +55,8 @@ public ModulesScreen(SolApplication solApplication, ModuleManager moduleManager) public void initialise() { selectedModules = new HashSet<>(); - UIList moduleList = find("modulesList", UIList.class); - List modules = new ArrayList<>(moduleManager.getEnvironment().getModulesOrderedByDependencies()); - modules.removeAll(moduleManager.getBuiltInModules()); - moduleList.setList(modules); + moduleList = find("modulesList", UIList.class); + moduleList.setItemRenderer(new StringTextRenderer() { @Override public String getString(Module value) { @@ -93,12 +95,20 @@ public Boolean get() { }); deactivateButton.subscribe(button -> selectedModules.remove(moduleList.getSelection())); - UIButton confirmButton = find("confirmButton", UIButton.class); + KeyActivatedButton confirmButton = find("confirmButton", KeyActivatedButton.class); + confirmButton.setKey(GDXInputUtil.GDXToNuiKey(solApplication.getOptions().getKeyEscape())); confirmButton.subscribe(button -> { nuiManager.setScreen(solApplication.getMenuScreens().newShip); }); } + @Override + public void onAdded() { + List modules = new ArrayList<>(moduleManager.getRegistry().getModuleIds().stream().map(moduleId -> moduleManager.getRegistry().getLatestModuleVersion(moduleId)).collect(Collectors.toList())); + modules.removeAll(moduleManager.getBuiltInModules()); + moduleList.setList(modules); + } + public Set getSelectedModules() { return selectedModules; } diff --git a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java index d2b6dcdc4..5dc9a3a74 100644 --- a/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java +++ b/engine/src/main/java/org/destinationsol/ui/nui/screens/mainMenu/NewShipScreen.java @@ -76,26 +76,9 @@ public void initialise() { ((UIButton)button).setText("Systems: " + worldConfig.getNumberOfSystems()); }); - for (ResourceUrn configUrn : Assets.getAssetHelper().listAssets(Json.class, "playerSpawnConfig")) { - JSONObject playerSpawnConfigs = Validator.getValidatedJSON(configUrn.toString(), "engine:schemaPlayerSpawnConfig"); - playerSpawnConfigNames.addAll(playerSpawnConfigs.keySet()); - for (String spawnConfigName : playerSpawnConfigs.keySet()) { - JSONObject playerSpawnConfig = playerSpawnConfigs.getJSONObject(spawnConfigName); - try { - playerSpawnConfigTextures.add(Assets.getDSTexture(playerSpawnConfig.getString("hull")).getUiTexture()); - } catch (RuntimeException e) { - logger.error("Failed to load ship texture!", e); - // Null values will not render any texture. - playerSpawnConfigTextures.add(null); - } - } - } - UIImage shipPreviewImage = find("shipPreviewImage", UIImage.class); - shipPreviewImage.setImage(playerSpawnConfigTextures.get(playerSpawnConfigIndex)); UIButton startingShipButton = find("startingShipButton", UIButton.class); - startingShipButton.setText("Starting Ship: " + playerSpawnConfigNames.get(playerSpawnConfigIndex)); startingShipButton.subscribe(button -> { playerSpawnConfigIndex = (playerSpawnConfigIndex + 1) % playerSpawnConfigNames.size(); ((UIButton)button).setText("Starting Ship: " + playerSpawnConfigNames.get(playerSpawnConfigIndex)); @@ -129,22 +112,42 @@ public void initialise() { public void onAdded() { worldConfig.setSeed(System.currentTimeMillis()); - String currentShip = playerSpawnConfigNames.get(playerSpawnConfigIndex); + String currentShip = null; + if (playerSpawnConfigIndex < playerSpawnConfigNames.size()) { + currentShip = playerSpawnConfigNames.get(playerSpawnConfigIndex); + } playerSpawnConfigNames.clear(); + playerSpawnConfigTextures.clear(); Set configUrns = Assets.getAssetHelper().listAssets(Json.class, "playerSpawnConfig"); for (Module module : worldConfig.getModules()) { ResourceUrn configUrn = new ResourceUrn(module.getId(), new Name("playerSpawnConfig")); if (configUrns.contains(configUrn)) { - playerSpawnConfigNames.addAll(Validator.getValidatedJSON(configUrn.toString(), "engine:schemaPlayerSpawnConfig").keySet()); + JSONObject playerSpawnConfigs = Validator.getValidatedJSON(configUrn.toString(), "engine:schemaPlayerSpawnConfig"); + playerSpawnConfigNames.addAll(playerSpawnConfigs.keySet()); + for (String spawnConfigName : playerSpawnConfigs.keySet()) { + JSONObject playerSpawnConfig = playerSpawnConfigs.getJSONObject(spawnConfigName); + try { + playerSpawnConfigTextures.add(Assets.getDSTexture(playerSpawnConfig.getString("hull")).getUiTexture()); + } catch (RuntimeException e) { + logger.error("Failed to load ship texture!", e); + // Null values will not render any texture. + playerSpawnConfigTextures.add(null); + } + } } } if (!playerSpawnConfigNames.contains(currentShip)) { // The player picked a ship that's now invalid, so reset their selection. playerSpawnConfigIndex = 0; - UIButton startingShipButton = find("startingShipButton", UIButton.class); - startingShipButton.setText("Starting Ship: " + playerSpawnConfigNames.get(playerSpawnConfigIndex)); + } else { + playerSpawnConfigIndex = playerSpawnConfigNames.indexOf(currentShip); } + + UIButton startingShipButton = find("startingShipButton", UIButton.class); + startingShipButton.setText("Starting Ship: " + playerSpawnConfigNames.get(playerSpawnConfigIndex)); + UIImage shipPreviewImage = find("shipPreviewImage", UIImage.class); + shipPreviewImage.setImage(playerSpawnConfigTextures.get(playerSpawnConfigIndex)); } @Override diff --git a/engine/src/main/java/org/destinationsol/util/JSONMerger.java b/engine/src/main/java/org/destinationsol/util/JSONMerger.java new file mode 100644 index 000000000..7d568213a --- /dev/null +++ b/engine/src/main/java/org/destinationsol/util/JSONMerger.java @@ -0,0 +1,81 @@ +/* + * Copyright 2026 The Terasology Foundation + * + * 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 + * + * https://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.destinationsol.util; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public final class JSONMerger { + private JSONMerger() { + } + + /** + * This method merges the JSONObject input with its delta by recursively checking for differing values. + * + * If a value does not exist in the delta, then the original input value is preserved. Otherwise, if the value is + * a primitive type (excluding array), then the delta value will override the input value. For JSONObject values, + * this method is called recursively to merge the sub-objects together. In the case of arrays, all of the values + * in the delta array are appended to the input array. + * + * @param input the JSONObject to merge into + * @param delta the JSONObject to merge with + */ + public static void merge(JSONObject input, JSONObject delta) { + for (String key : input.keySet()) { + Object subObject = input.get(key); + if (!delta.has(key)) { + // Value is not modified + continue; + } + + if (subObject instanceof JSONObject) { + Object deltaObject = delta.get(key); + if (deltaObject instanceof JSONObject) { + merge((JSONObject) subObject, (JSONObject) deltaObject); + } else { + throw new JSONException("Error when parsing delta: Type " + deltaObject.getClass().getSimpleName() + " does not equal JSONObject"); + } + + continue; + } + + if (subObject instanceof JSONArray) { + Object deltaObject = delta.get(key); + if (deltaObject instanceof JSONArray) { + mergeArray((JSONArray) subObject, (JSONArray) deltaObject); + } else { + throw new JSONException("Error when parsing delta: Type " + deltaObject.getClass().getSimpleName() + " does not equal JSONArray"); + } + + continue; + } + + // Assume that a primitive type is used (primitive types cannot be merged, only overridden) + input.put(key, delta.get(key)); + } + } + + /** + * Merges the input with its delta by adding all values from the delta to the input. + */ + private static void mergeArray(JSONArray input, JSONArray delta) { + for (int index = 0; index < delta.length(); index++) { + input.put(delta.get(index)); + } + } +} diff --git a/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/modulesScreen.ui b/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/modulesScreen.ui index ff3ef21bf..ca3185128 100644 --- a/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/modulesScreen.ui +++ b/engine/src/main/resources/org/destinationsol/assets/ui/mainMenu/modulesScreen.ui @@ -85,7 +85,7 @@ } }, { - "type": "UIButton", + "type": "KeyActivatedButton", "id": "confirmButton", "text": "Confirm", "layoutInfo": { diff --git a/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeDamageTest.java b/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeDamageTest.java index 95ac1efaa..25b8e13f2 100644 --- a/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeDamageTest.java +++ b/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeDamageTest.java @@ -44,7 +44,7 @@ public class NonNegativeDamageTest { @BeforeEach public void setUp() throws Exception { ModuleFactory moduleFactory = new ModuleFactory(); - moduleManager = new ModuleManager(new DefaultBeanContext(), moduleFactory, new TableModuleRegistry(), + moduleManager = new ModuleManager(new DefaultBeanContext(), new TableModuleRegistry(), new ModulePathScanner(moduleFactory), new TestModuleConfig()); moduleManager.init(); diff --git a/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeHealthTest.java b/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeHealthTest.java index 654e44802..7d66a62cb 100644 --- a/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeHealthTest.java +++ b/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/NonNegativeHealthTest.java @@ -45,7 +45,7 @@ public class NonNegativeHealthTest { @BeforeEach public void setUp() throws Exception { ModuleFactory moduleFactory = new ModuleFactory(); - moduleManager = new ModuleManager(new DefaultBeanContext(), moduleFactory, new TableModuleRegistry(), + moduleManager = new ModuleManager(new DefaultBeanContext(), new TableModuleRegistry(), new ModulePathScanner(moduleFactory), new TestModuleConfig()); moduleManager.init(); ServiceRegistry systemsRegistry = new ServiceRegistry(); diff --git a/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/OnDamageTest.java b/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/OnDamageTest.java index c1b6747d4..9ad578234 100644 --- a/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/OnDamageTest.java +++ b/engine/src/test/java/org/destinationsol/systems/DamageSystemTests/OnDamageTest.java @@ -45,7 +45,7 @@ public class OnDamageTest { @BeforeEach public void setUp() throws Exception { ModuleFactory moduleFactory = new ModuleFactory(); - moduleManager = new ModuleManager(new DefaultBeanContext(), moduleFactory, new TableModuleRegistry(), + moduleManager = new ModuleManager(new DefaultBeanContext(), new TableModuleRegistry(), new ModulePathScanner(moduleFactory), new TestModuleConfig()); moduleManager.init(); ServiceRegistry systemsRegistry = new ServiceRegistry(); diff --git a/engine/src/test/java/org/destinationsol/systems/LocationSystemTests/PositionUpdateTest.java b/engine/src/test/java/org/destinationsol/systems/LocationSystemTests/PositionUpdateTest.java index f33a92d60..826df6257 100644 --- a/engine/src/test/java/org/destinationsol/systems/LocationSystemTests/PositionUpdateTest.java +++ b/engine/src/test/java/org/destinationsol/systems/LocationSystemTests/PositionUpdateTest.java @@ -45,7 +45,7 @@ public class PositionUpdateTest implements Box2DInitializer { @BeforeEach public void setUp() throws Exception { ModuleFactory moduleFactory = new ModuleFactory(); - moduleManager = new ModuleManager(new DefaultBeanContext(), moduleFactory, new TableModuleRegistry(), + moduleManager = new ModuleManager(new DefaultBeanContext(), new TableModuleRegistry(), new ModulePathScanner(moduleFactory), new TestModuleConfig()); moduleManager.init(); ServiceRegistry systemsRegistry = new ServiceRegistry(); diff --git a/engine/src/test/java/org/destinationsol/testsupport/AssetsHelperInitializer.java b/engine/src/test/java/org/destinationsol/testsupport/AssetsHelperInitializer.java index d1e528193..930547205 100644 --- a/engine/src/test/java/org/destinationsol/testsupport/AssetsHelperInitializer.java +++ b/engine/src/test/java/org/destinationsol/testsupport/AssetsHelperInitializer.java @@ -57,7 +57,7 @@ default void initAssets() throws Exception { BeanContext beanContext = new DefaultBeanContext(); ModuleFactory moduleFactory = new ModuleFactory(); - ModuleManager moduleManager = new ModuleManager(beanContext, moduleFactory, new TableModuleRegistry(), + ModuleManager moduleManager = new ModuleManager(beanContext, new TableModuleRegistry(), new ModulePathScanner(moduleFactory), new TestModuleConfig()); moduleManager.init(); stateObject.setModuleManager(moduleManager); diff --git a/engine/src/test/java/org/destinationsol/testsupport/TestModuleConfig.java b/engine/src/test/java/org/destinationsol/testsupport/TestModuleConfig.java index b5cdf21c2..25c1ba960 100644 --- a/engine/src/test/java/org/destinationsol/testsupport/TestModuleConfig.java +++ b/engine/src/test/java/org/destinationsol/testsupport/TestModuleConfig.java @@ -6,8 +6,11 @@ import org.terasology.gestalt.module.ModuleFactory; import org.terasology.gestalt.module.sandbox.JavaModuleClassLoader; +import javax.inject.Inject; import java.io.File; import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; /** * Defines the settings used for module-based tests. @@ -16,9 +19,13 @@ * module paths. */ public class TestModuleConfig implements FacadeModuleConfig { + @Inject + public TestModuleConfig() { + } + @Override - public File getModulesPath() { - return Paths.get(".").resolve("modules").toFile(); + public Collection getModulePaths() { + return Collections.singletonList(Paths.get(".").resolve("modules").toFile()); } @Override diff --git a/settings.gradle b/settings.gradle index cea1ca502..fe49b896d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,11 @@ includeBuild 'build-logic' include 'desktop', 'engine', 'modules' import groovy.io.FileType +File steamGradle = new File(rootDir, 'steam/build.gradle') +if (steamGradle.exists()) { + include 'steam' +} + File gwtGradle = new File(rootDir, 'gwt/build.gradle') if (gwtGradle.exists()) { include 'gwt' diff --git a/steam/ContentBuilder/scripts/app_build_342980.vdf b/steam/ContentBuilder/scripts/app_build_342980.vdf deleted file mode 100755 index 37179d9c1..000000000 --- a/steam/ContentBuilder/scripts/app_build_342980.vdf +++ /dev/null @@ -1,18 +0,0 @@ -"appbuild" -{ - "appid" "342980" - "desc" "Your build description here" // description for this build - "buildoutput" "..\output\" // build output folder for .log, .csm & .csd files, relative to location of this file - "contentroot" "..\content\DestinationSol\" // root content folder, relative to location of this file - "setlive" "" // branch to set live after successful build, non if empty - "preview" "0" // to enable preview builds - "local" "" // set to file path of local content server - - "depots" - { - "342981" "depot_build_342981.vdf" - "342982" "depot_build_342982.vdf" - "342983" "depot_build_342983.vdf" - "342984" "depot_build_342984.vdf" - } -} diff --git a/steam/ContentBuilder/scripts/depot_build_342981.vdf b/steam/ContentBuilder/scripts/depot_build_342981.vdf deleted file mode 100755 index 1918fcbf1..000000000 --- a/steam/ContentBuilder/scripts/depot_build_342981.vdf +++ /dev/null @@ -1,44 +0,0 @@ -"DepotBuildConfig" -{ - // Set your assigned depot ID here - "DepotID" "342981" - - // Set a root for all content. - // All relative paths specified below (LocalPath in FileMapping entries, and FileExclusion paths) - // will be resolved relative to this root. - // If you don't define ContentRoot, then it will be assumed to be - // the location of this script file, which probably isn't what you want - "ContentRoot" "" - - // include all files recursivley - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "libs\*" - - // This is a path relative to the install folder of your game - "DepotPath" "libs\" - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "1" - } - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "modules\*" - - // This is a path relative to the install folder of your game - "DepotPath" "modules\" - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "1" - } - - // but exclude all symbol files - // This can be a full path, or a path relative to ContentRoot - "FileExclusion" "*.pdb" -} diff --git a/steam/ContentBuilder/scripts/depot_build_342982.vdf b/steam/ContentBuilder/scripts/depot_build_342982.vdf deleted file mode 100755 index c4260f413..000000000 --- a/steam/ContentBuilder/scripts/depot_build_342982.vdf +++ /dev/null @@ -1,45 +0,0 @@ -"DepotBuildConfig" -{ - // Set your assigned depot ID here - "DepotID" "342982" - - // Set a root for all content. - // All relative paths specified below (LocalPath in FileMapping entries, and FileExclusion paths) - // will be resolved relative to this root. - // If you don't define ContentRoot, then it will be assumed to be - // the location of this script file, which probably isn't what you want - "ContentRoot" "" - - // include all files recursivley - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "lwjre\*" - - // This is a path relative to the install folder of your game - "DepotPath" "lwjre\" - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "1" - } - - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "sol.exe" - - // This is a path relative to the install folder of your game - "DepotPath" "." - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "0" - } - - // but exclude all symbol files - // This can be a full path, or a path relative to ContentRoot - "FileExclusion" "*.pdb" -} diff --git a/steam/ContentBuilder/scripts/depot_build_342983.vdf b/steam/ContentBuilder/scripts/depot_build_342983.vdf deleted file mode 100755 index 95a7ea426..000000000 --- a/steam/ContentBuilder/scripts/depot_build_342983.vdf +++ /dev/null @@ -1,46 +0,0 @@ -"DepotBuildConfig" -{ - // Set your assigned depot ID here - "DepotID" "342983" - - // Set a root for all content. - // All relative paths specified below (LocalPath in FileMapping entries, and FileExclusion paths) - // will be resolved relative to this root. - // If you don't define ContentRoot, then it will be assumed to be - // the location of this script file, which probably isn't what you want - "ContentRoot" "" - - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "sol.sh" - - // This is a path relative to the install folder of your game - "DepotPath" "." - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "0" - } - - // include all files recursivley - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "lwjreLinux64\*" - - // This is a path relative to the install folder of your game - "DepotPath" "lwjreLinux64\" - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "1" - } - - - // but exclude all symbol files - // This can be a full path, or a path relative to ContentRoot - "FileExclusion" "*.pdb" -} diff --git a/steam/ContentBuilder/scripts/depot_build_342984.vdf b/steam/ContentBuilder/scripts/depot_build_342984.vdf deleted file mode 100644 index ab75fb994..000000000 --- a/steam/ContentBuilder/scripts/depot_build_342984.vdf +++ /dev/null @@ -1,45 +0,0 @@ -"DepotBuildConfig" -{ - // Set your assigned depot ID here - "DepotID" "342984" - - // Set a root for all content. - // All relative paths specified below (LocalPath in FileMapping entries, and FileExclusion paths) - // will be resolved relative to this root. - // If you don't define ContentRoot, then it will be assumed to be - // the location of this script file, which probably isn't what you want - "ContentRoot" "" - - // include all files recursively - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "lwjreOSX\*" - - // This is a path relative to the install folder of your game - "DepotPath" "lwjreOSX\" - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "1" - } - - "FileMapping" - { - // This can be a full path, or a path relative to ContentRoot - "LocalPath" "solOSX.sh" - - // This is a path relative to the install folder of your game - "DepotPath" "." - - // If LocalPath contains wildcards, setting this means that all - // matching files within subdirectories of LocalPath will also - // be included. - "recursive" "0" - } - - // but exclude all symbol files - // This can be a full path, or a path relative to ContentRoot - "FileExclusion" "*.pdb" -} diff --git a/steam/SteamRelease.md b/steam/SteamRelease.md deleted file mode 100644 index b4100b2e7..000000000 --- a/steam/SteamRelease.md +++ /dev/null @@ -1,27 +0,0 @@ -Steam Release Process ------------- -Here is a quick run down of the steam build process and instructions on how to manually upload a new build should anyone need to do it. The future goal is to automate this process. - -#### Prerequisites -1. You need to be a Steamworks developer with build permissions on the Destination Sol app. -2. Download the Steamworks SDK -3. Run the steamcmd in tools -> Content Builder. The first run downloads and installs some extra files. -4. I highly recommend reading the documentation: https://partner.steamgames.com/documentation/steampipe - -#### Upload Process -1. Copy the build scripts from the Destination Sol repo to the steam-sdk directory at: steam-sdk/tools/ContentBuilder/scripts -2. Get the build you want to use. This is most likely the latest build from Jenkins: http://jenkins.movingblocks.net/job/DestinationSol/ -3. Unzip the file and copy the DestinationSol folder into the steam-sdk directory at: steam-sdk/tools/ContentBuilder/content -4. Edit the script file app_build_342980.vdf and add a message to the "desc" field. This helps identify the build that you are uploading. -In most cases you can comment out depots 342982, 342983 and 342984 as these are the JRE for the different operating systems and only need to be included if they have been modified. If you are updating the JRE it should ideally be kept in sync with same version that Terasology uses. -5. From a terminal/command prompt at the ContentBuilder directory, run the following command. Replace username and password with your steam username and password. Change the steamcmd path so it's appropriate for your OS: -bash ./builder_osx/steamcmd.sh +login username password +run_app_build ../scripts/app_build_342980.vdf +quit -6. This will upload the files and this will show as a new build in Steamworks under the Builds tab. - -#### Set Beta Branch to Live -The next steps depend on what you want to do. The most common approach would be to promote that build to Beta. This is easily achieved by: -1. Go to the Steamworks Builds tab for the app. -2. Click the "Select an app branch" in the drop down of the newly uploaded build -3. Select the Beta branch and click "Preview Change." -4. Add a comment about the release -5. Click "Set Build Live Now" to make the build live as the new Beta.