/*
* Copyright 2015 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.logic.console.commands;
import com.google.common.collect.Ordering;
import org.terasology.assets.ResourceUrn;
import org.terasology.assets.management.AssetManager;
import org.terasology.config.Config;
import org.terasology.engine.GameEngine;
import org.terasology.engine.SimpleUri;
import org.terasology.engine.TerasologyConstants;
import org.terasology.engine.Time;
import org.terasology.engine.modes.StateLoading;
import org.terasology.engine.modes.StateMainMenu;
import org.terasology.engine.paths.PathManager;
import org.terasology.engine.subsystem.DisplayDevice;
import org.terasology.entitySystem.entity.EntityManager;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.entity.internal.EngineEntityManager;
import org.terasology.entitySystem.prefab.Prefab;
import org.terasology.entitySystem.prefab.PrefabManager;
import org.terasology.entitySystem.systems.BaseComponentSystem;
import org.terasology.entitySystem.systems.RegisterSystem;
import org.terasology.i18n.TranslationProject;
import org.terasology.i18n.TranslationSystem;
import org.terasology.logic.console.Console;
import org.terasology.logic.console.ConsoleColors;
import org.terasology.logic.console.commandSystem.ConsoleCommand;
import org.terasology.logic.console.commandSystem.annotations.Command;
import org.terasology.logic.console.commandSystem.annotations.CommandParam;
import org.terasology.logic.console.commandSystem.annotations.Sender;
import org.terasology.logic.console.suggesters.CommandNameSuggester;
import org.terasology.logic.console.suggesters.ScreenSuggester;
import org.terasology.logic.console.suggesters.SkinSuggester;
import org.terasology.logic.inventory.events.DropItemEvent;
import org.terasology.logic.location.LocationComponent;
import org.terasology.logic.permission.PermissionManager;
import org.terasology.math.Direction;
import org.terasology.math.geom.Quat4f;
import org.terasology.math.geom.Vector3f;
import org.terasology.naming.Name;
import org.terasology.network.ClientComponent;
import org.terasology.network.JoinStatus;
import org.terasology.network.NetworkMode;
import org.terasology.network.NetworkSystem;
import org.terasology.network.PingService;
import org.terasology.network.Server;
import org.terasology.persistence.WorldDumper;
import org.terasology.persistence.serializers.PrefabSerializer;
import org.terasology.registry.In;
import org.terasology.rendering.FontColor;
import org.terasology.rendering.nui.NUIManager;
import org.terasology.rendering.nui.asset.UIElement;
import org.terasology.rendering.nui.editor.layers.NUIEditorScreen;
import org.terasology.rendering.nui.editor.layers.NUISkinEditorScreen;
import org.terasology.rendering.nui.editor.systems.NUIEditorSystem;
import org.terasology.rendering.nui.editor.systems.NUISkinEditorSystem;
import org.terasology.rendering.nui.layers.mainMenu.MessagePopup;
import org.terasology.rendering.nui.layers.mainMenu.WaitPopup;
import org.terasology.rendering.nui.skin.UISkin;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.utilities.Assets;
import org.terasology.world.block.BlockManager;
import org.terasology.world.block.BlockUri;
import org.terasology.world.block.family.BlockFamily;
import org.terasology.world.block.items.BlockItemFactory;
import org.terasology.world.block.loader.BlockFamilyDefinition;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
*/
@RegisterSystem
public class CoreCommands extends BaseComponentSystem {
@In
private EntityManager entityManager;
@In
private WorldRenderer worldRenderer;
@In
private PrefabManager prefabManager;
@In
private BlockManager blockManager;
@In
private Console console;
@In
private Time time;
@In
private GameEngine gameEngine;
@In
private NetworkSystem networkSystem;
@In
private DisplayDevice displayDevice;
@In
private NUIManager nuiManager;
@In
private NUIEditorSystem nuiEditorSystem;
@In
private NUISkinEditorSystem nuiSkinEditorSystem;
@In
private AssetManager assetManager;
@In
private TranslationSystem translationSystem;
@In
private Config config;
/**
* Search commands/prefabs/assets with matching name, description, help text, usage or required permission
* @param searched String which is used to search for match
* @return String containing result of search
*/
@Command(shortDescription = "Search commands/prefabs/assets",
helpText = "Displays commands, prefabs, and assets with matching name, description, "
+ "help text, usage or required permission")
public String search(@CommandParam("searched") String searched) {
String searchLowercase = searched.toLowerCase();
List<String> commands = findCommandMatches(searchLowercase);
List<String> prefabs = findPrefabMatches(searchLowercase);
List<String> blocks = findBlockMatches(searchLowercase);
// String containing numbers of commands, prefabs and block that match searched string
String result = "Found " + commands.size() + " command matches, " + prefabs.size() +
" prefab matches and " + blocks.size() + " block matches when searching for '" + searched + "'.";
// iterate through commands adding them to result
if (commands.size() > 0) {
result += "\nCommands:";
result = commands.stream().reduce(result, (t, u) -> t + "\n " + u);
}
// iterate through prefabs adding them to result
if (prefabs.size() > 0) {
result += "\nPrefabs:";
result = prefabs.stream().reduce(result, (t, u) -> t + "\n " + u);
}
// iterate through blocks adding them to result
if (blocks.size() > 0) {
result += "\nBlocks:";
result = blocks.stream().reduce(result, (t, u) -> t + "\n " + u);
}
return result;
}
/**
* List commands that match searched string
* @param searchLowercase searched string lowercase
* @return List of commands that match searched string
*/
private List<String> findCommandMatches(String searchLowercase) {
return console.getCommands().stream().filter(command -> matchesSearch(searchLowercase, command))
.map(ConsoleCommand::getUsage).collect(Collectors.toList());
}
/**
* Determine if command is matching one of criteria
* @param searchLowercase searched string
* @param command ConsoleCommand to check if matches searched string
* @return boolean containing true if command matches searched string else false
*/
private static boolean matchesSearch(String searchLowercase, ConsoleCommand command) {
return command.getName().toLowerCase().contains(searchLowercase)
|| command.getDescription().toLowerCase().contains(searchLowercase)
|| command.getHelpText().toLowerCase().contains(searchLowercase)
|| command.getUsage().toLowerCase().contains(searchLowercase)
|| command.getRequiredPermission().toLowerCase().contains(searchLowercase);
}
/**
* List prefabs that match searched string
* @param searchLowercase searched string
* @return List of prefabs that match searched string
*/
private List<String> findPrefabMatches(String searchLowercase) {
return StreamSupport.stream(prefabManager.listPrefabs().spliterator(), false)
.filter(prefab -> matchesSearch(searchLowercase, prefab))
.map(prefab -> prefab.getUrn().toString()).collect(Collectors.toList());
}
/**
* Determine if prefab is matching one of criteria
* @param searchLowercase searched String
* @param prefab Prefab to check if matches searched string
* @return boolean containing true if prefab matches searched string else false
*/
private static boolean matchesSearch(String searchLowercase, Prefab prefab) {
return prefab.getName().toLowerCase().contains(searchLowercase)
|| prefab.getUrn().toString().toLowerCase().contains(searchLowercase);
}
/**
* List blocks that match searched string
* @param searchLowercase searched string
* @return List of blocks that match searched string
*/
private List<String> findBlockMatches(String searchLowercase) {
return assetManager.getAvailableAssets(BlockFamilyDefinition.class)
.stream().<Optional<BlockFamilyDefinition>>map(urn -> assetManager.getAsset(urn, BlockFamilyDefinition.class))
.filter(def -> def.isPresent() && def.get().isLoadable() && matchesSearch(searchLowercase, def.get()))
.map(r -> new BlockUri(r.get().getUrn()).toString()).collect(Collectors.toList());
}
/**
* Determine if block family matches one of criteria
* @param searchLowercase searched string
* @param def BlockFamilyDefinition to be checked
* @return boolean containing true if blockFamilyDefinition matches searched string else false
*/
private static boolean matchesSearch(String searchLowercase, BlockFamilyDefinition def) {
return def.getUrn().toString().toLowerCase().contains(searchLowercase);
}
/**
* Time dilation slows down the passage of time by affecting how the main game loop runs,
* with the goal being to handle high-latency situations by spreading out processing over a longer amount of time
* @param rate float time dilation
*/
@Command(shortDescription = "Alter the rate of time")
public void setTimeDilation(@CommandParam("dilation") float rate) {
time.setGameTimeDilation(rate);
}
/**
* Change the UI language
* @param langTag String containing language code to change
* @return String containing language or if not recognized error message
*/
@Command(shortDescription = "Changes the UI language")
public String setLanguage(@CommandParam("language-tag") String langTag) {
Locale locale = Locale.forLanguageTag(langTag);
TranslationProject proj = translationSystem.getProject(new SimpleUri("engine:menu"));
// Try if language exists
if (proj.getAvailableLocales().contains(locale)) {
config.getSystem().setLocale(locale);
nuiManager.invalidate();
String nat = translationSystem.translate("${engine:menu#this-language-native}", locale);
String eng = translationSystem.translate("${engine:menu#this-language-English}", locale);
return String.format("Language set to %s (%s)", nat, eng);
} else {
return "Unrecognized locale! Try one of: " + proj.getAvailableLocales();
}
}
/**
* Shows a ui screen
* @param uri String containing ui screen name
* @return String containing Success if UI was change or Not found if screen is missing
*/
@Command(shortDescription = "Shows a ui screen", helpText = "Can be used for debugging/testing, example: \"showScreen migTestScreen\"")
public String showScreen(@CommandParam(value = "uri", suggester = ScreenSuggester.class) String uri) {
return nuiManager.pushScreen(uri) != null ? "Success" : "Not found";
}
/**
* Reloads ui screen
* @param ui String containing ui screen name
* @return String containing Success if UI was reloaded or No unique resource found if more screens were found
*/
@Command(shortDescription = "Reloads a ui screen")
public String reloadScreen(@CommandParam("ui") String ui) {
Set<ResourceUrn> urns = assetManager.resolve(ui, UIElement.class);
if (urns.size() == 1) {
ResourceUrn urn = urns.iterator().next();
boolean wasOpen = nuiManager.isOpen(urn);
if (wasOpen) {
nuiManager.closeScreen(urn);
}
if (wasOpen) {
nuiManager.pushScreen(urn);
}
return "Success";
} else {
return "No unique resource found";
}
}
/**
* Opens the NUI editor for a ui screen
* @param uri String containing ui screen name
* @return String containing final message
*/
@Command(shortDescription = "Opens the NUI editor for a ui screen", requiredPermission = PermissionManager.NO_PERMISSION)
public String editScreen(@CommandParam(value = "uri", suggester = ScreenSuggester.class) String uri) {
if (!nuiEditorSystem.isEditorActive()) {
nuiEditorSystem.toggleEditor();
}
Set<ResourceUrn> urns = assetManager.resolve(uri, UIElement.class);
switch (urns.size()) {
case 0:
return String.format("No asset found for screen '%s'", uri);
case 1:
ResourceUrn urn = urns.iterator().next();
((NUIEditorScreen) nuiManager.getScreen(NUIEditorScreen.ASSET_URI)).selectAsset(urn);
return "Success";
default:
return String.format("Multiple matches for screen '%s': {%s}", uri, Arrays.toString(urns.toArray()));
}
}
/**
* Opens the NUI editor for a ui skin
* @param uri String containing name of ui skin
* @return String containing final message
*/
@Command(shortDescription = "Opens the NUI editor for a ui skin", requiredPermission = PermissionManager.NO_PERMISSION)
public String editSkin(@CommandParam(value = "uri", suggester = SkinSuggester.class) String uri) {
if (!nuiSkinEditorSystem.isEditorActive()) {
nuiSkinEditorSystem.toggleEditor();
}
Set<ResourceUrn> urns = assetManager.resolve(uri, UISkin.class);
switch (urns.size()) {
case 0:
return String.format("No asset found for screen '%s'", uri);
case 1:
ResourceUrn urn = urns.iterator().next();
((NUISkinEditorScreen) nuiManager.getScreen(NUISkinEditorScreen.ASSET_URI)).selectAsset(urn);
return "Success";
default:
return String.format("Multiple matches for screen '%s': {%s}", uri, Arrays.toString(urns.toArray()));
}
}
/**
* Switches to fullscreen or to windowed mode
* @return String containing final message
*/
@Command(shortDescription = "Toggles Fullscreen Mode", requiredPermission = PermissionManager.NO_PERMISSION)
public String fullscreen() {
displayDevice.setFullscreen(!displayDevice.isFullscreen());
if (displayDevice.isFullscreen()) {
return "Switched to fullscreen mode";
} else {
return "Switched to windowed mode";
}
}
/**
* Removes all entities of the given prefab
* @param prefabName String containing prefab name
*/
@Command(shortDescription = "Removes all entities of the given prefab", runOnServer = true)
public void destroyEntitiesUsingPrefab(@CommandParam("prefabName") String prefabName) {
Prefab prefab = entityManager.getPrefabManager().getPrefab(prefabName);
if (prefab != null) {
for (EntityRef entity : entityManager.getAllEntities()) {
if (prefab.equals(entity.getParentPrefab())) {
entity.destroy();
}
}
}
}
/**
* Triggers a graceful shutdown of the game after the current frame, attempting to dispose all game resources
*/
@Command(shortDescription = "Exits the game", requiredPermission = PermissionManager.NO_PERMISSION)
public void exit() {
gameEngine.shutdown();
}
/**
* Join a game
* @param address String containing address of game server
* @param portParam Integer containing game server port
*/
@Command(shortDescription = "Join a game", requiredPermission = PermissionManager.NO_PERMISSION)
public void join(@CommandParam("address") final String address, @CommandParam(value = "port", required = false) Integer portParam) {
final int port = portParam != null ? portParam : TerasologyConstants.DEFAULT_PORT;
Callable<JoinStatus> operation = () -> networkSystem.join(address, port);
final WaitPopup<JoinStatus> popup = nuiManager.pushScreen(WaitPopup.ASSET_URI, WaitPopup.class);
popup.setMessage("Join Game", "Connecting to '" + address + ":" + port + "' - please wait ...");
popup.onSuccess(result -> {
if (result.getStatus() != JoinStatus.Status.FAILED) {
gameEngine.changeState(new StateLoading(result));
} else {
MessagePopup screen = nuiManager.pushScreen(MessagePopup.ASSET_URI, MessagePopup.class);
screen.setMessage("Failed to Join", "Could not connect to server - " + result.getErrorMessage());
}
});
popup.startOperation(operation, true);
}
/**
* Leaves the current game and returns to main menu
* @return String containing final message
*/
@Command(shortDescription = "Leaves the current game and returns to main menu",
requiredPermission = PermissionManager.NO_PERMISSION)
public String leave() {
if (networkSystem.getMode() != NetworkMode.NONE) {
gameEngine.changeState(new StateMainMenu());
return "Leaving..";
} else {
return "Not connected";
}
}
/**
* Writes out information on all entities to a text file for debugging
* @throws IOException thrown when error with saving file occures
*/
@Command(shortDescription = "Writes out information on all entities to a text file for debugging",
helpText = "Writes entity information out into a file named \"entityDump.txt\".")
public void dumpEntities() throws IOException {
EngineEntityManager engineEntityManager = (EngineEntityManager) entityManager;
PrefabSerializer prefabSerializer = new PrefabSerializer(engineEntityManager.getComponentLibrary(), engineEntityManager.getTypeSerializerLibrary());
WorldDumper worldDumper = new WorldDumper(engineEntityManager, prefabSerializer);
worldDumper.save(PathManager.getInstance().getHomePath().resolve("entityDump.txt"));
}
/**
* Spawns an instance of a prefab in the world
* @param sender Sender of command
* @param prefabName String containing prefab name
* @return String containing final message
*/
@Command(shortDescription = "Spawns an instance of a prefab in the world", runOnServer = true, requiredPermission = PermissionManager.CHEAT_PERMISSION)
public String spawnPrefab(@Sender EntityRef sender, @CommandParam("prefabId") String prefabName) {
ClientComponent clientComponent = sender.getComponent(ClientComponent.class);
LocationComponent characterLocation = clientComponent.character.getComponent(LocationComponent.class);
Vector3f spawnPos = characterLocation.getWorldPosition();
Vector3f offset = new Vector3f(characterLocation.getWorldDirection());
offset.scale(2);
spawnPos.add(offset);
Vector3f dir = new Vector3f(characterLocation.getWorldDirection());
dir.y = 0;
if (dir.lengthSquared() > 0.001f) {
dir.normalize();
} else {
dir.set(Direction.FORWARD.getVector3f());
}
Quat4f rotation = Quat4f.shortestArcQuat(Direction.FORWARD.getVector3f(), dir);
Optional<Prefab> prefab = Assets.getPrefab(prefabName);
if (prefab.isPresent() && prefab.get().getComponent(LocationComponent.class) != null) {
entityManager.create(prefab.get(), spawnPos, rotation);
return "Done";
} else if (!prefab.isPresent()) {
return "Unknown prefab";
} else {
return "Prefab cannot be spawned (no location component)";
}
}
/**
* Spawns a block in front of the player
* @param sender Sender of command
* @param blockName String containing name of block to spawn
* @return String containg final message
*/
@Command(shortDescription = "Spawns a block in front of the player", helpText = "Spawns the specified block as a " +
"item in front of the player. You can simply pick it up.", runOnServer = true, requiredPermission = PermissionManager.CHEAT_PERMISSION)
public String spawnBlock(@Sender EntityRef sender, @CommandParam("blockName") String blockName) {
ClientComponent clientComponent = sender.getComponent(ClientComponent.class);
LocationComponent characterLocation = clientComponent.character.getComponent(LocationComponent.class);
Vector3f spawnPos = characterLocation.getWorldPosition();
Vector3f offset = characterLocation.getWorldDirection();
offset.scale(3);
spawnPos.add(offset);
BlockFamily block = blockManager.getBlockFamily(blockName);
if (block == null) {
return "";
}
BlockItemFactory blockItemFactory = new BlockItemFactory(entityManager);
EntityRef blockItem = blockItemFactory.newInstance(block);
blockItem.send(new DropItemEvent(spawnPos));
return "Spawned block.";
}
/**
* Your ping to the server
* @param sender Sender of command
* @return String containing ping or error message
*/
@Command(shortDescription = "Your ping to the server", helpText = "The time it takes the packet " +
"to reach the server and back", requiredPermission = PermissionManager.NO_PERMISSION)
public String ping(@Sender EntityRef sender) {
Server server = networkSystem.getServer();
if (server == null) {
//TODO: i18n
return "Please make sure you are connected to an online server (singleplayer doesn't count)";
}
String[] remoteAddress = server.getRemoteAddress().split("-");
String address = remoteAddress[1];
int port = Integer.valueOf(remoteAddress[2]);
try {
PingService pingService = new PingService(address, port);
long delay = pingService.call();
return String.format("%d ms", delay);
} catch (UnknownHostException e) {
return String.format("Error: Unknown host \"%s\" at %s:%s -- %s", remoteAddress[0], remoteAddress[1], remoteAddress[2], e);
} catch (IOException e) {
return String.format("Error: Failed to ping server \"%s\" at %s:%s -- %s", remoteAddress[0], remoteAddress[1], remoteAddress[2], e);
}
}
/**
* Prints out short descriptions for all available commands, or a longer help text if a command is provided.
* @param commandName String containing command for which will be displayed help
* @return String containing short description of all commands or longer help text if command is provided
*/
@Command(shortDescription = "Prints out short descriptions for all available commands, or a longer help text if a command is provided.",
requiredPermission = PermissionManager.NO_PERMISSION)
public String help(@CommandParam(value = "command", required = false, suggester = CommandNameSuggester.class) Name commandName) {
if (commandName == null) {
StringBuilder msg = new StringBuilder();
// Get all commands, with appropriate sorting
List<ConsoleCommand> commands = Ordering.natural().immutableSortedCopy(console.getCommands());
for (ConsoleCommand cmd : commands) {
if (!msg.toString().isEmpty()) {
msg.append(Console.NEW_LINE);
}
msg.append(FontColor.getColored(cmd.getUsage(), ConsoleColors.COMMAND));
msg.append(" - ");
msg.append(cmd.getDescription());
}
return msg.toString();
} else {
ConsoleCommand cmd = console.getCommand(commandName);
if (cmd == null) {
return "No help available for command '" + commandName + "'. Unknown command.";
} else {
StringBuilder msg = new StringBuilder();
msg.append("=====================================================================================================================");
msg.append(Console.NEW_LINE);
msg.append(cmd.getUsage());
msg.append(Console.NEW_LINE);
msg.append("=====================================================================================================================");
msg.append(Console.NEW_LINE);
if (!cmd.getHelpText().isEmpty()) {
msg.append(cmd.getHelpText());
msg.append(Console.NEW_LINE);
msg.append("=====================================================================================================================");
msg.append(Console.NEW_LINE);
} else if (!cmd.getDescription().isEmpty()) {
msg.append(cmd.getDescription());
msg.append(Console.NEW_LINE);
msg.append("=====================================================================================================================");
msg.append(Console.NEW_LINE);
}
return msg.toString();
}
}
}
/**
* Clears the console window of previous messages.
*/
@Command(shortDescription = "Clears the console window of previous messages.", requiredPermission = PermissionManager.NO_PERMISSION)
public void clear() {
console.clear();
}
}