diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/NameRecordingScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/NameRecordingScreen.java new file mode 100644 index 00000000000..cab18d9cf5c --- /dev/null +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/NameRecordingScreen.java @@ -0,0 +1,201 @@ +/* + * 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.codehaus.plexus.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.assets.ResourceUrn; +import org.terasology.config.Config; +import org.terasology.engine.GameEngine; +import org.terasology.engine.modes.StateLoading; +import org.terasology.engine.paths.PathManager; +import org.terasology.game.GameManifest; +import org.terasology.i18n.TranslationSystem; +import org.terasology.network.NetworkMode; +import org.terasology.recording.RecordAndReplayUtils; +import org.terasology.registry.CoreRegistry; +import org.terasology.registry.In; +import org.terasology.rendering.nui.CoreScreenLayer; +import org.terasology.rendering.nui.animation.MenuAnimationSystems; +import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameInfo; +import org.terasology.rendering.nui.widgets.UIButton; +import org.terasology.rendering.nui.widgets.UILabel; +import org.terasology.rendering.nui.widgets.UIText; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +/** + * Screen for setting the name of and ultimately loading a recording. + */ +public class NameRecordingScreen extends CoreScreenLayer { + public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:nameRecordingScreen!instance"); + + private static final Logger logger = LoggerFactory.getLogger(NameRecordingScreen.class); + + @In + protected Config config; + + @In + private TranslationSystem translationSystem; + + private GameInfo gameInfo; + + private RecordAndReplayUtils recordAndReplayUtils; + + // widgets + private UILabel description; + private UIText nameInput; + private UIButton enter; + private UIButton cancel; + + @Override + public void initialise() { + setAnimationSystem(MenuAnimationSystems.createDefaultSwipeAnimation()); + + initWidgets(); + + enter.subscribe(button -> enterPressed()); + cancel.subscribe(button -> cancelPressed()); + } + + @Override + public void onScreenOpened() { + // resets the description from any earlier error messages, in case the user re-opens the screen. + description.setText(translationSystem.translate("${engine:menu#name-recording-description}")); + } + + /** + * Sets the values of all widget references. + */ + private void initWidgets() { + description = find("description", UILabel.class); + nameInput = find("nameInput", UIText.class); + enter = find("enterButton", UIButton.class); + cancel = find("cancelButton", UIButton.class); + } + + /** + * Activates upon pressing the enter key. + */ + private void enterPressed() { + if (!isNameValid(nameInput.getText())) { + description.setText(translationSystem.translate("${engine:menu#name-recording-error-invalid}")); + return; + } + if (doesRecordingExist(nameInput.getText())) { + description.setText(translationSystem.translate("${engine:menu#name-recording-error-duplicate}")); + return; + } + + loadGame(nameInput.getText()); + } + + /** + * Activates upon pressing the cancel key. + */ + private void cancelPressed() { + triggerBackAnimation(); + } + + /** + * Last step of the recording setup process. Copies the save files from the selected game, transplants them into the 'recordings' folder, and renames the map files + * to match the provided recording name. Then launches the game loading state. + * + * @param newTitle The title of the new recording. + */ + private void loadGame(String newTitle) { + try { + final GameManifest manifest = gameInfo.getManifest(); + + copySaveDirectoryToRecordingLibrary(manifest.getTitle(), newTitle); + recordAndReplayUtils.setGameTitle(newTitle); + config.getWorldGeneration().setDefaultSeed(manifest.getSeed()); + config.getWorldGeneration().setWorldTitle(newTitle); + CoreRegistry.get(GameEngine.class).changeState(new StateLoading(manifest, 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()); + } + } + + /** + * Copies the selected save files to a new recording directory. + * + * @param oldTitle The name of the original save directory. + * @param newTitle The name of the new recording directory. + */ + private void copySaveDirectoryToRecordingLibrary(String oldTitle, String newTitle) { + File saveDirectory = new File(PathManager.getInstance().getSavePath(oldTitle).toString()); + Path destinationPath = PathManager.getInstance().getRecordingPath(newTitle); + File destDirectory = new File(destinationPath.toString()); + try { + FileUtils.copyDirectoryStructure(saveDirectory, destDirectory); + rewriteManifestTitle(destinationPath, newTitle); + } catch (Exception e) { + logger.error("Error trying to copy the save directory:", e); + } + } + + /** + * Rewrites the title of the save game manifest to match the new directory title. + * + * @param destinationPath The path of the new recording files. + * @param newTitle The new name for the recording manifest. + * @throws IOException + */ + private void rewriteManifestTitle(Path destinationPath, String newTitle) throws IOException { + // simply grabs the manifest, changes it, and saves again. + GameManifest manifest = GameManifest.load(destinationPath.resolve(GameManifest.DEFAULT_FILE_NAME)); + manifest.setTitle(newTitle); + GameManifest.save(destinationPath.resolve(GameManifest.DEFAULT_FILE_NAME), manifest); + } + + /** + * Tests if the provided string is valid for a game name. + * + * @param name The provided name string. + * @return true if name is valid, false otherwise. + */ + private boolean isNameValid(String name) { + Path destinationPath = PathManager.getInstance().getRecordingPath(name); + + // invalid characters are filtered from paths, so if the file name is made up of entirely invalid characters, the path will have a blank file name. + // also acts as a check for blank input. + return !destinationPath.equals(PathManager.getInstance().getRecordingPath("")); + } + + /** + * Tests if there is an existing recording with the provided name string. + * + * @param name The provided name string. + * @return true if recording exists, false otherwise. + */ + private boolean doesRecordingExist(String name) { + Path destinationPath = PathManager.getInstance().getRecordingPath(name); + return FileUtils.fileExists(destinationPath.toString()); + } + + public void setRecordAndReplayUtils(RecordAndReplayUtils recordAndReplayUtils) { + this.recordAndReplayUtils = recordAndReplayUtils; + } + + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + } +} diff --git a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/RecordScreen.java b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/RecordScreen.java index 201171433eb..f396929d6ce 100644 --- a/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/RecordScreen.java +++ b/engine/src/main/java/org/terasology/rendering/nui/layers/mainMenu/RecordScreen.java @@ -15,26 +15,19 @@ */ package org.terasology.rendering.nui.layers.mainMenu; -import org.codehaus.plexus.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.assets.ResourceUrn; -import org.terasology.engine.GameEngine; -import org.terasology.engine.modes.StateLoading; import org.terasology.engine.paths.PathManager; -import org.terasology.game.GameManifest; -import org.terasology.network.NetworkMode; import org.terasology.recording.RecordAndReplayCurrentStatus; import org.terasology.recording.RecordAndReplayStatus; import org.terasology.recording.RecordAndReplayUtils; -import org.terasology.registry.CoreRegistry; import org.terasology.registry.In; +import org.terasology.rendering.nui.animation.MenuAnimationSystems; import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameInfo; import org.terasology.rendering.nui.layers.mainMenu.savedGames.GameProvider; import org.terasology.rendering.nui.widgets.UIButton; -import java.io.File; -import java.nio.file.Path; import java.util.Objects; import java.util.stream.Stream; @@ -56,26 +49,29 @@ public class RecordScreen extends SelectionScreen { private UIButton load; private UIButton close; - @Override public void initialise() { + setAnimationSystem(MenuAnimationSystems.createDefaultSwipeAnimation()); + initWidgets(); if (isValidScreen()) { initSaveGamePathWidget(PathManager.getInstance().getSavesPath()); + NameRecordingScreen nameRecordingScreen = getManager().createScreen(NameRecordingScreen.ASSET_URI, NameRecordingScreen.class); + getGameInfos().subscribeSelection((widget, item) -> { load.setEnabled(item != null); updateDescription(item); }); - getGameInfos().subscribe((widget, item) -> loadGame(item)); + getGameInfos().subscribe((widget, item) -> launchNamingScreen(nameRecordingScreen, item)); load.subscribe(button -> { final GameInfo gameInfo = getGameInfos().getSelection(); if (gameInfo != null) { - loadGame(gameInfo); + launchNamingScreen(nameRecordingScreen, gameInfo); } }); @@ -92,7 +88,8 @@ public void onOpened() { refreshGameInfoList(GameProvider.getSavedGames()); } else { final MessagePopup popup = getManager().createScreen(MessagePopup.ASSET_URI, MessagePopup.class); - popup.setMessage(translationSystem.translate("${engine:menu#game-details-errors-message-title}"), translationSystem.translate("${engine:menu#game-details-errors-message-body}")); + popup.setMessage(translationSystem.translate("${engine:menu#game-details-errors-message-title}"), + translationSystem.translate("${engine:menu#game-details-errors-message-body}")); popup.subscribeButton(e -> triggerBackAnimation()); getManager().pushScreen(popup); // disable child widgets @@ -107,29 +104,16 @@ protected void initWidgets() { close = find("close", UIButton.class); } - private void loadGame(GameInfo item) { - try { - final GameManifest manifest = item.getManifest(); - copySaveDirectoryToRecordingLibrary(manifest.getTitle()); - recordAndReplayUtils.setGameTitle(manifest.getTitle()); - config.getWorldGeneration().setDefaultSeed(manifest.getSeed()); - config.getWorldGeneration().setWorldTitle(manifest.getTitle()); - CoreRegistry.get(GameEngine.class).changeState(new StateLoading(manifest, 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()); - } - } - - private void copySaveDirectoryToRecordingLibrary(String gameTitle) { - File saveDirectory = new File(PathManager.getInstance().getSavePath(gameTitle).toString()); - Path destinationPath = PathManager.getInstance().getRecordingPath(gameTitle); - File destDirectory = new File(destinationPath.toString()); - try { - FileUtils.copyDirectoryStructure(saveDirectory, destDirectory); - } catch (Exception e) { - logger.error("Error trying to copy the save directory:", e); - } + /** + * Launches {@link NameRecordingScreen} with the info of the game selected in this screen. + * + * @param nameRecordingScreen The instance of the screen to launch + * @param info The info of the selected game. + */ + private void launchNamingScreen(NameRecordingScreen nameRecordingScreen, GameInfo info) { + nameRecordingScreen.setGameInfo(info); + nameRecordingScreen.setRecordAndReplayUtils(recordAndReplayUtils); + triggerForwardAnimation(nameRecordingScreen); } void setRecordAndReplayUtils(RecordAndReplayUtils recordAndReplayUtils) { @@ -139,8 +123,8 @@ void setRecordAndReplayUtils(RecordAndReplayUtils recordAndReplayUtils) { @Override protected boolean isValidScreen() { if (Stream.of(load, close) - .anyMatch(Objects::isNull) || - !super.isValidScreen()) { + .anyMatch(Objects::isNull) + || !super.isValidScreen()) { logger.error("Can't initialize screen correctly. At least one widget was missed!"); return false; } diff --git a/engine/src/main/resources/assets/i18n/menu.lang b/engine/src/main/resources/assets/i18n/menu.lang index 4cc30ea0021..24807ae501c 100644 --- a/engine/src/main/resources/assets/i18n/menu.lang +++ b/engine/src/main/resources/assets/i18n/menu.lang @@ -214,6 +214,10 @@ "light-shafts": "light-shafts", "listed-servers": "listed-servers", "load-game": "load-game", + "name-recording-title": "name-recording-title", + "name-recording-description": "name-recording-description", + "name-recording-error-duplicate": "name-recording-error-duplicate", + "name-recording-error-invalid": "name-recording-error-invalid", "menu-animations": "menu-animations", "missing-name-message": "missing-name-message", "module-details-title": "module-details-title", diff --git a/engine/src/main/resources/assets/i18n/menu_en.lang b/engine/src/main/resources/assets/i18n/menu_en.lang index 40317a0c628..b5460f3d1e8 100644 --- a/engine/src/main/resources/assets/i18n/menu_en.lang +++ b/engine/src/main/resources/assets/i18n/menu_en.lang @@ -217,6 +217,10 @@ "light-shafts": "Light Shafts", "listed-servers": "Listed", "load-game": "Load", + "name-recording-title": "Name Recording", + "name-recording-description": "Enter a unique name for this recording.", + "name-recording-error-duplicate": "A recording with this name already exists! Enter a different name.", + "name-recording-error-invalid": "This recording name is invalid! Enter a different name.", "menu-animations": "Animated Menus", "missing-name-message": "You must enter a name", "module-details-title": "Module Details", diff --git a/engine/src/main/resources/assets/i18n/menu_fr.lang b/engine/src/main/resources/assets/i18n/menu_fr.lang index e4572c7f6a3..51c1b3ca880 100644 --- a/engine/src/main/resources/assets/i18n/menu_fr.lang +++ b/engine/src/main/resources/assets/i18n/menu_fr.lang @@ -142,6 +142,10 @@ "mouse-sensitivity": "Sensibilité de la souris", "movement-dead-zone": "Mouvement dans l'axe de la zone morte", "music-volume": "Volume de la musique", + "name-recording-title": "Nommez l'enregistrement", + "name-recording-description": "Entrez un nom unique pour cet enregistrement.", + "name-recording-error-duplicate": "Un enregistrement avec ce nom existe déjà ! Entrez un nom différent.", + "name-recording-error-invalid": "Ce nom de l'enregistrement est erroné ! Entrez un nom différent.", "new-binding": "Nouveau racourcie", "next-toolbar-item": "Prochaine barre d'outils", "none": "Aucun", diff --git a/engine/src/main/resources/assets/ui/menu/nameRecordingScreen.ui b/engine/src/main/resources/assets/ui/menu/nameRecordingScreen.ui new file mode 100644 index 00000000000..0e3780373c9 --- /dev/null +++ b/engine/src/main/resources/assets/ui/menu/nameRecordingScreen.ui @@ -0,0 +1,84 @@ +{ + "type": "nameRecordingScreen", + "skin": "engine:mainMenu", + "contents": { + "type": "relativeLayout", + "contents": [ + { + "type": "UILabel", + "id": "title", + "family": "title", + "text": "${engine:menu#name-recording-title}", + "layoutInfo": { + "height": 48, + "position-horizontal-center": {}, + "position-bottom": { + "target": "TOP", + "widget": "box", + "offset": 48 + } + } + }, + { + "type": "UIBox", + "id": "box", + "layoutInfo": { + "width": 500, + "use-content-height": true, + "position-horizontal-center": {}, + "position-vertical-center": {} + }, + "content": { + "type": "ColumnLayout", + "columns": 1, + "verticalSpacing": 8, + "contents": [ + { + "type": "UISpace", + "size": [ + 1, + 16 + ] + }, + { + "type": "UILabel", + "id": "description", + "text": "${engine:menu#name-recording-description}" + }, + { + "type": "UISpace", + "size": [ + 1, + 16 + ] + }, + { + "type": "UIText", + "id": "nameInput" + }, + { + "type": "RowLayout", + "horizontalSpacing": 4, + "contents": [ + { + "type": "UIButton", + "id": "enterButton", + "text": "${engine:menu#dialog-ok}" + }, + { + "type": "UIButton", + "id": "cancelButton", + "text": "${engine:menu#dialog-cancel}" + } + ], + "layoutInfo": { + "width": 200, + "position-right": {} + } + } + ] + } + } + ] + } +}