Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GUI screen to name a recording #3548

Merged
merged 7 commits into from
Nov 18, 2018
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
});

Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions engine/src/main/resources/assets/i18n/menu.lang
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions engine/src/main/resources/assets/i18n/menu_en.lang
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions engine/src/main/resources/assets/i18n/menu_fr.lang
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down