/*
* 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.world.block.entity;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import org.terasology.assets.ResourceUrn;
import org.terasology.assets.management.AssetManager;
import org.terasology.entitySystem.entity.EntityManager;
import org.terasology.entitySystem.entity.EntityRef;
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.input.cameraTarget.TargetSystem;
import org.terasology.logic.characters.GazeAuthoritySystem;
import org.terasology.logic.console.Console;
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.inventory.events.GiveItemEvent;
import org.terasology.logic.location.LocationComponent;
import org.terasology.logic.permission.PermissionManager;
import org.terasology.logic.players.LocalPlayer;
import org.terasology.network.ClientComponent;
import org.terasology.physics.Physics;
import org.terasology.registry.In;
import org.terasology.registry.Share;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.utilities.Assets;
import org.terasology.world.BlockEntityRegistry;
import org.terasology.world.WorldProvider;
import org.terasology.world.block.Block;
import org.terasology.world.block.BlockComponent;
import org.terasology.world.block.BlockExplorer;
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 org.terasology.world.block.shapes.BlockShape;
import org.terasology.world.internal.WorldProviderCoreImpl;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Contains a series of handy game console commands associated with blocks.
*/
@RegisterSystem
@Share(BlockCommands.class)
public class BlockCommands extends BaseComponentSystem {
private TargetSystem targetSystem;
// TODO: Remove once camera is handled better
@In
private WorldRenderer renderer;
@In
private AssetManager assetManager;
@In
private BlockManager blockManager;
@In
private WorldProvider world;
@In
private PrefabManager prefabManager;
@In
private LocalPlayer localPlayer;
@In
private EntityManager entityManager;
@In
private LocalPlayer player;
@In
private Physics physics;
@In
private BlockEntityRegistry blockRegistry;
@In
private WorldProviderCoreImpl worldImpl;
private BlockItemFactory blockItemFactory;
private BlockExplorer blockExplorer;
@Override
public void initialise() {
blockItemFactory = new BlockItemFactory(entityManager);
blockExplorer = new BlockExplorer(assetManager);
targetSystem = new TargetSystem(blockRegistry, physics);
}
@Command(shortDescription = "Lists all available items (prefabs)\nYou can filter by adding the beginning of words " +
"after the commands, e.g.: \"startsWith engine: core:\" will list all items from the engine and core module",
requiredPermission = PermissionManager.CHEAT_PERMISSION)
public String listItems(@CommandParam(value = "startsWith", required = false) String[] startsWith) {
List<String> stringItems = Lists.newArrayList();
for (Prefab prefab : prefabManager.listPrefabs()) {
if (!uriStartsWithAnyString(prefab.getName(), startsWith)) {
continue;
}
stringItems.add(prefab.getName());
}
Collections.sort(stringItems);
StringBuilder items = new StringBuilder();
for (String item : stringItems) {
if (!items.toString().isEmpty()) {
items.append(Console.NEW_LINE);
}
items.append(item);
}
return items.toString();
}
@Command(shortDescription = "List all available blocks\nYou can filter by adding the beginning of words after the" +
"commands, e.g.: \"listBlocks engine: core:\" will list all blocks from the engine and core module",
requiredPermission = PermissionManager.CHEAT_PERMISSION)
public String listBlocks(@CommandParam(value = "startsWith", required = false) String[] startsWith) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Used Blocks");
stringBuilder.append(Console.NEW_LINE);
stringBuilder.append("-----------");
stringBuilder.append(Console.NEW_LINE);
List<BlockUri> registeredBlocks = sortItems(blockManager.listRegisteredBlockUris());
for (BlockUri blockUri : registeredBlocks) {
if (!uriStartsWithAnyString(blockUri.toString(), startsWith)) {
continue;
}
stringBuilder.append(blockUri.toString());
stringBuilder.append(Console.NEW_LINE);
}
stringBuilder.append(Console.NEW_LINE);
stringBuilder.append("Available Blocks");
stringBuilder.append(Console.NEW_LINE);
stringBuilder.append("----------------");
stringBuilder.append(Console.NEW_LINE);
List<BlockUri> availableBlocks = sortItems(blockExplorer.getAvailableBlockFamilies());
for (BlockUri blockUri : availableBlocks) {
if (!uriStartsWithAnyString(blockUri.toString(), startsWith)) {
continue;
}
stringBuilder.append(blockUri.toString());
stringBuilder.append(Console.NEW_LINE);
}
return stringBuilder.toString();
}
@Command(shortDescription = "Lists all available shapes\nYou can filter by adding the beginning of words after the" +
"commands, e.g.: \"listShapes engine: core:\" will list all shapes from the engine and core module",
requiredPermission = PermissionManager.CHEAT_PERMISSION)
public String listShapes(@CommandParam(value = "startsWith", required = false) String[] startsWith) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Shapes");
stringBuilder.append(Console.NEW_LINE);
stringBuilder.append("-----------");
stringBuilder.append(Console.NEW_LINE);
List<ResourceUrn> sortedUris = sortItems(Assets.list(BlockShape.class));
for (ResourceUrn uri : sortedUris) {
if (!uriStartsWithAnyString(uri.toString(), startsWith)) {
continue;
}
stringBuilder.append(uri.toString());
stringBuilder.append(Console.NEW_LINE);
}
return stringBuilder.toString();
}
@Command(shortDescription = "Lists available free shape blocks",
helpText = "Lists all the available free shape blocks. These blocks can be created with any shape.\n" +
"You can filter by adding the beginning of words after the commands, e.g.: \"listFreeShapeBlocks" +
"engine: core:\" will list all free shape blocks from the engine and core module",
requiredPermission = PermissionManager.CHEAT_PERMISSION)
public String listFreeShapeBlocks(@CommandParam(value = "startsWith", required = false) String[] startsWith) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Free Shape Blocks");
stringBuilder.append(Console.NEW_LINE);
stringBuilder.append("-----------------");
stringBuilder.append(Console.NEW_LINE);
List<BlockUri> sortedUris = sortItems(blockExplorer.getFreeformBlockFamilies());
for (BlockUri uri : sortedUris) {
if (!uriStartsWithAnyString(uri.toString(), startsWith)) {
continue;
}
stringBuilder.append(uri.toString());
stringBuilder.append(Console.NEW_LINE);
}
return stringBuilder.toString();
}
@Command(shortDescription = "Replaces a block in front of user",
helpText = "Replaces a block in front of the user at the specified max distance", runOnServer = true, requiredPermission = PermissionManager.CHEAT_PERMISSION)
public void replaceBlock(
@Sender EntityRef sender,
@CommandParam("blockName") String uri,
@CommandParam(value = "maxDistance", required = false) Integer maxDistanceParam) {
int maxDistance = maxDistanceParam != null ? maxDistanceParam : 12;
EntityRef playerEntity = sender.getComponent(ClientComponent.class).character;
EntityRef gazeEntity = GazeAuthoritySystem.getGazeEntityForCharacter(playerEntity);
LocationComponent gazeLocation = gazeEntity.getComponent(LocationComponent.class);
Set<ResourceUrn> matchingUris = Assets.resolveAssetUri(uri, BlockFamilyDefinition.class);
targetSystem.updateTarget(gazeLocation.getWorldPosition(), gazeLocation.getWorldDirection(), maxDistance);
EntityRef target = targetSystem.getTarget();
BlockComponent targetLocation = target.getComponent(BlockComponent.class);
if (matchingUris.size() == 1) {
Optional<BlockFamilyDefinition> def = Assets.get(matchingUris.iterator().next(), BlockFamilyDefinition.class);
if (def.isPresent()) {
BlockFamily blockFamily = blockManager.getBlockFamily(uri);
Block block = blockManager.getBlock(blockFamily.getURI());
world.setBlock(targetLocation.getPosition(), block);
} else if (matchingUris.size() > 1) {
StringBuilder builder = new StringBuilder();
builder.append("Non-unique shape name, possible matches: ");
Iterator<ResourceUrn> shapeUris = sortItems(matchingUris).iterator();
while (shapeUris.hasNext()) {
builder.append(shapeUris.next().toString());
if (shapeUris.hasNext()) {
builder.append(", ");
}
}
}
}
}
@Command(shortDescription = "Gives multiple stacks of blocks matching a search",
helpText = "Adds all blocks that match the search parameter into your inventory",
runOnServer = true, requiredPermission = PermissionManager.CHEAT_PERMISSION)
public String bulkGiveBlock(
@Sender EntityRef sender,
@CommandParam("searched") String searched,
@CommandParam(value = "quantity", required = false) Integer quantityParam,
@CommandParam(value = "shapeName", required = false) String shapeUriParam) {
int quantity = quantityParam != null ? quantityParam : 16;
String searchLowercase = searched.toLowerCase();
List<String> blocks = findBlockMatches(searchLowercase);
String result = "Found " + blocks.size() + " block matches when searching for '" + searched + "'.";
if (blocks.size() > 0) {
result += "\nBlocks:";
for (String block : blocks) {
result += "\n" + block + "\n";
result += giveBlock(sender, block, quantityParam, shapeUriParam);
}
}
return result;
}
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());
}
private static boolean matchesSearch(String searchLowercase, BlockFamilyDefinition def) {
return def.getUrn().toString().toLowerCase().contains(searchLowercase);
}
/**
* Called by 'give' command in ItemCommands.java to attempt to put a block in the player's inventory when no item is found.
* Called by 'giveBulkBlock' command in BlockCommands.java to put a block in the player's inventory.
* @return Null if not found, otherwise success or warning message
*/
public String giveBlock(
@Sender EntityRef sender,
@CommandParam("blockName") String uri,
@CommandParam(value = "quantity", required = false) Integer quantityParam,
@CommandParam(value = "shapeName", required = false) String shapeUriParam) {
int quantity = quantityParam != null ? quantityParam : 16;
Set<ResourceUrn> matchingUris = Assets.resolveAssetUri(uri, BlockFamilyDefinition.class);
if (matchingUris.size() == 1) {
Optional<BlockFamilyDefinition> def = Assets.get(matchingUris.iterator().next(), BlockFamilyDefinition.class);
if (def.isPresent()) {
if (def.get().isFreeform()) {
if (shapeUriParam == null) {
return giveBlock(blockManager.getBlockFamily(new BlockUri(def.get().getUrn(), new ResourceUrn("engine:cube"))), quantity, sender);
} else {
Set<ResourceUrn> resolvedShapeUris = Assets.resolveAssetUri(shapeUriParam, BlockShape.class);
if (resolvedShapeUris.isEmpty()) {
return "Found block. No shape found for '" + shapeUriParam + "'";
} else if (resolvedShapeUris.size() > 1) {
StringBuilder builder = new StringBuilder();
builder.append("Found block. Non-unique shape name, possible matches: ");
Iterator<ResourceUrn> shapeUris = sortItems(resolvedShapeUris).iterator();
while (shapeUris.hasNext()) {
builder.append(shapeUris.next().toString());
if (shapeUris.hasNext()) {
builder.append(", ");
}
}
return builder.toString();
}
return giveBlock(blockManager.getBlockFamily(new BlockUri(def.get().getUrn(), resolvedShapeUris.iterator().next())), quantity, sender);
}
} else {
return giveBlock(blockManager.getBlockFamily(new BlockUri(def.get().getUrn())), quantity, sender);
}
}
} else if (matchingUris.size() > 1) {
StringBuilder builder = new StringBuilder();
builder.append("Non-unique block name, possible matches: ");
Joiner.on(", ").appendTo(builder, matchingUris);
return builder.toString();
}
return null;
}
/**
* Actual implementation of the giveBlock command.
*
* @param blockFamily the block family of the queried block
* @param quantity the number of blocks that are queried
*/
private String giveBlock(BlockFamily blockFamily, int quantity, EntityRef client) {
if (quantity < 1) {
return "Here, have these zero (0) blocks just like you wanted";
}
//continue giving blocks until there are no more blocks to give
//TODO reference maxStackSize instead of explicitly subtracting 99 and introduce an upper bound? 10 million lags ..
for (int quantityLeft = quantity; quantityLeft > 0; quantityLeft -= 99) {
EntityRef item = blockItemFactory.newInstance(blockFamily, quantityLeft > 99 ? 99 : quantityLeft);
if (!item.exists()) {
throw new IllegalArgumentException("Unknown block or item");
}
EntityRef playerEntity = client.getComponent(ClientComponent.class).character;
GiveItemEvent giveItemEvent = new GiveItemEvent(playerEntity);
item.send(giveItemEvent);
if (!giveItemEvent.isHandled()) {
item.destroy();
}
}
return "You received " + quantity + " blocks of " + blockFamily.getDisplayName();
}
private <T extends Comparable<T>> List<T> sortItems(Iterable<T> items) {
List<T> result = Lists.newArrayList();
for (T item : items) {
result.add(item);
}
Collections.sort(result);
return result;
}
/**
* Used to check if an item/prefab/etc name starts with a string that is in {@code uri}
* @param uri the name to be checked
* @param startsWithArray array of possible word to match at the beginning of {@code uri}
* @return true if {@code startsWithArray} is null, empty or {@code uri} starts with one of the elements in it
*/
private boolean uriStartsWithAnyString(String uri, String[] startsWithArray) {
if (startsWithArray == null || startsWithArray.length == 0) {
return true;
}
for (String startsWith : startsWithArray) {
if (uri.startsWith(startsWith)) {
return true;
}
}
return false;
}
}