/* Copyright (c) 2016 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chunky is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Chunky. If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.ui.render;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.stage.FileChooser;
import se.llbit.chunky.renderer.scene.PlayerModel;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.ui.DoubleAdjuster;
import se.llbit.chunky.ui.RenderControlsFxController;
import se.llbit.chunky.world.entity.Entity;
import se.llbit.chunky.world.entity.PlayerEntity;
import se.llbit.json.JsonObject;
import se.llbit.log.Log;
import se.llbit.math.QuickMath;
import se.llbit.math.Vector3;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.Consumer;
public class EntitiesTab extends Tab implements RenderControlsTab, Initializable {
private Scene scene;
static class PlayerData {
public final PlayerEntity entity;
public final String name;
public PlayerData(PlayerEntity entity, Scene scene) {
this.entity = entity;
JsonObject profile = scene.getPlayerProfile(entity);
name = getName(profile);
}
private static String getName(JsonObject profile) {
return profile.get("name").stringValue("Unknown");
}
@Override public String toString() {
return entity.uuid;
}
@Override public int hashCode() {
return entity.uuid.hashCode();
}
@Override public boolean equals(Object obj) {
// Identity comparison is used to ensure that the table in the
// entities tab is properly updated after rebuilding the scene.
if (obj instanceof PlayerData) {
return ((PlayerData) obj).entity == entity;
}
return false;
}
}
@FXML private TableView<PlayerData> entityTable;
@FXML private TableColumn<PlayerData, String> nameCol;
@FXML private TableColumn<PlayerData, String> idCol;
@FXML private Button delete;
@FXML private Button add;
@FXML private Button cameraToPlayer;
@FXML private Button playerToCamera;
@FXML private Button playerToTarget;
@FXML private Button faceCamera;
@FXML private Button faceTarget;
@FXML private ChoiceBox<PlayerModel> playerModel;
@FXML private TextField skin;
@FXML private Button selectSkin;
@FXML private DoubleAdjuster direction;
@FXML private DoubleAdjuster headYaw;
@FXML private DoubleAdjuster headPitch;
@FXML private DoubleAdjuster leftArmPose;
@FXML private DoubleAdjuster rightArmPose;
@FXML private DoubleAdjuster leftLegPose;
@FXML private DoubleAdjuster rightLegPose;
@FXML private DoubleAdjuster scale;
public EntitiesTab() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("EntitiesTab.fxml"));
loader.setRoot(this);
loader.setController(this);
loader.load();
}
@Override public void update(Scene scene) {
// TODO: it might be better to always just rebuild the whole table.
Collection<PlayerData> missing = new HashSet<>(entityTable.getItems());
for (Entity entity : scene.getActors()) {
if (entity instanceof PlayerEntity) {
PlayerData data = new PlayerData((PlayerEntity) entity, scene);
if (!entityTable.getItems().contains(data)) {
entityTable.getItems().add(data);
}
missing.remove(data);
}
}
entityTable.getItems().removeAll(missing);
}
@Override public Tab getTab() {
return this;
}
private void updatePlayer(PlayerEntity player) {
playerModel.getSelectionModel().select(player.model);
skin.setText(player.skin);
direction.set(player.yaw);
headYaw.set(player.headYaw);
headPitch.set(player.pitch);
leftArmPose.set(player.leftArmPose);
rightArmPose.set(player.rightArmPose);
leftLegPose.set(player.leftLegPose);
rightLegPose.set(player.rightLegPose);
scale.set(player.scale);
}
@Override public void initialize(URL location, ResourceBundle resources) {
add.setTooltip(new Tooltip("Add a player at the target position."));
add.setOnAction(e -> {
Collection<Entity> entities = scene.getActors();
Set<String> ids = new HashSet<>();
for (Entity entity : entities) {
if (entity instanceof PlayerEntity) {
ids.add(((PlayerEntity) entity).uuid);
}
}
// Pick a new UUID for the new entity.
long id = System.currentTimeMillis();
while (ids.contains(String.format("%016X%016X", 0, id))) {
id += 1;
}
Vector3 position = scene.getTargetPosition();
if (position == null) {
position = new Vector3(scene.camera().getPosition());
}
PlayerEntity player = new PlayerEntity(String.format("%016X%016X", 0, id), position, 0, 0);
withSelected(selected -> {
player.skin = selected.skin;
player.model = selected.model;
});
player.randomPoseAndLook();
scene.addPlayer(player);
PlayerData data = new PlayerData(player, scene);
entityTable.getItems().add(data);
entityTable.getSelectionModel().select(data);
});
delete.setTooltip(new Tooltip("Delete the selected player."));
delete.setOnAction(e -> withSelected(selected -> {
scene.removePlayer(selected);
update(scene);
}));
selectSkin.setOnAction(e -> withSelected(player -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Load Skin");
fileChooser
.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Minecraft skin", "*.png"));
File skinFile = fileChooser.showOpenDialog(getTabPane().getScene().getWindow());
if (skinFile != null) {
player.setTexture(skinFile.getAbsolutePath());
skin.setText(skinFile.getAbsolutePath());
scene.rebuildActorBvh();
}
}));
entityTable.setRowFactory(tbl -> {
TableRow<PlayerData> row = new TableRow<>();
row.setOnMouseClicked(e -> {
if (e.getClickCount() == 2 && !row.isEmpty()) {
e.consume();
try {
Poser poser = new Poser(row.getItem());
poser.show();
} catch (IOException e1) {
Log.warn("Could not open player poser window.", e1);
}
}
});
return row;
});
cameraToPlayer.setTooltip(new Tooltip("Move the camera to the selected player position."));
cameraToPlayer.setOnAction(e -> withSelected(player -> scene.camera().moveToPlayer(player)));
playerToCamera.setTooltip(new Tooltip("Move the selected player to the camera position."));
playerToCamera.setOnAction(e -> withSelected(player -> {
player.position.set(scene.camera().getPosition());
scene.rebuildActorBvh();
}));
playerToTarget.setTooltip(new Tooltip("Move the selected player to the current target."));
playerToTarget.setOnAction(e -> withSelected(player -> {
Vector3 target = scene.getTargetPosition();
if (target != null) {
player.position.set(target);
scene.rebuildActorBvh();
}
}));
faceCamera.setTooltip(new Tooltip("Makes the selected player look at the camera."));
faceCamera.setOnAction(e -> withSelected(player -> {
player.lookAt(scene.camera().getPosition());
scene.rebuildActorBvh();
}));
faceTarget.setTooltip(new Tooltip("Makes the selected player look at the current view target."));
faceTarget.setOnAction(e -> withSelected(player -> {
Vector3 target = scene.getTargetPosition();
if (target != null) {
player.lookAt(target);
scene.rebuildActorBvh();
}
}));
entityTable.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
updatePlayer(newValue.entity);
}
});
nameCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().name));
idCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().toString()));
playerModel.getItems().addAll(PlayerModel.values());
playerModel.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> withSelected(player -> {
player.model = newValue;
scene.rebuildActorBvh();
}));
direction.setName("Direction");
direction.setRange(-Math.PI, Math.PI);
direction.onValueChange(value -> withSelected(player -> {
player.yaw = value;
scene.rebuildActorBvh();
}));
headYaw.setName("Head yaw");
headYaw.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
headYaw.onValueChange(value -> withSelected(player -> {
player.headYaw = value;
scene.rebuildActorBvh();
}));
headPitch.setName("Head pitch");
headPitch.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
headPitch.onValueChange(value -> withSelected(player -> {
player.pitch = value;
scene.rebuildActorBvh();
}));
leftArmPose.setName("Left arm pose");
leftArmPose.setRange(-Math.PI, Math.PI);
leftArmPose.onValueChange(value -> withSelected(player -> {
player.leftArmPose = value;
scene.rebuildActorBvh();
}));
rightArmPose.setName("Right arm pose");
rightArmPose.setRange(-Math.PI, Math.PI);
rightArmPose.onValueChange(value -> withSelected(player -> {
player.rightArmPose = value;
scene.rebuildActorBvh();
}));
leftLegPose.setName("Left leg pose");
leftLegPose.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
leftLegPose.onValueChange(value -> withSelected(player -> {
player.leftLegPose = value;
scene.rebuildActorBvh();
}));
rightLegPose.setName("Right leg pose");
rightLegPose.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
rightLegPose.onValueChange(value -> withSelected(player -> {
player.rightLegPose = value;
scene.rebuildActorBvh();
}));
scale.setName("Scale");
scale.setRange(0.1, 10);
scale.onValueChange(value -> withSelected(player -> {
player.scale = value;
scene.rebuildActorBvh();
}));
}
private void withSelected(Consumer<PlayerEntity> consumer) {
PlayerData player = entityTable.getSelectionModel().getSelectedItem();
if (player != null) {
consumer.accept(player.entity);
}
}
@Override public void setController(RenderControlsFxController controller) {
scene = controller.getRenderController().getSceneManager().getScene();
}
}