diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/CreateGameScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/CreateGameScreen.java index 8deda990519..8c1515f1a10 100644 --- a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/CreateGameScreen.java +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/CreateGameScreen.java @@ -91,6 +91,11 @@ public class CreateGameScreen extends CoreScreenLayer { private boolean loadingAsServer; + /** + * A UniverseWrapper object used here to determine if the game is single-player or multi-player. + */ + private UniverseWrapper universeWrapper; + @Override @SuppressWarnings("unchecked") public void initialise() { @@ -102,7 +107,7 @@ public void initialise() { gameTypeTitle.bindText(new ReadOnlyBinding() { @Override public String get() { - if (loadingAsServer) { + if (isLoadingAsServer()) { return translationSystem.translate("${engine:menu#select-multiplayer-game-sub-title}"); } else { return translationSystem.translate("${engine:menu#select-singleplayer-game-sub-title}"); @@ -264,7 +269,7 @@ public void set(Boolean value) { (long) (WorldTime.DAY_LENGTH * timeOffset), worldGenerator.getSelection().getUri()); gameManifest.addWorld(worldInfo); - gameEngine.changeState(new StateLoading(gameManifest, (loadingAsServer) ? NetworkMode.DEDICATED_SERVER : NetworkMode.NONE)); + gameEngine.changeState(new StateLoading(gameManifest, (isLoadingAsServer()) ? NetworkMode.DEDICATED_SERVER : NetworkMode.NONE)); } }); @@ -428,11 +433,11 @@ private List getGameplayModules() { } public boolean isLoadingAsServer() { - return loadingAsServer; + return universeWrapper.getLoadingAsServer(); } - public void setLoadingAsServer(boolean loadingAsServer) { - this.loadingAsServer = loadingAsServer; + public void setUniverseWrapper(UniverseWrapper wrapper) { + this.universeWrapper = wrapper; } @Override diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/MainMenuScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/MainMenuScreen.java index 1dd4ba2328d..889a2cb5ff8 100644 --- a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/MainMenuScreen.java +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/MainMenuScreen.java @@ -66,13 +66,15 @@ public void initialise() { jvmWarningLabel.setVisible(NonNativeJVMDetector.JVM_ARCH_IS_NONNATIVE); SelectGameScreen selectScreen = getManager().createScreen(SelectGameScreen.ASSET_URI, SelectGameScreen.class); - + UniverseWrapper universeWrapper = new UniverseWrapper(); WidgetUtil.trySubscribe(this, "singleplayer", button -> { - selectScreen.setLoadingAsServer(false); + universeWrapper.setLoadingAsServer(false); + selectScreen.setUniverseWrapper(universeWrapper); triggerForwardAnimation(selectScreen); }); WidgetUtil.trySubscribe(this, "multiplayer", button -> { - selectScreen.setLoadingAsServer(true); + universeWrapper.setLoadingAsServer(true); + selectScreen.setUniverseWrapper(universeWrapper); triggerForwardAnimation(selectScreen); }); WidgetUtil.trySubscribe(this, "join", button -> { diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/NewGameScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/NewGameScreen.java new file mode 100644 index 00000000000..8ca5c849380 --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/NewGameScreen.java @@ -0,0 +1,323 @@ +/* + * Copyright 2018 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.rendering.nui.layers.mainMenu; + +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.assets.ResourceUrn; +import org.terasology.config.Config; +import org.terasology.config.ModuleConfig; +import org.terasology.engine.GameEngine; +import org.terasology.engine.SimpleUri; +import org.terasology.engine.TerasologyConstants; +import org.terasology.engine.modes.StateLoading; +import org.terasology.engine.module.ModuleManager; +import org.terasology.engine.module.StandardModuleExtension; +import org.terasology.game.GameManifest; +import org.terasology.i18n.TranslationSystem; +import org.terasology.module.DependencyResolver; +import org.terasology.module.Module; +import org.terasology.module.ResolutionResult; +import org.terasology.naming.Name; +import org.terasology.network.NetworkMode; +import org.terasology.registry.In; +import org.terasology.rendering.nui.Canvas; +import org.terasology.rendering.nui.Color; +import org.terasology.rendering.nui.CoreScreenLayer; +import org.terasology.rendering.nui.WidgetUtil; +import org.terasology.rendering.nui.animation.MenuAnimationSystems; +import org.terasology.rendering.nui.databinding.Binding; +import org.terasology.rendering.nui.databinding.ReadOnlyBinding; +import org.terasology.rendering.nui.itemRendering.StringTextRenderer; +import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameInfo; +import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameProvider; +import org.terasology.rendering.nui.layers.mainMenu.selectModulesScreen.AdvancedGameSetupScreen; +import org.terasology.rendering.nui.widgets.UIDropdown; +import org.terasology.rendering.nui.widgets.UIDropdownScrollable; +import org.terasology.rendering.nui.widgets.UILabel; +import org.terasology.rendering.nui.widgets.UIText; +import org.terasology.utilities.random.FastRandom; +import org.terasology.world.generator.internal.WorldGeneratorInfo; +import org.terasology.world.generator.internal.WorldGeneratorManager; +import org.terasology.world.internal.WorldInfo; +import org.terasology.world.time.WorldTime; + +import java.util.Collections; +import java.util.List; + +public class NewGameScreen extends CoreScreenLayer { + + public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:newGameScreen"); + private static final String DEFAULT_GAME_NAME_PREFIX = "Game "; + private static final Logger logger = LoggerFactory.getLogger(CreateGameScreen.class); + private static final String DEFAULT_GAME_TEMPLATE_NAME = "JoshariasSurvival"; + private static final String DEFAULT_WORLD_GENERATOR = "Core:FacetedPerlin"; + private boolean loadingAsServer; + + @In + private ModuleManager moduleManager; + + @In + private Config config; + + @In + private WorldGeneratorManager worldGeneratorManager; + + @In + private GameEngine gameEngine; + + @In + private TranslationSystem translationSystem; + + private UniverseWrapper universeWrapper; + + @Override + public void initialise() { + + setAnimationSystem(MenuAnimationSystems.createDefaultSwipeAnimation()); + + UILabel gameTypeTitle = find("gameTypeTitle", UILabel.class); + if (gameTypeTitle != null) { + gameTypeTitle.bindText(new ReadOnlyBinding() { + @Override + public String get() { + if (isLoadingAsServer()) { + return translationSystem.translate("${engine:menu#select-multiplayer-game-sub-title}"); + } else { + return translationSystem.translate("${engine:menu#select-singleplayer-game-sub-title}"); + } + } + }); + } + final UIText gameName = find("gameName", UIText.class); + setGameName(gameName); + + final UIDropdownScrollable gameplay = find("gameplay", UIDropdownScrollable.class); + gameplay.setOptions(getGameplayModules()); + gameplay.setVisibleOptions(5); + gameplay.bindSelection(new Binding() { + Module selected; + + @Override + public Module get() { + return selected; + } + + @Override + public void set(Module value) { + setSelectedGameplayModule(value); + selected = value; + } + }); + gameplay.setOptionRenderer(new StringTextRenderer() { + @Override + public String getString(Module value) { + return value.getMetadata().getDisplayName().value(); + } + + @Override + public void draw(Module value, Canvas canvas) { + canvas.getCurrentStyle().setTextColor(validateModuleDependencies(value.getId()) ? Color.WHITE : Color.RED); + super.draw(value, canvas); + canvas.getCurrentStyle().setTextColor(Color.WHITE); + } + }); + + UILabel gameplayDescription = find("gameplayDescription", UILabel.class); + gameplayDescription.bindText(new ReadOnlyBinding() { + @Override + public String get() { + Module selectedModule = gameplay.getSelection(); + if (selectedModule != null) { + return selectedModule.getMetadata().getDescription().value(); + } else { + return ""; + } + } + }); + + AdvancedGameSetupScreen advancedSetupGameScreen = getManager().createScreen(AdvancedGameSetupScreen.ASSET_URI, AdvancedGameSetupScreen.class); + WidgetUtil.trySubscribe(this, "advancedSetup", button -> { + universeWrapper.setGameName(gameName.getText()); + advancedSetupGameScreen.setUniverseWrapper(universeWrapper); + triggerForwardAnimation(advancedSetupGameScreen); + }); + + WidgetUtil.trySubscribe(this, "play", button -> { + if(gameName.getText().isEmpty()) { + getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class).setMessage("Error", "Game name cannot be empty"); + } else { + GameManifest gameManifest = new GameManifest(); + + gameManifest.setTitle(gameName.getText()); + String tempSeed = new FastRandom().nextString(32); + gameManifest.setSeed(tempSeed); + DependencyResolver resolver = new DependencyResolver(moduleManager.getRegistry()); + ResolutionResult result = resolver.resolve(config.getDefaultModSelection().listModules()); + if (!result.isSuccess()) { + MessagePopup errorMessagePopup = getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class); + if (errorMessagePopup != null) { + errorMessagePopup.setMessage("Invalid Module Selection", "Please review your module seleciton and try again"); + } + return; + } + for (Module module : result.getModules()) { + gameManifest.addModule(module.getId(), module.getVersion()); + } + + SimpleUri uri = config.getWorldGeneration().getDefaultGenerator(); + // This is multiplied by the number of seconds in a day (86400000) to determine the exact millisecond at which the game will start. + final float timeOffset = 0.50f; + WorldInfo worldInfo = new WorldInfo(TerasologyConstants.MAIN_WORLD, tempSeed, + (long) (WorldTime.DAY_LENGTH * timeOffset), uri); + gameManifest.addWorld(worldInfo); + gameEngine.changeState(new StateLoading(gameManifest, (isLoadingAsServer()) ? NetworkMode.DEDICATED_SERVER : NetworkMode.NONE)); + } + }); + + WidgetUtil.trySubscribe(this, "close", button -> + triggerBackAnimation() + ); + } + + /** + * Sets the game names based on the game number of the last saved game + * @param gameName The {@link UIText} in which the name will be displayed. + */ + private void setGameName(UIText gameName) { + if (gameName != null) { + int gameNumber = 1; + for (GameInfo info : GameProvider.getSavedGames()) { + if (info.getManifest().getTitle().startsWith(DEFAULT_GAME_NAME_PREFIX)) { + String remainder = info.getManifest().getTitle().substring(DEFAULT_GAME_NAME_PREFIX.length()); + try { + gameNumber = Math.max(gameNumber, Integer.parseInt(remainder) + 1); + } catch (NumberFormatException e) { + logger.trace("Could not parse {} as integer (not an error)", remainder, e); + } + } + } + + gameName.setText(DEFAULT_GAME_NAME_PREFIX + gameNumber); + } + } + + private List getGameplayModules() { + List gameplayModules = Lists.newArrayList(); + for (Name moduleId : moduleManager.getRegistry().getModuleIds()) { + Module latestVersion = moduleManager.getRegistry().getLatestModuleVersion(moduleId); + if (!latestVersion.isOnClasspath()) { + if (StandardModuleExtension.isGameplayModule(latestVersion)) { + gameplayModules.add(latestVersion); + } + } + } + Collections.sort(gameplayModules, (o1, o2) -> + o1.getMetadata().getDisplayName().value().compareTo(o2.getMetadata().getDisplayName().value())); + + return gameplayModules; + } + + private boolean validateModuleDependencies(Name moduleName) { + DependencyResolver resolver = new DependencyResolver(moduleManager.getRegistry()); + return resolver.resolve(moduleName).isSuccess(); + } + + private void setSelectedGameplayModule(Module module) { + ModuleConfig moduleConfig = config.getDefaultModSelection(); + if (moduleConfig.getDefaultGameplayModuleName().equals(module.getId().toString())) { + // same as before -> we're done + return; + } + + moduleConfig.setDefaultGameplayModuleName(module.getId().toString()); + moduleConfig.clear(); + moduleConfig.addModule(module.getId()); + + // Set the default generator of the selected gameplay module + setDefaultGeneratorOfGameplayModule(module); + + config.save(); + } + + // Sets the default generator of the passed in gameplay module. Make sure it's already selected. + private void setDefaultGeneratorOfGameplayModule(Module module) { + ModuleConfig moduleConfig = config.getDefaultModSelection(); + + // Set the default generator of the selected gameplay module + SimpleUri defaultWorldGenerator = StandardModuleExtension.getDefaultWorldGenerator(module); + if (defaultWorldGenerator != null) { + for (WorldGeneratorInfo worldGenInfo : worldGeneratorManager.getWorldGenerators()) { + if (worldGenInfo.getUri().equals(defaultWorldGenerator)) { + config.getWorldGeneration().setDefaultGenerator(worldGenInfo.getUri()); + } + } + } + + config.save(); + } + + + @Override + public void onOpened() { + final UIText gameName = find("gameName", UIText.class); + setGameName(gameName); + + final UIDropdown gameplay = find("gameplay", UIDropdown.class); + + String configDefaultModuleName = config.getDefaultModSelection().getDefaultGameplayModuleName(); + String useThisModuleName = ""; + + // Get the default gameplay module from the config if it exists. This is likely to have a user triggered selection. + // Otherwise, default to DEFAULT_GAME_TEMPLATE_NAME. + if ("".equalsIgnoreCase(configDefaultModuleName) || DEFAULT_GAME_TEMPLATE_NAME.equalsIgnoreCase(configDefaultModuleName)) { + useThisModuleName = DEFAULT_GAME_TEMPLATE_NAME; + } else { + useThisModuleName = configDefaultModuleName; + } + + Name defaultGameplayModuleName = new Name(useThisModuleName); + Module defaultGameplayModule = moduleManager.getRegistry().getLatestModuleVersion(defaultGameplayModuleName); + + if (defaultGameplayModule != null) { + gameplay.setSelection(defaultGameplayModule); + + if (configDefaultModuleName.equalsIgnoreCase(DEFAULT_GAME_TEMPLATE_NAME)) { + setDefaultGeneratorOfGameplayModule(defaultGameplayModule); + } + } else { + // Find the first gameplay module that is available. + for (Module module : moduleManager.getRegistry()) { + // Module is null if it is no longer present. + if (module != null && StandardModuleExtension.isGameplayModule(module)) { + gameplay.setSelection(module); + break; + } + } + } + } + + public boolean isLoadingAsServer() { + return universeWrapper.getLoadingAsServer(); + } + + public void setUniverseWrapper(UniverseWrapper wrapper) { + this.universeWrapper = wrapper; + } + +} + diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/SelectGameScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/SelectGameScreen.java index af2113777e0..49cf2abff55 100644 --- a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/SelectGameScreen.java +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/SelectGameScreen.java @@ -52,8 +52,7 @@ public class SelectGameScreen extends CoreScreenLayer { @In private TranslationSystem translationSystem; - private boolean loadingAsServer; - + private UniverseWrapper universeWrapper; @Override public void initialise() { @@ -64,7 +63,7 @@ public void initialise() { gameTypeTitle.bindText(new ReadOnlyBinding() { @Override public String get() { - if (loadingAsServer) { + if (isLoadingAsServer()) { return translationSystem.translate("${engine:menu#select-multiplayer-game-sub-title}"); } else { return translationSystem.translate("${engine:menu#select-singleplayer-game-sub-title}"); @@ -86,12 +85,6 @@ public String get() { gameList.select(0); gameList.subscribe((widget, item) -> loadGame(item)); - CreateGameScreen screen = getManager().createScreen(CreateGameScreen.ASSET_URI, CreateGameScreen.class); - WidgetUtil.trySubscribe(this, "create", button -> { - screen.setLoadingAsServer(loadingAsServer); - triggerForwardAnimation(screen); - }); - WidgetUtil.trySubscribe(this, "load", button -> { GameInfo gameInfo = gameList.getSelection(); if (gameInfo != null) { @@ -106,6 +99,11 @@ public String get() { confirmationPopup.setLeftButton(translationSystem.translate("${engine:menu#dialog-yes}"), () -> removeSelectedGame(gameList)); confirmationPopup.setRightButton(translationSystem.translate("${engine:menu#dialog-no}"), () -> { }); }); + NewGameScreen newGameScreen = getManager().createScreen(NewGameScreen.ASSET_URI, NewGameScreen.class); + WidgetUtil.trySubscribe(this, "create", button -> { + newGameScreen.setUniverseWrapper(universeWrapper); + triggerForwardAnimation(newGameScreen); + }); WidgetUtil.trySubscribe(this, "close", button -> triggerBackAnimation()); } @@ -133,7 +131,7 @@ public boolean isLowerLayerVisible() { @Override public void onOpened() { super.onOpened(); - if (loadingAsServer && !config.getPlayer().hasEnteredUsername()) { + if (isLoadingAsServer() && !config.getPlayer().hasEnteredUsername()) { getManager().pushScreen(EnterUsernamePopup.ASSET_URI, EnterUsernamePopup.class); } } @@ -144,7 +142,7 @@ private void loadGame(GameInfo item) { config.getWorldGeneration().setDefaultSeed(manifest.getSeed()); config.getWorldGeneration().setWorldTitle(manifest.getTitle()); - CoreRegistry.get(GameEngine.class).changeState(new StateLoading(manifest, (loadingAsServer) ? NetworkMode.DEDICATED_SERVER : NetworkMode.NONE)); + CoreRegistry.get(GameEngine.class).changeState(new StateLoading(manifest, (isLoadingAsServer()) ? NetworkMode.DEDICATED_SERVER : NetworkMode.NONE)); } catch (Exception e) { logger.error("Failed to load saved game", e); getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class).setMessage("Error Loading Game", e.getMessage()); @@ -152,11 +150,11 @@ private void loadGame(GameInfo item) { } public boolean isLoadingAsServer() { - return loadingAsServer; + return universeWrapper.getLoadingAsServer(); } - public void setLoadingAsServer(boolean loadingAsServer) { - this.loadingAsServer = loadingAsServer; + public void setUniverseWrapper(UniverseWrapper wrapper) { + this.universeWrapper = wrapper; } private void refreshList(UIList gameList) { diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/StartPlayingScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/StartPlayingScreen.java new file mode 100644 index 00000000000..00368b2276a --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/StartPlayingScreen.java @@ -0,0 +1,117 @@ +/* + * Copyright 2018 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.rendering.nui.layers.mainMenu; + +import org.terasology.assets.ResourceUrn; +import org.terasology.config.Config; +import org.terasology.context.Context; +import org.terasology.engine.GameEngine; +import org.terasology.engine.SimpleUri; +import org.terasology.engine.TerasologyConstants; +import org.terasology.engine.modes.StateLoading; +import org.terasology.engine.module.ModuleManager; +import org.terasology.game.GameManifest; +import org.terasology.i18n.TranslationSystem; +import org.terasology.module.DependencyResolver; +import org.terasology.module.Module; +import org.terasology.module.ResolutionResult; +import org.terasology.network.NetworkMode; +import org.terasology.registry.In; +import org.terasology.rendering.assets.texture.Texture; +import org.terasology.rendering.nui.CoreScreenLayer; +import org.terasology.rendering.nui.WidgetUtil; +import org.terasology.rendering.nui.widgets.UIImage; +import org.terasology.rendering.nui.widgets.UILabel; +import org.terasology.rendering.world.WorldSetupWrapper; +import org.terasology.world.internal.WorldInfo; +import org.terasology.world.time.WorldTime; + +public class StartPlayingScreen extends CoreScreenLayer { + + public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:startPlayingScreen"); + + @In + private ModuleManager moduleManager; + + @In + private Config config; + + @In + private GameEngine gameEngine; + + @In + private TranslationSystem translationSystem; + + private Texture texture; + private WorldSetupWrapper world; + private UniverseWrapper universeWrapper; + + @Override + public void initialise() { + + WidgetUtil.trySubscribe(this, "close", button -> + triggerBackAnimation() + ); + + WidgetUtil.trySubscribe(this, "play", button -> { + GameManifest gameManifest = new GameManifest(); + + gameManifest.setTitle(universeWrapper.getGameName()); + + DependencyResolver resolver = new DependencyResolver(moduleManager.getRegistry()); + ResolutionResult result = resolver.resolve(config.getDefaultModSelection().listModules()); + if (!result.isSuccess()) { + MessagePopup errorMessagePopup = getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class); + if (errorMessagePopup != null) { + errorMessagePopup.setMessage("Invalid Module Selection", "Please review your module seleciton and try again"); + } + return; + } + for (Module module : result.getModules()) { + gameManifest.addModule(module.getId(), module.getVersion()); + } + + SimpleUri uri = world.getWorldGeneratorInfo().getUri(); + // This is multiplied by the number of seconds in a day (86400000) to determine the exact millisecond at which the game will start. + final float timeOffset = 0.50f; + WorldInfo worldInfo = new WorldInfo(TerasologyConstants.MAIN_WORLD, world.getWorldGenerator().getWorldSeed(), + (long) (WorldTime.DAY_LENGTH * timeOffset), uri); + gameManifest.addWorld(worldInfo); + gameEngine.changeState(new StateLoading(gameManifest, (universeWrapper.getLoadingAsServer()) ? NetworkMode.DEDICATED_SERVER : NetworkMode.NONE)); + }); + } + + @Override + public void onOpened() { + UIImage previewImage = find("preview", UIImage.class); + previewImage.setImage(texture); + + UILabel subitle = find("subtitle", UILabel.class); + subitle.setText(translationSystem.translate("${engine:menu#start-playing}") + " in " + world.getWorldName().toString()); + } + + /** + * This method is called before the screen comes to the forefront to set the world + * in which the player is about to spawn. + * @param targetWorld The world in which the player is going to spawn. + * @param targetWorldTexture The world texture generated in {@link WorldPreGenerationScreen} to be displayed on this screen. + */ + public void setTargetWorld(WorldSetupWrapper targetWorld, Texture targetWorldTexture, Context context) { + texture = targetWorldTexture; + world = targetWorld; + universeWrapper = context.get(UniverseWrapper.class); + } +} diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/UniverseSetupScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/UniverseSetupScreen.java new file mode 100644 index 00000000000..9364e8b0502 --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/UniverseSetupScreen.java @@ -0,0 +1,365 @@ +/* + * Copyright 2018 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.rendering.nui.layers.mainMenu; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.terasology.assets.AssetFactory; +import org.terasology.assets.ResourceUrn; +import org.terasology.assets.management.AssetManager; +import org.terasology.assets.module.ModuleAwareAssetTypeManager; +import org.terasology.config.Config; +import org.terasology.config.ModuleConfig; +import org.terasology.context.Context; +import org.terasology.context.internal.ContextImpl; +import org.terasology.engine.bootstrap.EnvironmentSwitchHandler; +import org.terasology.engine.module.ModuleManager; +import org.terasology.entitySystem.prefab.Prefab; +import org.terasology.entitySystem.prefab.PrefabData; +import org.terasology.entitySystem.prefab.internal.PojoPrefab; +import org.terasology.logic.behavior.asset.BehaviorTree; +import org.terasology.logic.behavior.asset.BehaviorTreeData; +import org.terasology.module.DependencyInfo; +import org.terasology.module.DependencyResolver; +import org.terasology.module.Module; +import org.terasology.module.ModuleEnvironment; +import org.terasology.module.ResolutionResult; +import org.terasology.naming.Name; +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.registry.In; +import org.terasology.rendering.nui.CoreScreenLayer; +import org.terasology.rendering.nui.NUIManager; +import org.terasology.rendering.nui.WidgetUtil; +import org.terasology.rendering.nui.asset.UIData; +import org.terasology.rendering.nui.asset.UIElement; +import org.terasology.rendering.nui.databinding.Binding; +import org.terasology.rendering.nui.databinding.ReadOnlyBinding; +import org.terasology.rendering.nui.itemRendering.StringTextRenderer; +import org.terasology.rendering.nui.skin.UISkin; +import org.terasology.rendering.nui.skin.UISkinData; +import org.terasology.rendering.nui.widgets.UIDropdownScrollable; +import org.terasology.rendering.world.WorldSetupWrapper; +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 org.terasology.world.generator.UnresolvedWorldGeneratorException; +import org.terasology.world.generator.internal.WorldGeneratorInfo; +import org.terasology.world.generator.internal.WorldGeneratorManager; +import org.terasology.world.generator.plugin.TempWorldGeneratorPluginLibrary; +import org.terasology.world.generator.plugin.WorldGeneratorPluginLibrary; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Sets up the Universe for a user. Displays a list of {@link org.terasology.world.generator.WorldGenerator} + * for a particular game template. + */ +public class UniverseSetupScreen extends CoreScreenLayer { + + public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:universeSetupScreen"); + + @In + private WorldGeneratorManager worldGeneratorManager; + + @In + private ModuleManager moduleManager; + + @In + private Config config; + + private List worlds = Lists.newArrayList(); + private ModuleEnvironment environment; + private ModuleAwareAssetTypeManager assetTypeManager; + private Context context; + private int worldNumber; + private String selectedWorld = ""; + + @Override + public void initialise() { + + ModuleConfig moduleConfig = config.getDefaultModSelection(); + + + final UIDropdownScrollable worldGenerator = find("worldGenerators", UIDropdownScrollable.class); + if (worldGenerator != null) { + worldGenerator.bindOptions(new ReadOnlyBinding>() { + @Override + public List get() { + // grab all the module names and their dependencies + // This grabs modules from `config.getDefaultModSelection()` which is updated in AdvancedGameSetupScreen + Set enabledModuleNames = getAllEnabledModuleNames().stream().collect(Collectors.toSet()); + List result = Lists.newArrayList(); + for (WorldGeneratorInfo option : worldGeneratorManager.getWorldGenerators()) { + if (enabledModuleNames.contains(option.getUri().getModuleName())) { + result.add(option); + } + } + + return result; + } + }); + worldGenerator.setVisibleOptions(3); + worldGenerator.bindSelection(new Binding() { + @Override + public WorldGeneratorInfo get() { + // get the default generator from the config. This is likely to have a user triggered selection. + WorldGeneratorInfo info = worldGeneratorManager.getWorldGeneratorInfo(config.getWorldGeneration().getDefaultGenerator()); + if (info != null && getAllEnabledModuleNames().contains(info.getUri().getModuleName())) { + return info; + } + + // just use the first available generator + for (WorldGeneratorInfo worldGenInfo : worldGeneratorManager.getWorldGenerators()) { + if (getAllEnabledModuleNames().contains(worldGenInfo.getUri().getModuleName())) { + set(worldGenInfo); + return worldGenInfo; + } + } + + return null; + } + + @Override + public void set(WorldGeneratorInfo value) { + if (value != null) { + config.getWorldGeneration().setDefaultGenerator(value.getUri()); + } + } + }); + worldGenerator.setOptionRenderer(new StringTextRenderer() { + @Override + public String getString(WorldGeneratorInfo value) { + if (value != null) { + return value.getDisplayName(); + } + return ""; + } + }); + } + final UIDropdownScrollable worldsDropdown = find("worlds", UIDropdownScrollable.class); + worldsDropdown.bindSelection(new Binding() { + @Override + public String get() { + return selectedWorld; + } + + @Override + public void set(String value) { + selectedWorld = value; + } + }); + + WidgetUtil.trySubscribe(this, "close", button -> + triggerBackAnimation() + ); + + WorldSetupScreen worldSetupScreen = getManager().createScreen(WorldSetupScreen.ASSET_URI, WorldSetupScreen.class); + WidgetUtil.trySubscribe(this, "worldConfig", button -> { + try { + if (!worlds.isEmpty() || !selectedWorld.isEmpty()) { + worldSetupScreen.setWorld(context, findWorldByName()); + triggerForwardAnimation(worldSetupScreen); + } else { + getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class).setMessage("Worlds List Empty!", "No world found to configure."); + } + } catch (UnresolvedWorldGeneratorException e) { + e.printStackTrace(); + } + }); + + WidgetUtil.trySubscribe(this, "addGenerator", button -> { + if (worldGenerator.getSelection().getUri().toString().equals("Core:heightMap")) { + getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class).setMessage("HeightMap not supported", "HeightMap is not supported for advanced setup right now, a game template will be introduced soon."); + + } else { + addNewWorld(worldGenerator.getSelection()); + List worldOptions = worlds; + worldsDropdown.setOptions(worldNames()); + } + //triggerForwardAnimation(worldSetupScreen); + }); + + WorldPreGenerationScreen worldPreGenerationScreen = getManager().createScreen(WorldPreGenerationScreen.ASSET_URI, WorldPreGenerationScreen.class); + WidgetUtil.trySubscribe(this, "continue", button -> { + if (!worlds.isEmpty()) { + try { + worldPreGenerationScreen.setEnvironment(context); + triggerForwardAnimation(worldPreGenerationScreen); + } catch (UnresolvedWorldGeneratorException e) { + e.printStackTrace(); + } + } else { + getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class).setMessage("Worlds List Empty!", "Please select a world generator and add words to the dropdown!"); + } + }); + } + + @Override + public void onOpened() { + worlds.clear(); + worldNumber = 0; + final UIDropdownScrollable worldsDropdown = find("worlds", UIDropdownScrollable.class); + List worldOptions = worlds; + worldsDropdown.setOptions(worldNames()); + selectedWorld = ""; + } + + private Set getAllEnabledModuleNames() { + Set enabledModules = Sets.newHashSet(); + for (Name moduleName : config.getDefaultModSelection().listModules()) { + enabledModules.add(moduleName); + recursivelyAddModuleDependencies(enabledModules, moduleName); + } + + return enabledModules; + } + + + private void recursivelyAddModuleDependencies(Set modules, Name moduleName) { + Module module = moduleManager.getRegistry().getLatestModuleVersion(moduleName); + if (module != null) { + for (DependencyInfo dependencyInfo : module.getMetadata().getDependencies()) { + modules.add(dependencyInfo.getId()); + recursivelyAddModuleDependencies(modules, dependencyInfo.getId()); + } + } + } + + /** + * Called whenever the user decides to add a new world. + * @param worldGeneratorInfo The {@link WorldGeneratorInfo} object for the new world. + */ + private void addNewWorld(WorldGeneratorInfo worldGeneratorInfo) { + selectedWorld = worldGeneratorInfo.getDisplayName() + '-' + worldNumber; + worlds.add(new WorldSetupWrapper(new Name(worldGeneratorInfo.getDisplayName() + '-' + worldNumber), worldGeneratorInfo)); + worldNumber++; + } + + /** + * This method switches the environment of the game to a temporary one needed for + * creating a game. It creates a new {@link Context} and only puts the minimum classes + * needed for successful game creation. + * @param wrapper takes the {@link org.terasology.rendering.nui.layers.mainMenu.selectModulesScreen.AdvancedGameSetupScreen} and pushes it into the new context. + */ + public void setEnvironment(UniverseWrapper wrapper) { + context = new ContextImpl(); + CoreRegistry.setContext(context); + ReflectFactory reflectFactory = new ReflectionReflectFactory(); + context.put(ReflectFactory.class, reflectFactory); + CopyStrategyLibrary copyStrategyLibrary = new CopyStrategyLibrary(reflectFactory); + context.put(CopyStrategyLibrary.class, copyStrategyLibrary); + context.put(NUIManager.class, getManager()); + context.put(UniverseSetupScreen.class, this); + assetTypeManager = new ModuleAwareAssetTypeManager(); + context.put(AssetManager.class, assetTypeManager.getAssetManager()); + context.put(ModuleAwareAssetTypeManager.class, assetTypeManager); + context.put(ModuleManager.class, moduleManager); + DependencyResolver resolver = new DependencyResolver(moduleManager.getRegistry()); + context.put(UniverseWrapper.class, wrapper); + ResolutionResult result = resolver.resolve(config.getDefaultModSelection().listModules()); + + if (result.isSuccess()) { + environment = moduleManager.loadEnvironment(result.getModules(), false); + context.put(ModuleEnvironment.class, environment); + context.put(WorldGeneratorPluginLibrary.class, new TempWorldGeneratorPluginLibrary(environment, context)); + initAssets(); + + EnvironmentSwitchHandler environmentSwitcher = new EnvironmentSwitchHandler(); + context.put(EnvironmentSwitchHandler.class, environmentSwitcher); + + environmentSwitcher.handleSwitchToPreviewEnvironment(context, environment); + } + } + + private void initAssets() { + DefaultBlockFamilyFactoryRegistry familyFactoryRegistry = new DefaultBlockFamilyFactoryRegistry(); + context.put(BlockFamilyFactoryRegistry.class, familyFactoryRegistry); + + // cast lambdas explicitly to avoid inconsistent compiler behavior wrt. type inference + assetTypeManager.registerCoreAssetType(Prefab.class, + (AssetFactory) PojoPrefab::new, false, "prefabs"); + assetTypeManager.registerCoreAssetType(BlockShape.class, + (AssetFactory) BlockShapeImpl::new, "shapes"); + assetTypeManager.registerCoreAssetType(BlockSounds.class, + (AssetFactory) BlockSounds::new, "blockSounds"); + assetTypeManager.registerCoreAssetType(BlockTile.class, + (AssetFactory) BlockTile::new, "blockTiles"); + assetTypeManager.registerCoreAssetType(BlockFamilyDefinition.class, + (AssetFactory) BlockFamilyDefinition::new, "blocks"); + assetTypeManager.registerCoreFormat(BlockFamilyDefinition.class, + new BlockFamilyDefinitionFormat(assetTypeManager.getAssetManager(), familyFactoryRegistry)); + assetTypeManager.registerCoreAssetType(UISkin.class, + (AssetFactory) UISkin::new, "skins"); + assetTypeManager.registerCoreAssetType(BehaviorTree.class, + (AssetFactory) BehaviorTree::new, false, "behaviors"); + assetTypeManager.registerCoreAssetType(UIElement.class, + (AssetFactory) UIElement::new, "ui"); + } + + /** + * Create a list of the names of the world, so that they can be displayed as simple String + * in the drop-down. + * @return A list of world names encoded as a String + */ + public List worldNames() { + List worldNamesList = Lists.newArrayList(); + for (WorldSetupWrapper world : worlds) { + worldNamesList.add(world.getWorldName().toString()); + } + return worldNamesList; + } + + /** + * This method takes the name of the selected world as String and return the corresponding + * WorldSetupWrapper object. + * @return {@link WorldSetupWrapper} object. + */ + public WorldSetupWrapper findWorldByName() { + for (WorldSetupWrapper world : worlds) { + if (world.getWorldName().toString().equals(selectedWorld)) { + return world; + } + } + return null; + } + + public List getWorldsList() { + return worlds; + } + + /** + * @return the selcted world in the drop-down. + */ + public String getSelectedWorld() { + return selectedWorld; + } + +} + diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/UniverseWrapper.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/UniverseWrapper.java new file mode 100644 index 00000000000..5df8bd7ffda --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/UniverseWrapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 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.rendering.nui.layers.mainMenu; + +/** + * A class which stores the universe level properties for a game like whether + * the game is single-player or multi-player, seed value and the game name. + */ +public class UniverseWrapper { + + private String seed; + private boolean loadingAsServer; + private String gameName; + + public void setSeed(String seed) { + this.seed = seed; + } + + public String getSeed() { + return seed; + } + + public void setLoadingAsServer(boolean loadingAsServer) { + this.loadingAsServer = loadingAsServer; + } + + public boolean getLoadingAsServer() { + return loadingAsServer; + } + + public void setGameName(String gameName) { + this.gameName = gameName; + } + + public String getGameName() { + return gameName; + } +} diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/WorldPreGenerationScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/WorldPreGenerationScreen.java new file mode 100644 index 00000000000..0348f2e0af9 --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/WorldPreGenerationScreen.java @@ -0,0 +1,274 @@ +/* + * Copyright 2018 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.rendering.nui.layers.mainMenu; + +import com.google.common.collect.Lists; +import org.terasology.assets.ResourceUrn; +import org.terasology.config.Config; +import org.terasology.context.Context; +import org.terasology.engine.module.ModuleManager; +import org.terasology.math.TeraMath; +import org.terasology.module.ModuleEnvironment; +import org.terasology.registry.In; +import org.terasology.rendering.assets.texture.Texture; +import org.terasology.rendering.assets.texture.TextureData; +import org.terasology.rendering.nui.CoreScreenLayer; +import org.terasology.rendering.nui.NUIManager; +import org.terasology.rendering.nui.WidgetUtil; +import org.terasology.rendering.nui.databinding.Binding; +import org.terasology.rendering.nui.layers.mainMenu.preview.FacetLayerPreview; +import org.terasology.rendering.nui.layers.mainMenu.preview.PreviewGenerator; +import org.terasology.rendering.nui.widgets.UIDropdownScrollable; +import org.terasology.rendering.nui.widgets.UIImage; +import org.terasology.rendering.nui.widgets.UISlider; +import org.terasology.rendering.nui.widgets.UISliderOnChangeTriggeredListener; +import org.terasology.rendering.world.WorldSetupWrapper; +import org.terasology.utilities.Assets; +import org.terasology.world.generator.UnresolvedWorldGeneratorException; +import org.terasology.world.generator.WorldGenerator; +import org.terasology.world.generator.internal.WorldGeneratorManager; +import org.terasology.world.generator.plugin.TempWorldGeneratorPluginLibrary; +import org.terasology.world.generator.plugin.WorldGeneratorPluginLibrary; +import org.terasology.world.zones.Zone; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +/** + * This class lets the user preview different worlds added in the + * {@link UniverseSetupScreen}. Each world is still configurable and its seed + * can be changed by the re-roll button. Note that each world has a unique seed. + */ +public class WorldPreGenerationScreen extends CoreScreenLayer implements UISliderOnChangeTriggeredListener { + + public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:worldPreGenerationScreen"); + + @In + private ModuleManager moduleManager; + + @In + private Config config; + + private ModuleEnvironment environment; + private WorldGenerator worldGenerator; + private Texture texture; + private UIImage previewImage; + private Context context; + private PreviewGenerator previewGen; + private List worldList; + private String selectedWorld; + private List worldNames; + private int seedNumber = 0; + private UISlider zoomSlider; + + /** + * A function called before the screen comes to the forefront to setup the environment + * and extract necessary objects from the new Context. + * + * @param subContext The new environment created in {@link UniverseSetupScreen} + * @throws UnresolvedWorldGeneratorException The creation of a world generator can throw this Exception + */ + public void setEnvironment(Context subContext) throws UnresolvedWorldGeneratorException { + + context = subContext; + environment = context.get(ModuleEnvironment.class); + context.put(WorldGeneratorPluginLibrary.class, new TempWorldGeneratorPluginLibrary(environment, context)); + worldList = context.get(UniverseSetupScreen.class).getWorldsList(); + selectedWorld = context.get(UniverseSetupScreen.class).getSelectedWorld(); + worldNames = context.get(UniverseSetupScreen.class).worldNames(); + if (findWorldByName().getWorldGenerator() == null) { + worldGenerator = WorldGeneratorManager.createWorldGenerator(findWorldByName().getWorldGeneratorInfo().getUri(), context, environment); + findWorldByName().setWorldGenerator(worldGenerator); + } else { + worldGenerator = findWorldByName().getWorldGenerator(); + } + + worldGenerator.setWorldSeed(createSeed(selectedWorld)); + final UIDropdownScrollable worldsDropdown = find("worlds", UIDropdownScrollable.class); + worldsDropdown.setOptions(worldNames); + genTexture(); + + List previewZones = Lists.newArrayList(worldGenerator.getZones()) + .stream() + .filter(z -> !z.getPreviewLayers().isEmpty()) + .collect(Collectors.toList()); + if (previewZones.isEmpty()) { + previewGen = new FacetLayerPreview(environment, worldGenerator); + } + } + + @Override + public void initialise() { + + zoomSlider = find("zoomSlider", UISlider.class); + if (zoomSlider != null) { + zoomSlider.setValue(2f); + zoomSlider.setUiSliderOnChangeTriggeredListener(this); + } + + + final UIDropdownScrollable worldsDropdown = find("worlds", UIDropdownScrollable.class); + worldsDropdown.bindSelection(new Binding() { + @Override + public String get() { + return selectedWorld; + } + + @Override + public void set(String value) { + selectedWorld = value; + try { + if (findWorldByName().getWorldGenerator() == null) { + worldGenerator = WorldGeneratorManager.createWorldGenerator(findWorldByName().getWorldGeneratorInfo().getUri(), context, environment); + findWorldByName().setWorldGenerator(worldGenerator); + } else { + worldGenerator = findWorldByName().getWorldGenerator(); + } + if (worldGenerator.getWorldSeed() == null) { + worldGenerator.setWorldSeed(createSeed(selectedWorld)); + } + previewGen = new FacetLayerPreview(environment, worldGenerator); + updatePreview(); + } catch (UnresolvedWorldGeneratorException e) { + e.printStackTrace(); + } + } + }); + + WidgetUtil.trySubscribe(this, "reRoll", button -> { + worldGenerator.setWorldSeed(createSeed(selectedWorld)); + updatePreview(); + }); + + StartPlayingScreen startPlayingScreen = getManager().createScreen(StartPlayingScreen.ASSET_URI, StartPlayingScreen.class); + WidgetUtil.trySubscribe(this, "continue", button -> { + startPlayingScreen.setTargetWorld(findWorldByName(), texture, context); + triggerForwardAnimation(startPlayingScreen); + }); + + WorldSetupScreen worldSetupScreen = getManager().createScreen(WorldSetupScreen.ASSET_URI, WorldSetupScreen.class); + WidgetUtil.trySubscribe(this, "config", button -> { + try { + if (!selectedWorld.isEmpty()) { + worldSetupScreen.setWorld(context, findWorldByName()); + triggerForwardAnimation(worldSetupScreen); + } else { + getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class).setMessage("Worlds List Empty!", "No world found to configure."); + } + } catch (UnresolvedWorldGeneratorException e) { + e.printStackTrace(); + } + }); + + WidgetUtil.trySubscribe(this, "close", button -> + triggerBackAnimation() + ); + } + + @Override + public void onOpened() { + try { + if (findWorldByName().getWorldGenerator() == null) { + worldGenerator = WorldGeneratorManager.createWorldGenerator(findWorldByName().getWorldGeneratorInfo().getUri(), context, environment); + findWorldByName().setWorldGenerator(worldGenerator); + } else { + worldGenerator = findWorldByName().getWorldGenerator(); + } + if (worldGenerator.getWorldSeed().isEmpty()) { + worldGenerator.setWorldSeed(createSeed(selectedWorld)); + } + previewGen = new FacetLayerPreview(environment, worldGenerator); + updatePreview(); + } catch (UnresolvedWorldGeneratorException e) { + e.printStackTrace(); + } + } + + /** + * Generates a texture and sets it to the image view, thus previewing the world. + */ + private void genTexture() { + int imgWidth = 384; + int imgHeight = 384; + ByteBuffer buffer = ByteBuffer.allocateDirect(imgWidth * imgHeight * Integer.BYTES); + ByteBuffer[] data = new ByteBuffer[]{buffer}; + ResourceUrn uri = new ResourceUrn("engine:terrainPreview"); + TextureData texData = new TextureData(imgWidth, imgHeight, data, Texture.WrapMode.CLAMP, Texture.FilterMode.LINEAR); + texture = Assets.generateAsset(uri, texData, Texture.class); + + previewImage = find("preview", UIImage.class); + previewImage.setImage(texture); + } + + /** + * Updates the preview according to any changes made to the configurator. + * Also pops up a message and keeps track of percentage world preview prepared. + */ + private void updatePreview() { + + final NUIManager manager = context.get(NUIManager.class); + final WaitPopup popup = manager.pushScreen(WaitPopup.ASSET_URI, WaitPopup.class); + popup.setMessage("Updating Preview", "Please wait ..."); + + ProgressListener progressListener = progress -> + popup.setMessage("Updating Preview", String.format("Please wait ... %d%%", (int) (progress * 100f))); + + Callable operation = () -> { + int zoom = TeraMath.floorToInt(zoomSlider.getValue()); + TextureData data = texture.getData(); + + previewGen.render(data, zoom, progressListener); + + return data; + }; + + popup.onSuccess(texture::reload); + popup.startOperation(operation, true); + } + + /** + * This method takes the name of the world selected in the worldsDropdown + * as String and return the corresponding WorldSetupWrapper object. + * + * @return {@link WorldSetupWrapper} object of the selected world. + */ + private WorldSetupWrapper findWorldByName() { + for (WorldSetupWrapper world : worldList) { + if (world.getWorldName().toString().equals(selectedWorld)) { + return world; + } + } + return null; + } + + /** + * Sets a unique seed by simply appending the world name with an incrementing number. + * + * @param world {@link WorldSetupWrapper} object whose seed is to be set. + * @return The seed as a string. + */ + private String createSeed(String world) { + return world + seedNumber++; + } + + @Override + public void onSliderValueChanged(float val) { + updatePreview(); + } + +} diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/WorldSetupScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/WorldSetupScreen.java new file mode 100644 index 00000000000..a972029a331 --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/WorldSetupScreen.java @@ -0,0 +1,234 @@ +/* + * Copyright 2018 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.rendering.nui.layers.mainMenu; + +import org.terasology.assets.ResourceUrn; +import org.terasology.config.Config; +import org.terasology.context.Context; +import org.terasology.engine.SimpleUri; +import org.terasology.entitySystem.Component; +import org.terasology.entitySystem.metadata.ComponentLibrary; +import org.terasology.i18n.TranslationSystem; +import org.terasology.module.ModuleEnvironment; +import org.terasology.reflection.metadata.FieldMetadata; +import org.terasology.registry.In; +import org.terasology.rendering.nui.CoreScreenLayer; +import org.terasology.rendering.nui.WidgetUtil; +import org.terasology.rendering.nui.databinding.Binding; +import org.terasology.rendering.nui.layouts.PropertyLayout; +import org.terasology.rendering.nui.properties.Property; +import org.terasology.rendering.nui.properties.PropertyOrdering; +import org.terasology.rendering.nui.properties.PropertyProvider; +import org.terasology.rendering.nui.widgets.UILabel; +import org.terasology.rendering.world.WorldSetupWrapper; +import org.terasology.world.generator.UnresolvedWorldGeneratorException; +import org.terasology.world.generator.WorldConfigurator; +import org.terasology.world.generator.WorldGenerator; +import org.terasology.world.generator.internal.WorldGeneratorManager; +import org.terasology.world.generator.plugin.TempWorldGeneratorPluginLibrary; +import org.terasology.world.generator.plugin.WorldGeneratorPluginLibrary; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Allows configuration of a single world. + */ +public class WorldSetupScreen extends CoreScreenLayer { + + public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:worldSetupScreen"); + + @In + private WorldGeneratorManager worldGeneratorManager; + + @In + private Config config; + + @In + private TranslationSystem translationSystem; + + private WorldGenerator worldGenerator; + private WorldSetupWrapper world; + private ModuleEnvironment environment; + private Context context; + private WorldConfigurator oldWorldConfig; + + @Override + public void initialise() { + + WidgetUtil.trySubscribe(this, "close", button -> { + triggerBackAnimation(); + }); + } + + @Override + public void onOpened() { + UILabel subitle = find("subtitle", UILabel.class); + subitle.setText(translationSystem.translate("${engine:menu#world-setup}") + " for " + world.getWorldName().toString()); + } + + /** + * This method sets the world whose properties are to be changed. This function is called before the screen comes + * to the forefront. + * @param subContext the new environment created in {@link UniverseSetupScreen} + * @param worldSelected the world whose configurations are to be changed. + * @throws UnresolvedWorldGeneratorException + */ + public void setWorld(Context subContext, WorldSetupWrapper worldSelected) throws UnresolvedWorldGeneratorException { + world = worldSelected; + context = subContext; + SimpleUri worldGenUri = worldSelected.getWorldGeneratorInfo().getUri(); + environment = context.get(ModuleEnvironment.class); + context.put(WorldGeneratorPluginLibrary.class, new TempWorldGeneratorPluginLibrary(environment, context)); + if (world.getWorldGenerator() == null) { + worldGenerator = WorldGeneratorManager.createWorldGenerator(worldGenUri, context, environment); + world.setWorldGenerator(worldGenerator); + } else { + worldGenerator = world.getWorldGenerator(); + } + configureProperties(); + } + + /** + * Assigns a {@link WorldConfigurator} for every world if it doesn't exist. Using + * the WorldConfigurator it gets the properties associated with that world. + */ + private void configureProperties() { + + PropertyLayout propLayout = find("properties", PropertyLayout.class); + propLayout.setOrdering(PropertyOrdering.byLabel()); + propLayout.clear(); + WorldConfigurator worldConfig; + if (world.getWorldConfigurator() != null) { + worldConfig = world.getWorldConfigurator(); + } else { + worldConfig = worldGenerator.getConfigurator(); + world.setWorldConfigurator(worldConfig); + } + oldWorldConfig = worldConfig; + + Map params = worldConfig.getProperties(); + + for (String key : params.keySet()) { + Class clazz = params.get(key).getClass(); + Component comp = config.getModuleConfig(worldGenerator.getUri(), key, clazz); + if (comp != null) { + // use the data from the config instead of defaults + worldConfig.setProperty(key, comp); + } + } + + ComponentLibrary compLib = context.get(ComponentLibrary.class); + + for (String label : params.keySet()) { + + PropertyProvider provider = new PropertyProvider() { + @Override + protected Binding createTextBinding(Object target, FieldMetadata fieldMetadata) { + return new WorldSetupScreen.WorldConfigBinding<>(worldConfig, label, compLib, fieldMetadata); + } + + @Override + protected Binding createFloatBinding(Object target, FieldMetadata fieldMetadata) { + return new WorldSetupScreen.WorldConfigNumberBinding(worldConfig, label, compLib, fieldMetadata); + } + }; + + Component target = params.get(label); + List> properties = provider.createProperties(target); + propLayout.addProperties(label, properties); + } + } + + /** + * Updates a world configurator through setProperty() whenever Binding#set() is called. + */ + private static class WorldConfigBinding implements Binding { + private final String label; + private final WorldConfigurator worldConfig; + private final FieldMetadata fieldMetadata; + private final ComponentLibrary compLib; + + protected WorldConfigBinding(WorldConfigurator config, String label, ComponentLibrary compLib, FieldMetadata fieldMetadata) { + this.worldConfig = config; + this.label = label; + this.compLib = compLib; + this.fieldMetadata = fieldMetadata; + } + + @Override + public T get() { + Component comp = worldConfig.getProperties().get(label); + return fieldMetadata.getValue(comp); + } + + @Override + public void set(T value) { + T old = get(); + + if (!Objects.equals(old, value)) { + cloneAndSet(label, value); + } + } + + private void cloneAndSet(String group, Object value) { + Component comp = worldConfig.getProperties().get(group); + Component clone = compLib.copy(comp); + fieldMetadata.setValue(clone, value); + + // notify the world generator about the new component + worldConfig.setProperty(label, clone); + } + } + + private static class WorldConfigNumberBinding implements Binding { + + private WorldSetupScreen.WorldConfigBinding binding; + + @SuppressWarnings("unchecked") + protected WorldConfigNumberBinding(WorldConfigurator config, String label, ComponentLibrary compLib, FieldMetadata field) { + Class type = field.getType(); + if (type == Integer.TYPE || type == Integer.class) { + this.binding = new WorldSetupScreen.WorldConfigBinding<>(config, label, compLib, (FieldMetadata) field); + } else if (type == Float.TYPE || type == Float.class) { + this.binding = new WorldSetupScreen.WorldConfigBinding<>(config, label, compLib, (FieldMetadata) field); + } + } + + @Override + public Float get() { + Number val = binding.get(); + if (val instanceof Float) { + // use boxed instance directly + return (Float) val; + } + // create a boxed instance otherwise + return val.floatValue(); + } + + @Override + @SuppressWarnings("unchecked") + public void set(Float value) { + Class type = binding.fieldMetadata.getType(); + if (type == Integer.TYPE || type == Integer.class) { + ((Binding) binding).set(value.intValue()); + } else if (type == Float.TYPE || type == Float.class) { + ((Binding) binding).set(value); + } + } + } +} diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/selectModulesScreen/AdvancedGameSetupScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/selectModulesScreen/AdvancedGameSetupScreen.java new file mode 100644 index 00000000000..c707b1c60f7 --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/selectModulesScreen/AdvancedGameSetupScreen.java @@ -0,0 +1,713 @@ +/* + * Copyright 2018 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.rendering.nui.layers.mainMenu.selectModulesScreen; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.assets.ResourceUrn; +import org.terasology.config.Config; +import org.terasology.config.ModuleConfig; +import org.terasology.config.SelectModulesConfig; +import org.terasology.engine.SimpleUri; +import org.terasology.engine.TerasologyConstants; +import org.terasology.engine.module.DependencyResolutionFailedException; +import org.terasology.engine.module.ModuleInstaller; +import org.terasology.engine.module.ModuleManager; +import org.terasology.engine.module.StandardModuleExtension; +import org.terasology.i18n.TranslationSystem; +import org.terasology.math.geom.Vector2i; +import org.terasology.module.DependencyInfo; +import org.terasology.module.DependencyResolver; +import org.terasology.module.Module; +import org.terasology.module.ModuleMetadata; +import org.terasology.module.ResolutionResult; +import org.terasology.naming.Name; +import org.terasology.registry.In; +import org.terasology.rendering.nui.Canvas; +import org.terasology.rendering.nui.CoreScreenLayer; +import org.terasology.rendering.nui.WidgetUtil; +import org.terasology.rendering.nui.animation.MenuAnimationSystems; +import org.terasology.rendering.nui.databinding.Binding; +import org.terasology.rendering.nui.databinding.ReadOnlyBinding; +import org.terasology.rendering.nui.itemRendering.AbstractItemRenderer; +import org.terasology.rendering.nui.layers.mainMenu.ConfirmPopup; +import org.terasology.rendering.nui.layers.mainMenu.MessagePopup; +import org.terasology.rendering.nui.layers.mainMenu.UniverseSetupScreen; +import org.terasology.rendering.nui.layers.mainMenu.UniverseWrapper; +import org.terasology.rendering.nui.layers.mainMenu.WaitPopup; +import org.terasology.rendering.nui.widgets.ResettableUIText; +import org.terasology.rendering.nui.widgets.TextChangeEventListener; +import org.terasology.rendering.nui.widgets.UIButton; +import org.terasology.rendering.nui.widgets.UICheckbox; +import org.terasology.rendering.nui.widgets.UILabel; +import org.terasology.rendering.nui.widgets.UIList; +import org.terasology.rendering.nui.widgets.UIText; +import org.terasology.utilities.random.FastRandom; +import org.terasology.world.generator.internal.WorldGeneratorManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +/** + * This screen loads up all the modules, local and remote. The modules + * are displayed in different colours according to their state of selection. + * For downloading remote modules list a FutureTask is used. The modules can also be filtered and the choice + * of filtering is saved in the config. + */ +public class AdvancedGameSetupScreen extends CoreScreenLayer { + + public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:advancedGameSetupScreen"); + + private static final Logger logger = LoggerFactory.getLogger(SelectModulesScreen.class); + private final Comparator moduleInfoComparator = (o1, o2) -> o1.getMetadata() + .getDisplayName().toString().compareTo(o2.getMetadata().getDisplayName().toString()); + @In + private ModuleManager moduleManager; + @In + private Config config; + @In + private WorldGeneratorManager worldGenManager; + @In + private TranslationSystem translationSystem; + + private Map modulesLookup; + private List sortedModules; + private List allSortedModules; + private DependencyResolver resolver; + private Future remoteModuleRegistryUpdater; + private boolean needsUpdate = true; + private ResettableUIText moduleSearch; + private SelectModulesConfig selectModulesConfig; + private UniverseWrapper universeWrapper; + + @Override + public void onOpened() { + super.onOpened(); + + for (ModuleSelectionInfo info : sortedModules) { + info.setExplicitSelection(config.getDefaultModSelection().hasModule(info.getMetadata().getId())); + } + + refreshSelection(); + } + + @Override + public void initialise() { + + setAnimationSystem(MenuAnimationSystems.createDefaultSwipeAnimation()); + remoteModuleRegistryUpdater = Executors.newSingleThreadExecutor() + .submit(moduleManager.getInstallManager().updateRemoteRegistry()); + + final UIText seed = find("seed", UIText.class); + if (seed != null) { + seed.setText(new FastRandom().nextString(32)); + } + + selectModulesConfig = config.getSelectModulesConfig(); + + resolver = new DependencyResolver(moduleManager.getRegistry()); + + modulesLookup = Maps.newHashMap(); + sortedModules = Lists.newArrayList(); + for (Name moduleId : moduleManager.getRegistry().getModuleIds()) { + Module latestVersion = moduleManager.getRegistry().getLatestModuleVersion(moduleId); + if (!latestVersion.isOnClasspath()) { + ModuleSelectionInfo info = ModuleSelectionInfo.local(latestVersion); + modulesLookup.put(info.getMetadata().getId(), info); + sortedModules.add(info); + } + } + + Collections.sort(sortedModules, moduleInfoComparator); + allSortedModules = new ArrayList<>(sortedModules); + + final UIList moduleList = find("moduleList", UIList.class); + if (moduleList != null) { + moduleList.setList(sortedModules); + moduleList.setItemRenderer(new AbstractItemRenderer() { + + public String getString(ModuleSelectionInfo value) { + return value.getMetadata().getDisplayName().toString(); + } + + @Override + public void draw(ModuleSelectionInfo value, Canvas canvas) { + if (isSelectedGameplayModule(value) && value.isValidToSelect()) { + canvas.setMode("gameplay"); + } else if (value.isSelected() && value.isExplicitSelection()) { + canvas.setMode("enabled"); + } else if (value.isSelected()) { + canvas.setMode("dependency"); + } else if (!value.isPresent()) { + canvas.setMode("disabled"); + } else if (!value.isValidToSelect()) { + canvas.setMode("invalid"); + } else { + canvas.setMode("available"); + } + canvas.drawText(getString(value), canvas.getRegion()); + } + + @Override + public Vector2i getPreferredSize(ModuleSelectionInfo value, Canvas canvas) { + String text = getString(value); + return new Vector2i(canvas.getCurrentStyle().getFont().getWidth(text), + canvas.getCurrentStyle().getFont().getLineHeight()); + } + }); + + // ItemActivateEventListener is triggered by double clicking + moduleList.subscribe((widget, item) -> { + if (item.isSelected() && moduleList.getSelection().isExplicitSelection()) { + deselect(item); + } else if (item.isValidToSelect()) { + select(item); + } + }); + + moduleSearch = find("moduleSearch", ResettableUIText.class); + if (moduleSearch != null) { + moduleSearch.subscribe(new TextChangeEventListener() { + @Override + public void onTextChange(String oldText, String newText) { + filterModules(); + } + }); + } + + final Binding moduleInfoBinding = new ReadOnlyBinding() { + @Override + public ModuleMetadata get() { + if (moduleList.getSelection() != null) { + return moduleList.getSelection().getMetadata(); + } + return null; + } + }; + + UILabel name = find("name", UILabel.class); + if (name != null) { + name.bindText(new ReadOnlyBinding() { + @Override + public String get() { + if (moduleInfoBinding.get() != null) { + return moduleInfoBinding.get().getDisplayName().toString(); + } + return ""; + } + }); + } + + UILabel installedVersion = find("installedVersion", UILabel.class); + if (installedVersion != null) { + installedVersion.bindText(new ReadOnlyBinding() { + @Override + public String get() { + ModuleSelectionInfo sel = moduleList.getSelection(); + if (sel == null) { + return ""; + } + return sel.isPresent() ? sel.getMetadata().getVersion().toString() + : translationSystem.translate("${engine:menu#module-version-installed-none}"); + } + }); + } + + UILabel onlineVersion = find("onlineVersion", UILabel.class); + if (onlineVersion != null) { + onlineVersion.bindText(new ReadOnlyBinding() { + @Override + public String get() { + ModuleSelectionInfo sel = moduleList.getSelection(); + if (sel == null) { + return ""; + } + return (sel.getOnlineVersion() != null) ? sel.getOnlineVersion().getVersion().toString() + : "none"; + } + }); + } + + UILabel description = find("description", UILabel.class); + if (description != null) { + description.bindText(new ReadOnlyBinding() { + @Override + public String get() { + ModuleMetadata moduleMetadata = moduleInfoBinding.get(); + if (moduleMetadata != null) { + String dependenciesNames; + List dependencies = moduleMetadata.getDependencies(); + if (dependencies != null && dependencies.size() > 0) { + dependenciesNames = translationSystem + .translate("${engine:menu#module-dependencies-exist}") + ":" + '\n'; + for (DependencyInfo dependency : dependencies) { + dependenciesNames += " " + dependency.getId().toString() + '\n'; + } + } else { + dependenciesNames = translationSystem + .translate("${engine:menu#module-dependencies-empty}") + "."; + } + return moduleMetadata.getDescription().toString() + '\n' + '\n' + dependenciesNames; + } + return ""; + } + }); + } + + UILabel status = find("status", UILabel.class); + if (status != null) { + status.bindText(new ReadOnlyBinding() { + @Override + public String get() { + ModuleSelectionInfo info = moduleList.getSelection(); + if (info != null) { + if (isSelectedGameplayModule(info)) { + return translationSystem.translate("${engine:menu#module-status-activegameplay}"); + } else if (info.isSelected() && info.isExplicitSelection()) { + return translationSystem.translate("${engine:menu#module-status-activated}"); + } else if (info.isSelected()) { + return translationSystem.translate("${engine:menu#module-status-dependency}"); + } else if (!info.isPresent()) { + return translationSystem.translate("${engine:menu#module-status-notpresent}"); + } else if (info.isValidToSelect()) { + return translationSystem.translate("${engine:menu#module-status-available}"); + } else { + return translationSystem.translate("${engine:menu#module-status-error}"); + } + } + return ""; + } + }); + } + + UIButton toggleActivate = find("toggleActivation", UIButton.class); + if (toggleActivate != null) { + toggleActivate.subscribe(button -> { + ModuleSelectionInfo info = moduleList.getSelection(); + if (info != null) { + // Toggle + if (info.isSelected() && info.isExplicitSelection()) { + deselect(info); + } else if (info.isValidToSelect()) { + select(info); + } + } + }); + toggleActivate.bindEnabled(new ReadOnlyBinding() { + @Override + public Boolean get() { + ModuleSelectionInfo info = moduleList.getSelection(); + return info != null && info.isPresent() && !isSelectedGameplayModule(info) + && (info.isSelected() || info.isValidToSelect()); + } + }); + toggleActivate.bindText(new ReadOnlyBinding() { + @Override + public String get() { + if (moduleList.getSelection() != null) { + if (moduleList.getSelection().isExplicitSelection()) { + return translationSystem.translate("${engine:menu#deactivate-module}"); + } else { + return translationSystem.translate("${engine:menu#activate-module}"); + } + } + // button should be disabled + return translationSystem.translate("${engine:menu#activate-module}"); + } + }); + } + + UIButton downloadButton = find("download", UIButton.class); + if (downloadButton != null) { + downloadButton.subscribe(button -> { + if (moduleList.getSelection() != null) { + ModuleSelectionInfo info = moduleList.getSelection(); + startDownloadingNewestModulesRequiredFor(info); + } + }); + downloadButton.bindEnabled(new ReadOnlyBinding() { + @Override + public Boolean get() { + ModuleSelectionInfo selection = moduleList.getSelection(); + if (null == selection) { + return false; + } + return selection.getOnlineVersion() != null; + } + }); + downloadButton.bindText(new ReadOnlyBinding() { + @Override + public String get() { + ModuleSelectionInfo info = moduleList.getSelection(); + if (info != null && !info.isPresent()) { + return translationSystem.translate("${engine:menu#download-module}"); + } else { + return translationSystem.translate("${engine:menu#update-module}"); + } + } + }); + } + + UIButton disableAll = find("disableAll", UIButton.class); + if (disableAll != null) { + disableAll.subscribe(button -> sortedModules.stream() + .filter(info -> info.isSelected() && info.isExplicitSelection()).forEach(this::deselect)); + } + + for (AdvancedGameSetupScreen.CheckboxAssociation checkboxAssociation : AdvancedGameSetupScreen.CheckboxAssociation.values()) { + String checkboxName = checkboxAssociation.getCheckboxName(); + StandardModuleExtension standardModuleExtension = checkboxAssociation.getStandardModuleExtension(); + + UICheckbox checkBox = find(checkboxName, UICheckbox.class); + if (null != checkBox) { + checkBox.setChecked(selectModulesConfig.isStandardModuleExtensionSelected(standardModuleExtension)); + checkBox.subscribe(e -> { + selectModulesConfig.toggleStandardModuleExtensionSelected(standardModuleExtension); + checkBox.setChecked( + selectModulesConfig.isStandardModuleExtensionSelected(standardModuleExtension)); + filterModules(); + }); + } else { + logger.error("Unable to find checkbox named " + checkboxName + " in " + ASSET_URI.toString()); + selectModulesConfig.unselectStandardModuleExtension(standardModuleExtension); + } + } + + UICheckbox localOnlyCheckbox = find("localOnlyCheckbox", UICheckbox.class); + localOnlyCheckbox.setChecked(selectModulesConfig.isLocalOnlySelected()); + localOnlyCheckbox.subscribe(e -> { + selectModulesConfig.toggleIsLocalOnlySelected(); + localOnlyCheckbox.setChecked(selectModulesConfig.isLocalOnlySelected()); + filterModules(); + }); + + UICheckbox uncategorizedCheckbox = find("uncategorizedCheckbox", UICheckbox.class); + uncategorizedCheckbox.setChecked(selectModulesConfig.isUncategorizedSelected()); + uncategorizedCheckbox.subscribe(e -> { + selectModulesConfig.toggleUncategorizedSelected(); + boolean isUncategorizedSelected = selectModulesConfig.isUncategorizedSelected(); + uncategorizedCheckbox.setChecked(isUncategorizedSelected); + for (AdvancedGameSetupScreen.CheckboxAssociation checkboxAssociation : AdvancedGameSetupScreen.CheckboxAssociation.values()) { + String checkboxName = checkboxAssociation.getCheckboxName(); + UICheckbox checkbox = find(checkboxName, UICheckbox.class); + if (null != checkbox) { + checkbox.setEnabled(!isUncategorizedSelected); + } + } + filterModules(); + }); + } + UniverseSetupScreen universeSetupScreen = getManager().createScreen(UniverseSetupScreen.ASSET_URI, UniverseSetupScreen.class); + WidgetUtil.trySubscribe(this, "continue", button -> { + universeSetupScreen.setEnvironment(universeWrapper); + universeWrapper.setSeed(seed.getText()); + triggerForwardAnimation(universeSetupScreen); + }); + + WidgetUtil.trySubscribe(this, "close", button -> triggerBackAnimation()); + } + + private void filterModules() { + sortedModules.clear(); + sortedModules.addAll(allSortedModules); + if (selectModulesConfig.isUncategorizedSelected()) { + uncategorizedModuleFilter(); + } else { + if (selectModulesConfig.isAnyStandardModuleExtensionSelected()) { + advancedModuleFilter(); + } + } + + if (selectModulesConfig.isLocalOnlySelected()) { + localModuleFilter(); + } + + filterText(); + } + + private void uncategorizedModuleFilter() { + Iterator iter = sortedModules.iterator(); + while (iter.hasNext()) { + ModuleSelectionInfo m = iter.next(); + Module module; + if (m.isPresent()) { + module = moduleManager.getRegistry().getLatestModuleVersion(m.getMetadata().getId()); + } else { + module = (m.getOnlineVersion() == null) ? m.getLatestVersion() : m.getOnlineVersion(); + } + + boolean isUncategorized = true; + Set booleanStandardModuleExtensionEnumSet = StandardModuleExtension.booleanPropertySet(); + for (StandardModuleExtension standardModuleExtension : booleanStandardModuleExtensionEnumSet) { + if (standardModuleExtension.isProvidedBy(module)) { + isUncategorized = false; + break; + } + } + + if (!isUncategorized) { + iter.remove(); + } + } + } + + private void localModuleFilter() { + Iterator iter = sortedModules.iterator(); + while (iter.hasNext()) { + if (!iter.next().isPresent()) { + iter.remove(); + } + } + } + + private void advancedModuleFilter() { + Iterator iter = sortedModules.iterator(); + while (iter.hasNext()) { + ModuleSelectionInfo m = iter.next(); + Module module; + if (m.isPresent()) { + module = moduleManager.getRegistry().getLatestModuleVersion(m.getMetadata().getId()); + } else { + module = (m.getOnlineVersion() == null) ? m.getLatestVersion() : m.getOnlineVersion(); + } + + boolean matches = false; + Collection selectedStandardModuleExtensions = selectModulesConfig + .getSelectedStandardModuleExtensions(); + for (StandardModuleExtension standardModuleExtension : selectedStandardModuleExtensions) { + if (standardModuleExtension.isProvidedBy(module)) { + matches = true; + break; + } + } + + if (!matches) { + iter.remove(); + } + } + } + + private void filterText() { + String newText = moduleSearch.getText(); + Iterator iter = sortedModules.iterator(); + while (iter.hasNext()) { + if (!iter.next().getMetadata().getDisplayName().toString().toLowerCase().contains(newText.toLowerCase())) { + iter.remove(); + } + } + } + + private void startDownloadingNewestModulesRequiredFor(ModuleSelectionInfo moduleMetadata) { + Set modulesToDownload; + try { + modulesToDownload = moduleManager.getInstallManager() + .getAllModulesToDownloadFor(moduleMetadata.getMetadata().getId()); + } catch (DependencyResolutionFailedException ex) { + MessagePopup messagePopup = getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class); + messagePopup.setMessage("Error", ex.getMessage()); + return; + } + ConfirmPopup confirmPopup = getManager().pushScreen(ConfirmPopup.ASSET_URI, ConfirmPopup.class); + confirmPopup.setMessage("Confirm Download", modulesToDownload.size() + " modules will be downloaded"); + confirmPopup.setOkHandler(() -> downloadModules(modulesToDownload)); + } + + private void downloadModules(Iterable modulesToDownload) { + final WaitPopup> popup = getManager().pushScreen(WaitPopup.ASSET_URI, WaitPopup.class); + popup.onSuccess(newModules -> { + for (Module module : newModules) { + modulesLookup.get(module.getId()).setLocalVersion(module); + updateValidToSelect(); + } + }); + ModuleInstaller operation = moduleManager.getInstallManager().createInstaller(modulesToDownload, + new DownloadPopupProgressListener(popup)); + popup.startOperation(operation, true); + } + + private void updateValidToSelect() { + List selectedModules = Lists.newArrayList(); + selectedModules.addAll(sortedModules.stream().filter(ModuleSelectionInfo::isSelected) + .map(info -> info.getMetadata().getId()).collect(Collectors.toList())); + Name[] selectedModulesArray = selectedModules.toArray(new Name[selectedModules.size()]); + sortedModules.stream().filter(info -> !info.isSelected()).forEach(info -> info + .setValidToSelect(resolver.resolve(info.getMetadata().getId(), selectedModulesArray).isSuccess())); + } + + private void setSelectedVersions(ResolutionResult currentSelectionResults) { + if (currentSelectionResults.isSuccess()) { + for (Module module : currentSelectionResults.getModules()) { + ModuleSelectionInfo info = modulesLookup.get(module.getId()); + + // the engine module is not listed + if (info != null) { + info.setSelectedVersion(module); + } + } + } + } + + private void updateModuleInformation() { + Iterable remoteModuleRegistry = moduleManager.getInstallManager().getRemoteRegistry(); + Set filtered = ImmutableSet.of(TerasologyConstants.ENGINE_MODULE, new Name("engine-test")); + for (Module remote : remoteModuleRegistry) { + ModuleSelectionInfo info = modulesLookup.get(remote.getId()); + if (!filtered.contains(remote.getId())) { + if (info == null) { + info = ModuleSelectionInfo.remote(remote); + modulesLookup.put(remote.getId(), info); + int pos = Collections.binarySearch(sortedModules, info, moduleInfoComparator); + if (pos < 0) { // not yet in the (sorted) list + sortedModules.add(-pos - 1, info); // use "insertion point" to keep the list sorted + allSortedModules.clear(); + allSortedModules.addAll(sortedModules); + } + } + info.setOnlineVersion(remote); + } + } + } + + @Override + public void update(float delta) { + super.update(delta); + + if (needsUpdate && remoteModuleRegistryUpdater.isDone() && !selectModulesConfig.isLocalOnlySelected()) { + needsUpdate = false; + try { + // it'a a Callable so just a null is returned, but it's used instead of a runnable because it can throw exceptions + remoteModuleRegistryUpdater.get(); + } catch (CancellationException | InterruptedException | ExecutionException ex) { + logger.warn("Failed to retrieve module list from meta server", ex); + MessagePopup message = getManager().pushScreen(MessagePopup.ASSET_URI, MessagePopup.class); + message.setMessage("Warning", + "Failed to retrieve a module list from the master server. Only locally installed modules are available."); + return; + } + updateModuleInformation(); + } + } + + @Override + public void onClosed() { + // moduleConfig passes the module collection to the Create Game Screen. + ModuleConfig moduleConfig = config.getDefaultModSelection(); + moduleConfig.clear(); + // Fetch all the selected/activated modules using allSortedModules + // instead of fetching only selected/activated modules from filtered collection + // of modules using sortedModules + allSortedModules.stream().filter(info -> info.isSelected() && info.isExplicitSelection()) + .forEach(info -> moduleConfig.addModule(info.getMetadata().getId())); + SimpleUri defaultGenerator = config.getWorldGeneration().getDefaultGenerator(); + ModuleSelectionInfo info = modulesLookup.get(defaultGenerator.getModuleName()); + if (info != null && !info.isSelected()) { + config.getWorldGeneration().setDefaultGenerator(new SimpleUri()); + } + + worldGenManager.refresh(); + + config.save(); + } + + @Override + public boolean isLowerLayerVisible() { + return false; + } + + private void select(ModuleSelectionInfo target) { + if (target.isValidToSelect() && !target.isExplicitSelection()) { + boolean previouslySelected = target.isSelected(); + target.setExplicitSelection(true); + if (!previouslySelected) { + refreshSelection(); + } + } + } + + private List getExplicitlySelectedModules() { + return allSortedModules.stream().filter(ModuleSelectionInfo::isExplicitSelection) + .map(info -> info.getMetadata().getId()).collect(Collectors.toCollection(ArrayList::new)); + } + + private void deselect(ModuleSelectionInfo target) { + // only deselect if it is already selected and if it is not the currently + // selected gameplay module + if (target.isExplicitSelection() && !isSelectedGameplayModule(target)) { + target.setExplicitSelection(false); + refreshSelection(); + } + } + + private boolean isSelectedGameplayModule(ModuleSelectionInfo target) { + return target.getMetadata().getId() + .equals(new Name(config.getDefaultModSelection().getDefaultGameplayModuleName())); + } + + private void refreshSelection() { + List selectedModules = getExplicitlySelectedModules(); + for (ModuleSelectionInfo info : sortedModules) { + info.setSelectedVersion(null); + } + setSelectedVersions(resolver.resolve(selectedModules)); + updateValidToSelect(); + } + + // TODO: should dynamically generate checkbox list from boolean + // StandardModuleExtensions rather than hardcoding associations here + private static enum CheckboxAssociation { + IS_LIBRARY("libraryCheckbox", StandardModuleExtension.IS_LIBRARY), + IS_ASSETPLAY("assetCheckbox", StandardModuleExtension.IS_ASSETPLAY), + IS_IS_WORLD("worldCheckbox", StandardModuleExtension.IS_WORLD), + IS_GAMEPLAY("gameplayCheckbox", StandardModuleExtension.IS_GAMEPLAY), + IS_AUGMENTATION("augmentationCheckbox", StandardModuleExtension.IS_AUGMENTATION), + IS_SPECIAL("specialCheckbox", StandardModuleExtension.IS_SPECIAL), + SERVER_SIDE_ONLY("serverSideOnlyCheckbox", StandardModuleExtension.SERVER_SIDE_ONLY); + + private String checkboxName; + private StandardModuleExtension standardModuleExtension; + + private CheckboxAssociation(String checkboxName, StandardModuleExtension standardModuleExtension) { + this.checkboxName = checkboxName; + this.standardModuleExtension = standardModuleExtension; + } + + public String getCheckboxName() { + return checkboxName; + } + + public StandardModuleExtension getStandardModuleExtension() { + return standardModuleExtension; + } + } + + public void setUniverseWrapper(UniverseWrapper wrapper) { + universeWrapper = wrapper; + } +} diff --git a/engine/src/main/java/org/terasology/rendering/nui/widgets/UISlider.java b/engine/src/main/java/org/terasology/rendering/nui/widgets/UISlider.java index 7484c035061..952f4c18fe9 100644 --- a/engine/src/main/java/org/terasology/rendering/nui/widgets/UISlider.java +++ b/engine/src/main/java/org/terasology/rendering/nui/widgets/UISlider.java @@ -39,6 +39,8 @@ public class UISlider extends CoreWidget { public static final String SLIDER = "slider"; public static final String TICKER = "ticker"; + private static UISliderOnChangeTriggeredListener uiSliderOnChangeTriggeredListener; + private InteractionListener tickerListener = new BaseInteractionListener() { private Vector2i offset = new Vector2i(); @@ -56,6 +58,9 @@ public boolean onMouseClick(NUIMouseClickEvent event) { @Override public void onMouseRelease(NUIMouseReleaseEvent event) { active = false; + if (uiSliderOnChangeTriggeredListener != null) { + uiSliderOnChangeTriggeredListener.onSliderValueChanged(getValue()); + } } @Override @@ -265,4 +270,9 @@ public void setPrecision(int precision) { private int pixelOffsetFor(float val, int width) { return TeraMath.floorToInt(width * (val - getMinimum()) / getRange()); } + + + public void setUiSliderOnChangeTriggeredListener(UISliderOnChangeTriggeredListener listener) { + uiSliderOnChangeTriggeredListener = listener; + } } diff --git a/engine/src/main/java/org/terasology/rendering/nui/widgets/UISliderOnChangeTriggeredListener.java b/engine/src/main/java/org/terasology/rendering/nui/widgets/UISliderOnChangeTriggeredListener.java new file mode 100644 index 00000000000..ae33d9aa3c0 --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/widgets/UISliderOnChangeTriggeredListener.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017 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.rendering.nui.widgets; + +/** + * A listener which runs when the value of a {@link UISlider} changes + */ +public interface UISliderOnChangeTriggeredListener { + + void onSliderValueChanged(float val); +} diff --git a/engine/src/main/java/org/terasology/rendering/world/WorldSetupWrapper.java b/engine/src/main/java/org/terasology/rendering/world/WorldSetupWrapper.java new file mode 100644 index 00000000000..6e9aa174a30 --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/world/WorldSetupWrapper.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 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.rendering.world; + +import org.terasology.naming.Name; +import org.terasology.world.generator.WorldConfigurator; +import org.terasology.world.generator.WorldGenerator; +import org.terasology.world.generator.internal.WorldGeneratorInfo; + +/** + * This class only has significance during the create game phase. + * Every world is an object of this class. + */ +public class WorldSetupWrapper { + + private Name worldName; + private WorldGeneratorInfo worldGeneratorInfo; + private WorldConfigurator worldConfigurator; + private WorldGenerator worldGenerator; + + /** + * A constructor to initialise a world object. + * @param worldName The World Name which is displayed in the drop-downs. + * @param worldGeneratorInfo Contains the {@link WorldGeneratorInfo} object for that world. + */ + public WorldSetupWrapper(Name worldName, WorldGeneratorInfo worldGeneratorInfo) { + this.worldName = worldName; + this.worldGeneratorInfo = worldGeneratorInfo; + } + + public WorldGeneratorInfo getWorldGeneratorInfo() { + return this.worldGeneratorInfo; + } + + public Name getWorldName() { + return this.worldName; + } + + public void setWorldConfigurator(WorldConfigurator worldConfigurator) { + this.worldConfigurator = worldConfigurator; + } + + public WorldConfigurator getWorldConfigurator() { + return worldConfigurator; + } + + public void setWorldGenerator(WorldGenerator worldGenerator) { + this.worldGenerator = worldGenerator; + } + + public WorldGenerator getWorldGenerator() { + return worldGenerator; + } +} diff --git a/engine/src/main/resources/assets/i18n/menu.lang b/engine/src/main/resources/assets/i18n/menu.lang index 22138d0fb84..5c450b91c22 100644 --- a/engine/src/main/resources/assets/i18n/menu.lang +++ b/engine/src/main/resources/assets/i18n/menu.lang @@ -1,7 +1,12 @@ { + "accept": "accept", "activate-module": "activate-module", + "add": "add", "add-server": "add-server", "add-server-tip": "add-server-tip", + "advance": "advance", + "advanced": "advanced", + "advanced-game-setup": "advanced-game-setup", "advanced-usage": "advanced-usage", "animate-grass": "animate-grass", "animate-water": "animate-water", @@ -71,8 +76,10 @@ "change-keybind-popup-title": "change-keybind-popup-title", "clamp-lighting": "clamp-lighting", "cloud-shadows": "cloud-shadows", + "config": "config", "connecting-to": "connecting-to", "connection-failed": "connection-failed", + "continue": "continue", "could-not-connect-to-server": "could-not-connect-to-server", "crash-reporter": "crash-reporter", "non-native-jvm-warn": "non-native-jvm-warn", @@ -130,6 +137,7 @@ "game-name": "game-name", "game-settings": "game-settings", "game-worlds": "game-worlds", + "game-world-generators": "game-world-generators", "gameplay": "gameplay", "gameplay-info": "gameplay-info", "host-multiplayer-game": "host-multiplayer-game", @@ -183,6 +191,7 @@ "movement-dead-zone": "movement-dead-zone", "music-volume": "music-volume", "new-binding": "new-binding", + "new-game-title": "new-game-title", "next-toolbar-item": "next-toolbar-item", "none": "none", "not-bound": "not-bound", @@ -221,6 +230,7 @@ "remove-confirmation-popup-message": "remove-confirmation-popup-message", "remove-binding": "remove-binding", "remove-server": "remove-server", + "re-roll": "re-roll", "reset-default": "reset-default", "reset-fov-amount": "reset-fov-amount", "resolution-scale": "resolution-scale", @@ -257,6 +267,7 @@ "ssao": "ssao", "start-game": "start-game", "start-singleplayer-game": "start-singleplayer-game", + "start-playing": "start-playing", "storage-service": "storage-service", "storage-service-conflict-message": "storage-service-conflict-message", "storage-service-conflict-keep-local": "storage-service-conflict-keep-local", @@ -302,7 +313,9 @@ "telemetry-modules": "telemetry-modules", "this-language-English": "this-language-English", "this-language-native": "this-language-native", + "universe-setup": "universe-setup", "update-module": "update-module", + "time-progression-during-pre-generation": "time-progression-during-pre-generation", "video-windowed-fullscreen": "video-windowed-fullscreen", "video-fullscreen": "video-fullscreen", "video-graphic-settings": "video-graphic-settings", @@ -337,5 +350,8 @@ "widget-selection-prompt": "widget-selection-prompt", "widget-selection-title": "widget-selection-title", "world-config-preview": "world-config-preview", - "world-seed": "world-seed" + "world-configuration": "world-configuration", + "world-seed": "world-seed", + "world-setup": "world-setup", + "world-pre-generation": "world-pre-generation" } \ No newline at end of file diff --git a/engine/src/main/resources/assets/i18n/menu_en.lang b/engine/src/main/resources/assets/i18n/menu_en.lang index 63e2f5bac8a..82cd2551176 100644 --- a/engine/src/main/resources/assets/i18n/menu_en.lang +++ b/engine/src/main/resources/assets/i18n/menu_en.lang @@ -1,7 +1,12 @@ { + "accept": "Accept", "activate-module": "Activate", + "add": "Add", "add-server": "Add New", "add-server-tip": "You can press ENTER to copy the name to the address field", + "advance": "Advance", + "advanced": "Advanced", + "advanced-game-setup": "Advanced Game Setup", "advanced-usage": "Advanced Usage!", "animate-grass": "Animate Grass", "animate-water": "Animate Water", @@ -72,8 +77,10 @@ "change-keybind-popup-title": "Key already bound", "clamp-lighting": "Clamp Lighting", "cloud-shadows": "Cloud Shadows", + "config": "Configure", "connecting-to": "Connecting to", "connection-failed": "Connection Failed!", + "continue": "Continue", "could-not-connect-to-server": "Could not connect to server", "crash-reporter": "Issue Reporter", "non-native-jvm-warn": "WARNING: you are using a 32-bit JVM on a 64-bit system!\nUse a 64-bit Java installation for the best performance.", @@ -133,6 +140,7 @@ "game-name": "Game name", "game-settings": "Settings", "game-worlds": "Worlds", + "game-world-generators": "World Generators", "gameplay": "Gameplay Template", "gameplay-info": "can click 'modules' to customize further", "host-multiplayer-game": "Host Game", @@ -186,6 +194,7 @@ "movement-dead-zone": "Movement Axis Dead Zone", "music-volume": "Music Volume", "new-binding": "New binding", + "new-game-title": "New Game", "next-toolbar-item": "Next Toolbar Item", "none": "none", "not-bound": "not bound", @@ -228,6 +237,7 @@ "remove-binding": "Remove", "remove-server": "Remove", "reset-default": "Reset", + "re-roll": "Re-Roll", "reset-fov-amount": "Reset fov amount", "resolution-scale": "Resolution Scale", "respawn": "Respawn", @@ -263,6 +273,7 @@ "ssao": "SSAO", "start-game": "Play", "start-singleplayer-game": "Singleplayer", + "start-playing": "Start Playing", "storage-service": "Identity storage service", "storage-service-conflict-message": "During the last synchronization of the locally stored client identities with the storage service, a conflict was detected. For the server with id %s, the locally stored certificate has id %s while the remote one has id %s. What do you want to do? Please note that keeping one of the versions will delete the other one; ignoring will keep the local identity locally and the remote identity remotely, and the conflict will be detected again during the next synchronization.", "storage-service-conflict-keep-local": "Keep local version (overwrite on the service)", @@ -308,7 +319,9 @@ "telemetry-modules": "Modules", "this-language-English": "English", "this-language-native": "English", + "universe-setup": "Universe Setup", "update-module": "Update", + "time-progression-during-pre-generation": "Time progression during pre-generation", "validation-username-max-length": "Username can't be longer then 100 chars", "video-display-mode": "Display Mode", "video-fullscreen": "Fullscreen", @@ -346,5 +359,8 @@ "widget-selection-prompt": "Select the type of the widget:", "widget-selection-title": "Widget Selection", "world-config-preview": "Details...", - "world-seed": "Seed" + "world-configuration": "World Configuration", + "world-seed": "Seed", + "world-setup": "World Setup", + "world-pre-generation": "World Pre-generation" } diff --git a/engine/src/main/resources/assets/ui/advancedGameSetupScreen.ui b/engine/src/main/resources/assets/ui/advancedGameSetupScreen.ui new file mode 100644 index 00000000000..b16db6889c5 --- /dev/null +++ b/engine/src/main/resources/assets/ui/advancedGameSetupScreen.ui @@ -0,0 +1,454 @@ +{ + "type": "AdvancedGameSetupScreen", + "skin": "engine:mainMenu", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "UIImage", + "image": "engine:terasology", + "id": "title", + "layoutInfo": { + "width": 512, + "height": 128, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "offset": 40 + } + } + }, + { + "type": "UILabel", + "id": "subtitle", + "family": "title", + "text": "${engine:menu#advanced-game-setup}", + "layoutInfo": { + "height": 18, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "title" + } + } + }, + { + "type": "UILabel", + "id": "warning", + "text": "${engine:menu#warn-modules}", + "family": "warning", + "layoutInfo": { + "height": 12, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "subtitle", + "offset": 36 + } + } + }, + { + "type": "UIBox", + "id": "container", + "layoutInfo": { + "width": 850, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 32, + "widget": "warning" + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 16 + } + } + }, + { + "type": "ColumnLayout", + "columns": 1, + "verticalSpacing": 8, + "horizontalSpacing": 8, + "layoutInfo": { + "width": 272, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "widget": "container", + "offset": 4 + }, + "position-bottom": { + "target": "TOP", + "widget": "moduleContainer", + "offset": 4 + } + }, + "contents": [ + { + "type": "UILabel", + "text": "${engine:menu#world-seed}" + }, + { + "type": "UIText", + "id": "seed" + } + ] + + }, + { + "type": "ColumnLayout", + "columns": 3, + "verticalSpacing": 16, + "horizontalSpacing": 8, + "id": "moduleContainer", + "layoutInfo": { + "width": 834, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "widget": "container", + "offset": 64 + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 24 + } + }, + "contents": [ + { + "type":"RelativeLayout", + "family":"AdvanceFilter", + "contents": [ + { + "type":"ColumnLayout", + "id":"advanceFilterItems", + "columns":2, + "column-widths":[0.1,0.90], + "verticalSpacing": 4, + "horizontalSpacing": -32 , + "contents": [ + { + "type": "UICheckbox", + "id": "localOnlyCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "Local Only", + "id": "localOnly" + }, + { + "type": "UICheckbox", + "id": "uncategorizedCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "Uncategorized", + "id": "uncategorized" + }, + { + "type": "UICheckbox", + "id": "serverSideOnlyCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "Server-side Only", + "id": "serverSideOnly" + }, + { + "type": "UICheckbox", + "id": "gameplayCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "Gameplay", + "id": "gameplay" + }, + { + "type": "UICheckbox", + "id": "worldCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "World ", + "id": "world" + }, + { + "type": "UICheckbox", + "id": "augmentationCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "Augmentation", + "id": "augmentation" + }, + { + "type": "UICheckbox", + "id":"libraryCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "id": "library", + "text": "Library" + }, + { + "type": "UICheckbox", + "id": "assetCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "Asset ", + "id": "asset" + }, + { + "type": "UICheckbox", + "id": "specialCheckbox", + "family": "option-grid" + }, + { + "type": "UILabel", + "text": "Special", + "id": "special" + } + ], + "layoutInfo": { + "use-content-height": true, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP" + } + } + } + ] + }, + { + "type": "RelativeLayout", + "family": "description", + "contents": [ + { + "type": "RowLayout", + "id": "searchLayout", + "contents": [ + { + "type": "UILabel", + "text": "${engine:menu#module-filter}: ", + "layoutInfo": { + "relativeWidth": 0.25 + } + }, + { + "type": "ResettableUIText", + "id": "moduleSearch", + "layoutInfo": { + "relativeWidth": 0.75, + "use-content-height": true + } + } + ], + "layoutInfo": { + "use-content-height": true, + "position-top": { + "target": "TOP" + } + } + }, + { + "type": "ScrollableArea", + "content": { + "type": "UIList", + "id": "moduleList", + "family": "module-list" + }, + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "offset": 32 + }, + "position-bottom": { + "target": "TOP", + "widget": "actionButtons", + "offset": 8 + } + } + }, + { + "type": "RowLayout", + "id": "actionButtons", + "horizontalSpacing": 8, + "contents": [ + { + "type": "UIButton", + "id": "disableAll", + "text": "${engine:menu#disable-all-modules}" + } + ], + "layoutInfo": { + "width": 250, + "height": 24, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM" + } + } + } + ] + }, + { + "type": "RelativeLayout", + "family": "description", + "contents": [ + { + "type": "ColumnLayout", + "id": "simpleItems", + "columns": 2, + "column-widths": [0.3, 0.7], + "verticalSpacing": 8, + "contents": [ + { + "type": "UILabel", + "text": "${engine:menu#module-name}: " + }, + { + "type": "UILabel", + "id": "name", + "text": "Module Name Goes Here" + }, + { + "type": "UILabel", + "text": "${engine:menu#module-version-installed}: " + }, + { + "type": "UILabel", + "id": "installedVersion", + "text": "Module Version Goes Here" + }, + { + "type": "UILabel", + "text": "${engine:menu#module-version-available}: " + }, + { + "type": "UILabel", + "id": "onlineVersion", + "text": "Module Version Goes Here" + }, + { + "type": "UILabel", + "text": "${engine:menu#module-status}: " + }, + { + "type": "UILabel", + "id": "status", + "text": "Module Status Goes Here" + } + ], + "layoutInfo": { + "use-content-height": true, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP" + } + } + }, + { + "type": "ScrollableArea", + "content": { + "type": "UILabel", + "id": "description", + "text": "This is the description of a module. It can be quite long and even include new lines.\n\nLike this.\n\nI'm going to put in a lot more text to ensure the scrollable area fills up. Lorem ipsum and all that.\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + }, + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "simpleItems", + "offset": 8 + }, + "position-bottom": { + "target": "TOP", + "widget": "buttonRow", + "offset": 8 + } + } + }, + { + "type": "ColumnLayout", + "id": "buttonRow", + "columns": 2, + "horizontalSpacing": 8, + "verticalSpacing": 8, + "contents": [ + { + "type": "UIButton", + "id": "toggleActivation", + "text": "${engine:menu#activate-module}" + }, + { + "type": "UIButton", + "id": "download", + "text": "${engine:menu#download-module}" + } + ], + "layoutInfo": { + "height": 24, + "position-bottom": { + "target": "BOTTOM" + } + } + } + ] + } + ] + }, + { + "type": "UISpace", + "id": "close", + "layoutInfo": { + "width": 128, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": 48 + } + } + }, + { + "type": "RowLayout", + "id": "actionsRow", + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIButton", + "text": "${engine:menu#back}", + "id": "close" + }, + { + "type": "UIButton", + "text": "${engine:menu#continue}", + "id": "continue" + } + ], + "layoutInfo": { + "width": 400, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": -48, + "widget": "container" + } + } + } + ] + } +} diff --git a/engine/src/main/resources/assets/ui/newGameScreen.ui b/engine/src/main/resources/assets/ui/newGameScreen.ui new file mode 100644 index 00000000000..ac6578944fb --- /dev/null +++ b/engine/src/main/resources/assets/ui/newGameScreen.ui @@ -0,0 +1,142 @@ +{ + "type": "NewGameScreen", + "skin": "engine:mainMenu", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "UIImage", + "image": "engine:terasology", + "id": "title", + "layoutInfo": { + "width": 512, + "height": 128, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "offset": 48 + } + } + }, + { + "type": "UILabel", + "id": "subtitle", + "family": "title", + "text": "${engine:menu#new-game-title}", + "layoutInfo": { + "height": 48, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "title" + } + } + }, + { + "type": "UILabel", + "id": "gameTypeTitle", + "family": "subtitle", + "text": "${engine:menu#select-singleplayer-game-sub-title}", + "layoutInfo": { + "height": 30, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "subtitle" + } + } + }, + { + "type": "UIBox", + "id":"mainBox", + "content": { + "type": "ColumnLayout", + "columns": 1, + "verticalSpacing": 4, + "horizontalSpacing": 4, + "contents": [ + { + "type": "UILabel", + "text": "${engine:menu#game-name}" + }, + { + "type": "UIText", + "id": "gameName" + }, + { + "type": "UISpace", + "size": [ + 1, + 16 + ] + }, + { + "type": "UILabel", + "text": "${engine:menu#gameplay}" + }, + { + "type": "UIDropdownScrollable", + "id": "gameplay", + "layoutInfo": { + "relativeWidth": 0.7 + } + }, + { + "type": "UISpace", + "size": [ + 1, + 16 + ] + }, + { + "type": "UILabel", + "id": "gameplayDescription" + } + ] + }, + "layoutInfo": { + "width": 500, + "use-content-height": true, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "gameTypeTitle" + } + } + }, + { + "type": "RowLayout", + "id": "actionsRow", + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIButton", + "text": "${engine:menu#back}", + "id": "close" + }, + { + "type": "UIButton", + "text": "${engine:menu#advanced}", + "id": "advancedSetup" + }, + { + "type": "UIButton", + "text": "${engine:menu#start-game}", + "id": "play" + } + ], + "layoutInfo": { + "width": 400, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": -48, + "widget": "mainBox" + } + } + } + ] + } +} \ No newline at end of file diff --git a/engine/src/main/resources/assets/ui/startPlayingScreen.ui b/engine/src/main/resources/assets/ui/startPlayingScreen.ui new file mode 100644 index 00000000000..20298ec9760 --- /dev/null +++ b/engine/src/main/resources/assets/ui/startPlayingScreen.ui @@ -0,0 +1,121 @@ +{ + "type": "engine:StartPlayingScreen", + "skin": "engine:mainMenu", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "UIImage", + "image": "engine:terasology", + "id": "title", + "layoutInfo": { + "width": 512, + "height": 128, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "offset": 48 + } + } + }, + { + "type": "UILabel", + "id": "subtitle", + "family": "title", + "text": "${engine:menu#start-playing}", + "layoutInfo": { + "height": 48, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "title" + } + } + }, + { + "type": "UIBox", + "id": "container", + "layoutInfo": { + "width": 400, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "subtitle" + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 16 + } + } + }, + { + "type": "ColumnLayout", + "columns": 1, + "horizontalSpacing": 8, + "layoutInfo": { + "width": 384, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "widget": "container", + "offset": 8 + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 24 + } + }, + "contents": [ + { + "type": "UIImage", + "skin": "framed_image", + "id": "preview" + } + ] + }, + { + "type": "UISpace", + "id": "close", + "layoutInfo": { + "width": 128, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": 48 + } + } + }, + { + "type": "RowLayout", + "id": "actionsRow", + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIButton", + "text": "${engine:menu#back}", + "id": "close" + }, + { + "type": "UIButton", + "text": "${engine:menu#start-game}", + "id": "play" + } + ], + "layoutInfo": { + "width": 400, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": -48, + "widget": "container" + } + } + } + ] + } +} diff --git a/engine/src/main/resources/assets/ui/universeSetupScreen.ui b/engine/src/main/resources/assets/ui/universeSetupScreen.ui new file mode 100644 index 00000000000..a4b7b528a9c --- /dev/null +++ b/engine/src/main/resources/assets/ui/universeSetupScreen.ui @@ -0,0 +1,179 @@ +{ + "type": "UniverseSetupScreen", + "skin": "engine:mainMenu", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "UIImage", + "image": "engine:terasology", + "id": "title", + "layoutInfo": { + "width": 512, + "height": 128, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "offset": 48 + } + } + }, + { + "type": "UILabel", + "id": "subtitle", + "family": "title", + "text": "${engine:menu#universe-setup}", + "layoutInfo": { + "height": 48, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "title" + } + } + }, + { + "type": "UIBox", + "id":"mainBox", + "content": { + "type": "ColumnLayout", + "columns": 1, + "verticalSpacing": 4, + "horizontalSpacing": 4, + "contents": [ + { + "type": "UILabel", + "text": "Pick a world generator and add it. Then select and configure worlds from the freshly filled Worlds dropdown" + }, + { + "type": "UILabel", + "text": "${engine:menu#game-world-generators}:", + "family": "left-label" + }, + { + "type": "RowLayout", + "verticalSpacing": 4, + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIDropdownScrollable", + "id": "worldGenerators", + "layoutInfo": { + "relativeWidth": 0.50 + } + }, + { + "type": "UISpace", + "size": [ + 1, + 8 + ] + }, + { + "type": "UIButton", + "id": "addGenerator", + "text": "${engine:menu#add}", + "layoutInfo": { + "relativeWidth": 0.30 + } + }, + { + "type": "UISpace", + "size": [ + 1, + 8 + ] + } + ] + }, + { + "type": "UISpace", + "size": [ + 1, + 8 + ] + }, + { + "type": "UILabel", + "text": "${engine:menu#game-worlds}:", + "family": "left-label" + }, + { + "type": "RowLayout", + "verticalSpacing": 4, + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIDropdownScrollable", + "id": "worlds", + "layoutInfo": { + "relativeWidth": 0.50 + } + }, + { + "type": "UISpace", + "size": [ + 1, + 8 + ] + }, + { + "type": "UIButton", + "id": "worldConfig", + "text": "${engine:menu#config}", + "layoutInfo": { + "relativeWidth": 0.30 + } + }, + { + "type": "UISpace", + "size": [ + 1, + 8 + ] + } + ] + } + ] + }, + "layoutInfo": { + "width": 500, + "use-content-height": true, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 64, + "widget": "title" + } + } + }, + { + "type": "RowLayout", + "id": "actionsRow", + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIButton", + "text": "${engine:menu#back}", + "id": "close" + }, + { + "type": "UIButton", + "text": "${engine:menu#continue}", + "id": "continue" + } + ], + "layoutInfo": { + "width": 400, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": -48, + "widget": "mainBox" + } + } + } + ] + } +} \ No newline at end of file diff --git a/engine/src/main/resources/assets/ui/worldPreGenerationScreen.ui b/engine/src/main/resources/assets/ui/worldPreGenerationScreen.ui new file mode 100644 index 00000000000..ed54407a588 --- /dev/null +++ b/engine/src/main/resources/assets/ui/worldPreGenerationScreen.ui @@ -0,0 +1,231 @@ +{ + "type": "engine:WorldPreGenerationScreen", + "skin": "engine:mainMenu", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "UIImage", + "image": "engine:terasology", + "id": "title", + "layoutInfo": { + "width": 512, + "height": 128, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "offset": 48 + } + } + }, + { + "type": "UILabel", + "id": "subtitle", + "family": "title", + "text": "${engine:menu#world-pre-generation}", + "layoutInfo": { + "height": 48, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "title" + } + } + }, + { + "type": "UIBox", + "id": "container", + "layoutInfo": { + "width": 720, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "subtitle" + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 16 + } + } + }, + { + "type": "ColumnLayout", + "columns": 2, + "horizontalSpacing": 8, + "column-widths": [0.53, 0.47], + "layoutInfo": { + "width": 704, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "widget": "container", + "offset": 8 + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 24 + } + }, + "contents": [ + { + "type": "UIImage", + "skin": "framed_image", + "id": "preview" + }, + { + "type": "ColumnLayout", + "columns": 1, + "verticalSpacing": 4, + "contents": [ + { + "type": "UILabel", + "text": "Select a world in the drop-down to see it previewed. Click Re-Roll to randomly pick a new seed or Configure to manually tweak the world further" + }, + { + "type": "UILabel", + "text": "${engine:menu#time-progression-during-pre-generation}:", + "id": "timeLabel", + "family": "left-label", + "enabled": false + }, + { + "type": "UISlider", + "id": "timeSlider", + "minimum": 0.0, + "range": 9.0, + "increment": 1.0, + "precision": 0, + "value": 0.0, + "enabled": false, + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "timeLabel", + "offset": 0 + } + } + }, + { + "type": "UILabel", + "text": "${engine:menu#preview-zoom-factor}:", + "id": "zoomLabel", + "family": "left-label", + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "timeSlider", + "offset": 0 + } + } + }, + { + "type": "UISlider", + "id": "zoomSlider", + "minimum": 1.0, + "range": 7.0, + "increment": 1.0, + "precision": 0, + "layoutInfo": { + "use-content-height": true, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "zoomLabel" + } + } + }, + { + "type": "UILabel", + "text": "${engine:menu#game-worlds}:", + "id": "worldLabel", + "family": "left-label", + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "zoomSlider", + "offset": 0 + } + } + }, + { + "type": "UIDropdownScrollable", + "id": "worlds", + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "worldGenLabel", + "offset": 0 + } + } + }, + { + "type": "RowLayout", + "id": "worldGenActions", + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIButton", + "text": "${engine:menu#re-roll}", + "id": "reRoll" + }, + { + "type": "UIButton", + "text": "${engine:menu#config}", + "id": "config" + } + ] + } + ] + } + ] + }, + { + "type": "UISpace", + "id": "close", + "layoutInfo": { + "width": 128, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": 48 + } + } + }, + { + "type": "RowLayout", + "id": "actionsRow", + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIButton", + "text": "${engine:menu#back}", + "id": "close" + }, + { + "type": "UIButton", + "text": "${engine:menu#continue}", + "id": "continue" + } + ], + "layoutInfo": { + "width": 400, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": -48, + "widget": "container" + } + } + } + ] + } +} diff --git a/engine/src/main/resources/assets/ui/worldSetupScreen.ui b/engine/src/main/resources/assets/ui/worldSetupScreen.ui new file mode 100644 index 00000000000..92eb2c4e967 --- /dev/null +++ b/engine/src/main/resources/assets/ui/worldSetupScreen.ui @@ -0,0 +1,158 @@ +{ + "type": "engine:worldSetupScreen", + "skin": "engine:mainMenu", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "UIImage", + "image": "engine:terasology", + "id": "title", + "layoutInfo": { + "width": 512, + "height": 128, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "offset": 48 + } + } + }, + { + "type": "UILabel", + "id": "subtitle", + "family": "title", + "text": "${engine:menu#world-setup}", + "layoutInfo": { + "height": 48, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "widget": "title" + } + } + }, + { + "type": "UIBox", + "id": "container", + "layoutInfo": { + "width": 520, + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 16, + "widget": "subtitle" + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 16 + } + } + }, + { + "type": "ColumnLayout", + "columns": 1, + "verticalSpacing": 16, + "horizontalSpacing": 8, + "layoutInfo": { + "width": 504, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP", + "widget": "container", + "offset": 8 + }, + "position-bottom": { + "target": "TOP", + "widget": "close", + "offset": 24 + } + }, + "contents": [ + { + "type": "RelativeLayout", + "family": "description", + "contents": [ + { + "type": "RowLayout", + "id": "shape", + "layoutInfo": { + "use-content-height": true, + "position-horizontal-center": {}, + "position-top": { + "target": "TOP" + } + }, + "contents": [ + { + "type": "UILabel", + "text": "World Shape:", + "enabled": false + }, + { + "type": "UIDropdownScrollable", + "id": "worlds", + "enabled": false + } + ] + }, + { + "type": "ScrollableArea", + "content": { + "type": "propertyLayout", + "id": "properties", + "rowConstraints": "[min]" + }, + "layoutInfo": { + "position-horizontal-center": {}, + "position-top": { + "target": "BOTTOM", + "offset": 8, + "widget": "shape" + }, + "position-bottom": { + "target": "TOP", + "offset": 16, + "widget": "apply" + } + } + }, + { + "type": "RowLayout", + "horizontalSpacing": 4, + "id": "apply", + "layoutInfo": { + "position-horizontal-center": {}, + "height": 32, + "width": 260, + "position-top": { + "target": "BOTTOM", + "widget": "properties", + "offset": 16 + } + }, + "contents": [ + ] + } + ] + } + ] + }, + { + "type": "UIButton", + "text": "Done", + "id": "close", + "layoutInfo": { + "width": 128, + "height": 32, + "position-horizontal-center": {}, + "position-bottom": { + "target": "BOTTOM", + "offset": 48 + } + } + } + ] + } +}