/* Copyright (c) 2012-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.renderer.scene;
import se.llbit.chunky.renderer.RenderContext;
import se.llbit.chunky.renderer.Renderer;
import se.llbit.chunky.renderer.SceneProvider;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.World;
import se.llbit.log.Log;
import se.llbit.util.TaskTracker;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
/**
* This scene manager is used for asynchronous loading and saving of scenes.
* This class ensures that only one scene action happens at a time, and
* the actions are performed on a separate thread to avoid blocking the GUI.
*
* @author Jesper Öqvist <jesper@llbit.se>
*/
public class AsynchronousSceneManager extends Thread implements SceneManager {
private final SynchronousSceneManager sceneManager;
private Runnable currentTask = null;
public AsynchronousSceneManager(RenderContext context, Renderer renderer) {
super("Scene Manager");
sceneManager = new SynchronousSceneManager(context, renderer);
}
public SceneProvider getSceneProvider() {
return sceneManager;
}
public void setResetHandler(RenderResetHandler resetHandler) {
sceneManager.setResetHandler(resetHandler);
}
public void setTaskTracker(TaskTracker taskTracker) {
sceneManager.setTaskTracker(taskTracker);
}
public void setOnSceneLoaded(Runnable onSceneLoaded) {
sceneManager.setOnSceneLoaded(onSceneLoaded);
}
public void setOnChunksLoaded(Runnable onChunksLoaded) {
sceneManager.setOnChunksLoaded(onChunksLoaded);
}
@Override public Scene getScene() {
return sceneManager.getScene();
}
@Override public void run() {
try {
while (!isInterrupted()) {
synchronized (this) {
while (currentTask == null) {
wait();
}
}
currentTask.run();
synchronized (this) {
currentTask = null;
}
}
} catch (InterruptedException ignored) {
// Interrupted.
}
}
/**
* Load the given scene.
*
* @param name the name of the scene to load.
*/
@Override public synchronized void loadScene(String name) {
if (currentTask != null) {
Log.warn("Can't load scene right now.");
} else {
currentTask = () -> {
try {
sceneManager.loadScene(name);
} catch (IOException e) {
Log.warn("Could not load scene.\nReason: " + e.getMessage());
} catch (SceneLoadingError e) {
Log.warn("Could not open scene description.\nReason: " + e.getMessage());
} catch (InterruptedException e) {
Log.warn("Scene loading was interrupted.");
}
};
notifyAll();
}
}
/**
* Save the current scene.
*/
@Override public synchronized void saveScene() {
if (currentTask != null) {
Log.warn("Can't save the scene right now.");
} else {
currentTask = () -> {
try {
sceneManager.saveScene();
} catch (InterruptedException e) {
Log.warn("Scene saving was interrupted.");
}
};
notifyAll();
}
}
/**
* Load chunks and reset camera.
*/
@Override
public synchronized void loadFreshChunks(World world, Collection<ChunkPosition> chunks) {
if (currentTask != null) {
Log.warn("Can't load chunks right now.");
} else {
currentTask = () -> sceneManager.loadFreshChunks(world, chunks);
notifyAll();
}
}
/**
* Load chunks without moving the camera.
*/
@Override public synchronized void loadChunks(World world, Collection<ChunkPosition> chunks) {
if (currentTask != null) {
Log.warn("Can't load chunks right now.");
} else {
currentTask = () -> sceneManager.loadChunks(world, chunks);
notifyAll();
}
}
/**
* Reload all chunks
*/
@Override public synchronized void reloadChunks() {
if (currentTask != null) {
Log.warn("Can't load chunks right now.");
} else {
currentTask = sceneManager::reloadChunks;
notifyAll();
}
}
/**
* Merge a render dump into the current render.
*/
public synchronized void mergeRenderDump(File renderDump) {
if (currentTask != null) {
Log.warn("Can't merge render dump right now.");
} else {
currentTask = () -> sceneManager.mergeDump(renderDump);
notifyAll();
}
}
/**
* Find a preferred scene name by attempting to avoid name collisions.
*
* @return the preferred scene name
*/
public static String preferredSceneName(RenderContext context, String name) {
String suffix = "";
name = sanitizedSceneName(name);
int count = 0;
do {
String targetName = name + suffix;
if (sceneNameIsAvailable(context, targetName)) {
return targetName;
}
count += 1;
suffix = "" + count;
} while (count < 256);
// Give up.
return name;
}
/**
* Remove problematic characters from scene name.
*
* @return sanitized scene name
*/
public static String sanitizedSceneName(String name) {
name = name.trim();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < name.length(); ++i) {
char c = name.charAt(i);
if (isValidSceneNameChar(c)) {
sb.append(c);
} else if (c >= '\u0020' && c <= '\u007e') {
sb.append('_');
}
}
String stripped = sb.toString().trim();
if (stripped.isEmpty()) {
return "Scene";
} else {
return stripped;
}
}
/**
* @return <code>false</code> if the character can cause problems on any
* supported platform.
*/
public static boolean isValidSceneNameChar(char c) {
switch (c) {
case '/':
case ':':
case ';':
case '\\': // Windows file separator.
case '*':
case '?':
case '"':
case '<':
case '>':
case '|':
return false;
}
if (c < '\u0020') {
return false;
}
if (c > '\u007e' && c < '\u00a0') {
return false;
}
return true;
}
/**
* Check for scene name collision.
*
* @return <code>true</code> if the scene name does not collide with an
* already existing scene
*/
public static boolean sceneNameIsAvailable(RenderContext context, String sceneName) {
return !context.getSceneDescriptionFile(sceneName).exists();
}
/**
* Check for scene name validity.
*
* @return <code>true</code> if the scene name contains only legal characters
*/
public static boolean sceneNameIsValid(String name) {
for (int i = 0; i < name.length(); ++i) {
if (!isValidSceneNameChar(name.charAt(i))) {
return false;
}
}
return true;
}
public void discardSceneChanges() {
sceneManager.discardSceneChanges();
}
public void applySceneChanges() {
sceneManager.applySceneChanges();
}
}