
com.bladecoder.engine.ink.InkManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of blade-engine Show documentation
Show all versions of blade-engine Show documentation
Classic point and click adventure game engine
The newest version!
package com.bladecoder.engine.ink;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.Json.Serializable;
import com.badlogic.gdx.utils.JsonValue;
import com.badlogic.gdx.utils.reflect.ReflectionException;
import com.bladecoder.engine.actions.Action;
import com.bladecoder.engine.actions.ActionCallback;
import com.bladecoder.engine.actions.ActionFactory;
import com.bladecoder.engine.assets.EngineAssetManager;
import com.bladecoder.engine.i18n.I18N;
import com.bladecoder.engine.model.Text.Type;
import com.bladecoder.engine.model.World;
import com.bladecoder.engine.serialization.BladeJson;
import com.bladecoder.engine.serialization.BladeJson.Mode;
import com.bladecoder.engine.util.ActionUtils;
import com.bladecoder.engine.util.EngineLogger;
import com.bladecoder.ink.runtime.Choice;
import com.bladecoder.ink.runtime.InkList;
import com.bladecoder.ink.runtime.ListDefinition;
import com.bladecoder.ink.runtime.Story;
import com.bladecoder.ink.runtime.StoryState;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.ResourceBundle;
public class InkManager implements Serializable {
public final static int KEY_SIZE = 10;
public final static char NAME_VALUE_TAG_SEPARATOR = ':';
public final static char NAME_VALUE_PARAM_SEPARATOR = '=';
private final static String PARAM_SEPARATOR = ",";
public final static char COMMAND_MARK = '>';
public final static char CHAR_SEPARATOR_MARK = ':';
private ResourceBundle i18n;
private Story story = null;
private boolean wasInCutmode;
private String storyName;
private final World w;
private Thread loaderThread;
private final HashMap verbRunners = new HashMap<>();
public InkManager(World w) {
this.w = w;
}
/**
* This method is called when changing scene. All flows, except default, are
* removed.
*/
public void init() {
wasInCutmode = false;
for (String flow : verbRunners.keySet()) {
try {
if (flow.equals(StoryState.kDefaultFlowName))
continue;
story.removeFlow(flow);
} catch (Exception e) {
EngineLogger.error("InkManager - Cannot remove flow: " + flow);
}
}
verbRunners.clear();
}
public void newStory(final String name) throws Exception {
loadThreaded(name, null);
}
private void loadStory(String name) {
try {
FileHandle asset = EngineAssetManager.getInstance()
.getAsset(EngineAssetManager.MODEL_DIR + name + EngineAssetManager.INK_EXT);
long initTime = System.currentTimeMillis();
String json = getJsonString(asset.read());
story = new Story(json);
ExternalFunctions.bindExternalFunctions(w, story);
storyName = name;
loadI18NBundle();
EngineLogger.debug("INK STORY LOADING TIME (ms): " + (System.currentTimeMillis() - initTime));
} catch (Exception e) {
EngineLogger.error("Cannot load Ink Story: " + name + " " + e.getMessage());
story = null;
storyName = null;
}
}
private void loadStoryState(String stateString) {
try {
long initTime = System.currentTimeMillis();
story.getState().loadJson(stateString);
EngineLogger.debug("INK *SAVED STATE* LOADING TIME (ms): " + (System.currentTimeMillis() - initTime));
} catch (Exception e) {
EngineLogger.error("Cannot load Ink Story State for: " + storyName + " " + e.getMessage());
}
}
public void loadI18NBundle() {
if (getStoryName() != null
&& EngineAssetManager.getInstance().getModelFile(storyName + "-ink.properties").exists())
i18n = w.getI18N().getBundle(EngineAssetManager.MODEL_DIR + storyName + "-ink", true);
}
public String translateLine(String line) {
if (line.charAt(0) == I18N.PREFIX) {
String key = line.substring(1);
// In ink, several keys can be included in the same line.
String[] keys = key.split("@");
String translated = "";
for (String k : keys) {
try {
// some untranslated words may follow the key
String k2 = k.substring(0, KEY_SIZE);
translated += i18n.getString(k2);
if (k.length() > KEY_SIZE) {
String trailing = k.substring(KEY_SIZE);
translated += trailing;
}
} catch (Exception e) {
EngineLogger.error("MISSING TRANSLATION KEY: " + key);
return key;
}
}
// In translated lines, spaces can be escaped with '_'.
translated = translated.replace('_', ' ');
return translated;
}
return line;
}
public String getVariable(String name) {
return story.getVariablesState().get(name).toString();
}
public boolean compareVariable(String name, String value) {
waitIfNotLoaded();
if (story.getVariablesState().get(name) instanceof InkList) {
return ((InkList) story.getVariablesState().get(name)).ContainsItemNamed(value);
} else {
return story.getVariablesState().get(name).toString().equals(value == null ? "" : value);
}
}
public void setVariable(String name, String value) throws Exception {
waitIfNotLoaded();
if (story.getVariablesState().get(name) instanceof InkList) {
InkList oldList = (InkList) story.getVariablesState().get(name);
InkList rawList = new InkList(oldList);
if (rawList.getOrigins() == null) {
List names = rawList.getOriginNames();
if (names != null) {
ArrayList origins = new ArrayList<>();
for (String n : names) {
ListDefinition def = story.getListDefinitions().getListDefinition(n);
if (!origins.contains(def))
origins.add(def);
}
rawList.setOrigins(origins);
}
}
rawList.addItem(value);
story.getVariablesState().set(name, rawList);
} else {
story.getVariablesState().set(name, value);
}
}
public void continueMaximally(InkVerbRunner inkVerbRunner) {
waitIfNotLoaded();
String line = null;
HashMap currentLineParams = new HashMap<>();
if (story.canContinue()) {
try {
do {
line = story.Continue();
// Remove trailing '\n'
if (!line.isEmpty())
line = line.substring(0, line.length() - 1);
if (line.isEmpty()) {
EngineLogger.debug("INK EMPTY LINE!");
}
} while (line.isEmpty() && story.canContinue());
if (!line.isEmpty()) {
if (EngineLogger.debugMode())
EngineLogger.debug("INK LINE: " + translateLine(line));
processParams(story.getCurrentTags(), currentLineParams);
// PROCESS COMMANDS
if (line.charAt(0) == COMMAND_MARK) {
processCommand(inkVerbRunner, currentLineParams, line);
} else {
processTextLine(inkVerbRunner, currentLineParams, line);
}
}
} catch (Exception e) {
EngineLogger.error(e.getMessage(), e);
}
}
if (!inkVerbRunner.isFinish()) {
inkVerbRunner.runCurrentAction();
} else {
if (hasChoices()) {
wasInCutmode = w.inCutMode();
w.setCutMode(false);
w.getListener().dialogOptions();
} else {
inkVerbRunner.callCb();
}
}
}
private void processParams(List input, HashMap output) {
for (String t : input) {
String key;
String value;
int i = t.indexOf(NAME_VALUE_TAG_SEPARATOR);
// support ':' and '=' as param separator
if (i == -1)
i = t.indexOf(NAME_VALUE_PARAM_SEPARATOR);
if (i != -1) {
key = t.substring(0, i).trim();
value = t.substring(i + 1, t.length()).trim();
} else {
key = t.trim();
value = null;
}
EngineLogger.debug("PARAM: " + key + " value: " + value);
output.put(key, value);
}
}
private void processCommand(InkVerbRunner inkVerbRunner, HashMap params, String line) {
String commandName;
String[] commandParams;
int i = line.indexOf(NAME_VALUE_TAG_SEPARATOR);
if (i == -1) {
commandName = line.substring(1).trim();
} else {
commandName = line.substring(1, i).trim();
commandParams = line.substring(i + 1).split(PARAM_SEPARATOR);
processParams(Arrays.asList(commandParams), params);
}
if ("LeaveNow".equals(commandName)) {
boolean init = true;
String initVerb = null;
if (params.get("init") != null)
init = Boolean.parseBoolean(params.get("init"));
if (params.get("initVerb") != null)
initVerb = params.get("initVerb");
w.setCurrentScene(params.get("scene"), init, initVerb);
} else {
// Some preliminar validation to see if it's an action
if (commandName.length() > 0) {
// Try to create action by default
Action action;
try {
Class> c = ActionFactory.getClassTags().get(commandName);
if (c == null && commandName.indexOf('.') == -1) {
commandName = "com.bladecoder.engine.actions." + commandName + "Action";
}
action = ActionFactory.create(commandName, params);
action.init(w);
inkVerbRunner.getActions().add(action);
} catch (ClassNotFoundException | ReflectionException e) {
EngineLogger.error(e.getMessage(), e);
}
} else {
EngineLogger.error("Ink command not found: " + commandName);
}
}
}
private void processTextLine(InkVerbRunner inkVerbRunner, HashMap params, String line) {
// Get actor name from Line. Actor is separated by '>' or ':'.
// ej. "Johnny: Hello punks!"
if (!params.containsKey("actor")) {
int idx = line.indexOf(COMMAND_MARK);
if (idx == -1) {
idx = line.indexOf(CHAR_SEPARATOR_MARK);
}
if (idx != -1) {
params.put("actor", line.substring(0, idx).trim());
line = line.substring(idx + 1).trim();
}
}
if (!params.containsKey("actor") && w.getCurrentScene().getPlayer() != null) {
if (!params.containsKey("type")) {
params.put("type", Type.SUBTITLE.toString());
}
} else if (params.containsKey("actor") && !params.containsKey("type")) {
params.put("type", Type.TALK.toString());
} else if (!params.containsKey("type")) {
params.put("type", Type.SUBTITLE.toString());
}
params.put("text", translateLine(line));
try {
Action action = null;
if (!params.containsKey("actor")) {
action = ActionFactory.create("Text", params);
} else {
action = ActionFactory.create("Say", params);
}
action.init(w);
inkVerbRunner.getActions().add(action);
} catch (ClassNotFoundException | ReflectionException e) {
EngineLogger.error(e.getMessage(), e);
}
}
public Story getStory() {
waitIfNotLoaded();
return story;
}
public void runPath(String path, Object[] params, String flow, ActionCallback cb) throws Exception {
waitIfNotLoaded();
if (story == null) {
EngineLogger.error("Ink Story not loaded!");
return;
}
if (flow == null) {
story.switchToDefaultFlow();
} else {
story.switchFlow(flow);
}
story.choosePathString(path, true, params);
InkVerbRunner verbRunner = createVerbRunner(cb);
continueMaximally(verbRunner);
}
private InkVerbRunner createVerbRunner(ActionCallback cb) {
String f = story.getCurrentFlowName();
InkVerbRunner prev = verbRunners.put(story.getCurrentFlowName(), new InkVerbRunner(w, this, f, cb, null));
if (prev != null) {
// we cancel it in case there is some action executing
prev.cancel();
}
return verbRunners.get(f);
}
public boolean hasChoices() {
waitIfNotLoaded();
if (story == null) {
return false;
}
try {
story.switchToDefaultFlow();
} catch (Exception e) {
EngineLogger.error("InkManager: " + e.getMessage());
return false;
}
return ((!verbRunners.containsKey(StoryState.kDefaultFlowName)
|| verbRunners.get(StoryState.kDefaultFlowName).isFinish()) && story.getCurrentChoices().size() > 0);
}
public List getChoices() {
List options = story.getCurrentChoices();
List choices = new ArrayList<>(options.size());
for (Choice o : options) {
String line = o.getText();
// the line maybe empty in default choices.
if (line.isEmpty())
continue;
int idx = line.indexOf(COMMAND_MARK);
if (idx == -1) {
idx = line.indexOf(CHAR_SEPARATOR_MARK);
}
if (idx != -1) {
line = line.substring(idx + 1).trim();
}
choices.add(translateLine(line));
}
return choices;
}
private String getJsonString(InputStream is) throws IOException {
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line = br.readLine();
// Replace the BOM mark
if (line != null)
line = line.replace('\uFEFF', ' ');
while (line != null) {
sb.append(line);
sb.append("\n");
line = br.readLine();
}
return sb.toString();
}
}
public void selectChoice(int i) {
w.setCutMode(wasInCutmode);
try {
story.switchToDefaultFlow();
story.chooseChoiceIndex(i);
continueMaximally(verbRunners.get(StoryState.kDefaultFlowName));
} catch (Exception e) {
EngineLogger.error(e.getMessage(), e);
}
}
public String getStoryName() {
return storyName;
}
public void setStoryName(String storyName) {
this.storyName = storyName;
}
private void waitIfNotLoaded() {
if (loaderThread != null && loaderThread.isAlive()) {
EngineLogger.debug(">>> Loader thread not finished. Waiting for it!!!");
try {
loaderThread.join();
} catch (InterruptedException e) {
}
}
}
private void loadThreaded(final String name, final String state) {
EngineLogger.debug("LOADING INK STORY: " + name + (state == null ? "" : " WITH SAVED STATE."));
loaderThread = new Thread() {
@Override
public void run() {
if (name != null)
loadStory(name);
if (state != null)
loadStoryState(state);
}
};
loaderThread.start();
}
public HashMap getVerbRunners() {
return verbRunners;
}
public InkVerbRunner getDefaultVerbRunner() {
return verbRunners.get(StoryState.kDefaultFlowName);
}
@Override
public void write(Json json) {
BladeJson bjson = (BladeJson) json;
json.writeValue("storyName", storyName);
if (bjson.getMode() == Mode.STATE) {
json.writeValue("wasInCutmode", wasInCutmode);
// SAVE STORY
if (story != null) {
try {
json.writeValue("story", story.getState().toJson());
} catch (Exception e) {
EngineLogger.error("Error saving Ink state", e);
}
json.writeValue("verbRunners", verbRunners, verbRunners.getClass(), InkVerbRunner.class);
}
}
}
@Override
public void read(Json json, JsonValue jsonData) {
BladeJson bjson = (BladeJson) json;
World w = bjson.getWorld();
final String name = json.readValue("storyName", String.class, jsonData);
if (bjson.getMode() == Mode.MODEL) {
story = null;
storyName = name;
// Only load in new game.
// If the SAVED_GAME_VERSION property exists we are loading a saved
// game and we will load the story in the STATE mode.
if (bjson.getInit()) {
loadThreaded(name, null);
}
} else {
wasInCutmode = json.readValue("wasInCutmode", Boolean.class, jsonData);
// READ STORY
final String storyString = json.readValue("story", String.class, jsonData);
if (storyString != null) {
loadThreaded(name, storyString);
}
// FOR BACKWARD COMPATIBILITY
if (jsonData.has("actions")) {
String sCb = json.readValue("cb", String.class, jsonData);
// READ ACTIONS
JsonValue actionsValue = jsonData.get("actions");
InkVerbRunner inkVerbRunner = new InkVerbRunner(w, this, StoryState.kDefaultFlowName, null, sCb);
verbRunners.put(StoryState.kDefaultFlowName, inkVerbRunner);
for (int i = 0; i < actionsValue.size; i++) {
JsonValue aValue = actionsValue.get(i);
Action a = ActionUtils.readJson(w, json, aValue);
inkVerbRunner.getActions().add(a);
}
inkVerbRunner.setIP(json.readValue("ip", Integer.class, jsonData));
actionsValue = jsonData.get("actionsSer");
int i = 0;
for (Action a : inkVerbRunner.getActions()) {
if (a instanceof Serializable && i < actionsValue.size) {
if (actionsValue.get(i) == null)
break;
((Serializable) a).read(json, actionsValue.get(i));
i++;
}
}
} else {
for (int i = 0; i < jsonData.get("verbRunners").size; i++) {
JsonValue jRunner = jsonData.get("verbRunners").get(i);
InkVerbRunner inkVerbRunner = new InkVerbRunner(w, this, jRunner.name, null, null);
verbRunners.put(jRunner.name, inkVerbRunner);
inkVerbRunner.read(json, jRunner);
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy