package com.bladecoder.ink.runtime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map.Entry; import com.bladecoder.ink.runtime.ControlCommand.CommandType; public class Json { public static List<RTObject> jArrayToRuntimeObjList(List<Object> jArray, boolean skipLast) throws Exception { int count = jArray.size(); if (skipLast) count--; List<RTObject> list = new ArrayList<>(jArray.size()); for (int i = 0; i < count; i++) { Object jTok = jArray.get(i); RTObject runtimeObj = jTokenToRuntimeObject(jTok); list.add(runtimeObj); } return list; } @SuppressWarnings("unchecked") public static <T extends RTObject> List<T> jArrayToRuntimeObjList(List<Object> jArray) throws Exception { return (List<T>) jArrayToRuntimeObjList(jArray, false); } public static void writeDictionaryRuntimeObjs(SimpleJson.Writer writer, HashMap<String, RTObject> dictionary) throws Exception { writer.writeObjectStart(); for (Entry<String, RTObject> keyVal : dictionary.entrySet()) { writer.writePropertyStart(keyVal.getKey()); writeRuntimeObject(writer, keyVal.getValue()); writer.writePropertyEnd(); } writer.writeObjectEnd(); } public static void writeListRuntimeObjs(SimpleJson.Writer writer, List<RTObject> list) throws Exception { writer.writeArrayStart(); for (RTObject val : list) { writeRuntimeObject(writer, val); } writer.writeArrayEnd(); } public static void WriteIntDictionary(SimpleJson.Writer writer, HashMap<String, Integer> dict) throws Exception { writer.writeObjectStart(); for (Entry<String, Integer> keyVal : dict.entrySet()) writer.writeProperty(keyVal.getKey(), keyVal.getValue()); writer.writeObjectEnd(); } public static void writeRuntimeObject(SimpleJson.Writer writer, RTObject obj) throws Exception { if (obj instanceof Container) { writeRuntimeContainer(writer, (Container) obj); return; } if (obj instanceof Divert) { Divert divert = (Divert) obj; String divTypeKey = "->"; if (divert.isExternal()) divTypeKey = "x()"; else if (divert.getPushesToStack()) { if (divert.getStackPushType() == PushPopType.Function) divTypeKey = "f()"; else if (divert.getStackPushType() == PushPopType.Tunnel) divTypeKey = "->t->"; } String targetStr; if (divert.hasVariableTarget()) targetStr = divert.getVariableDivertName(); else targetStr = divert.getTargetPathString(); writer.writeObjectStart(); writer.writeProperty(divTypeKey, targetStr); if (divert.hasVariableTarget()) writer.writeProperty("var", true); if (divert.isConditional()) writer.writeProperty("c", true); if (divert.getExternalArgs() > 0) writer.writeProperty("exArgs", divert.getExternalArgs()); writer.writeObjectEnd(); return; } if (obj instanceof ChoicePoint) { ChoicePoint choicePoint = (ChoicePoint) obj; writer.writeObjectStart(); writer.writeProperty("*", choicePoint.getPathStringOnChoice()); writer.writeProperty("flg", choicePoint.getFlags()); writer.writeObjectEnd(); return; } if (obj instanceof IntValue) { IntValue intVal = (IntValue) obj; writer.write(intVal.value); return; } if (obj instanceof FloatValue) { FloatValue floatVal = (FloatValue) obj; writer.write(floatVal.value); return; } if (obj instanceof StringValue) { StringValue strVal = (StringValue) obj; if (strVal.isNewline()) writer.write("\\n", false); else { writer.writeStringStart(); writer.writeStringInner("^"); writer.writeStringInner(strVal.value); writer.writeStringEnd(); } return; } if (obj instanceof ListValue) { writeInkList(writer, (ListValue) obj); return; } if (obj instanceof DivertTargetValue) { DivertTargetValue divTargetVal = (DivertTargetValue) obj; writer.writeObjectStart(); writer.writeProperty("^->", divTargetVal.value.getComponentsString()); writer.writeObjectEnd(); return; } if (obj instanceof VariablePointerValue) { VariablePointerValue varPtrVal = (VariablePointerValue) obj; writer.writeObjectStart(); writer.writeProperty("^var", varPtrVal.value); writer.writeProperty("ci", varPtrVal.getContextIndex()); writer.writeObjectEnd(); return; } if (obj instanceof Glue) { writer.write("<>"); return; } if (obj instanceof ControlCommand) { ControlCommand controlCmd = (ControlCommand) obj; writer.write(controlCommandNames[controlCmd.getCommandType().ordinal()]); return; } if (obj instanceof NativeFunctionCall) { NativeFunctionCall nativeFunc = (NativeFunctionCall) obj; String name = nativeFunc.getName(); // Avoid collision with ^ used to indicate a string if (name == "^") name = "L^"; writer.write(name); return; } // Variable reference if (obj instanceof VariableReference) { VariableReference varRef = (VariableReference) obj; writer.writeObjectStart(); String readCountPath = varRef.getPathStringForCount(); if (readCountPath != null) { writer.writeProperty("CNT?", readCountPath); } else { writer.writeProperty("VAR?", varRef.getName()); } writer.writeObjectEnd(); return; } // Variable assignment if (obj instanceof VariableAssignment) { VariableAssignment varAss = (VariableAssignment) obj; writer.writeObjectStart(); String key = varAss.isGlobal() ? "VAR=" : "temp="; writer.writeProperty(key, varAss.getVariableName()); // Reassignment? if (!varAss.isNewDeclaration()) writer.writeProperty("re", true); writer.writeObjectEnd(); return; } // Void if (obj instanceof Void) { writer.write("void"); return; } // Tag if (obj instanceof Tag) { Tag tag = (Tag) obj; writer.writeObjectStart(); writer.writeProperty("#", tag.getText()); writer.writeObjectEnd(); return; } // Used when serialising save state only if (obj instanceof Choice) { Choice choice = (Choice) obj; writeChoice(writer, choice); return; } throw new Exception("Failed to write runtime object to JSON: " + obj); } public static HashMap<String, RTObject> jObjectToHashMapRuntimeObjs(HashMap<String, Object> jRTObject) throws Exception { HashMap<String, RTObject> dict = new HashMap<>(jRTObject.size()); for (Entry<String, Object> keyVal : jRTObject.entrySet()) { dict.put(keyVal.getKey(), jTokenToRuntimeObject(keyVal.getValue())); } return dict; } public static HashMap<String, Integer> jObjectToIntHashMap(HashMap<String, Object> jRTObject) throws Exception { HashMap<String, Integer> dict = new HashMap<>(jRTObject.size()); for (Entry<String, Object> keyVal : jRTObject.entrySet()) { dict.put(keyVal.getKey(), (Integer) keyVal.getValue()); } return dict; } // ---------------------- // JSON ENCODING SCHEME // ---------------------- // // Glue: "<>", "G<", "G>" // // ControlCommand: "ev", "out", "/ev", "du" "pop", "->->", "~ret", "str", // "/str", "nop", // "choiceCnt", "turns", "visit", "seq", "thread", "done", "end" // // NativeFunction: "+", "-", "/", "*", "%" "~", "==", ">", "<", ">=", "<=", // "!=", "!"... etc // // Void: "void" // // Value: "^string value", "^^string value beginning with ^" // 5, 5.2 // {"^->": "path.target"} // {"^var": "varname", "ci": 0} // // Container: [...] // [..., // { // "subContainerName": ..., // "#f": 5, // flags // "#n": "containerOwnName" // only if not redundant // } // ] // // Divert: {"->": "path.target", "c": true } // {"->": "path.target", "var": true} // {"f()": "path.func"} // {"->t->": "path.tunnel"} // {"x()": "externalFuncName", "exArgs": 5} // // Var Assign: {"VAR=": "varName", "re": true} // reassignment // {"temp=": "varName"} // // Var ref: {"VAR?": "varName"} // {"CNT?": "stitch name"} // // ChoicePoint: {"*": pathString, // "flg": 18 } // // Choice: Nothing too clever, it's only used in the save state, // there's not likely to be many of them. // // Tag: {"#": "the tag text"} @SuppressWarnings("unchecked") public static RTObject jTokenToRuntimeObject(Object token) throws Exception { if (token instanceof Integer || token instanceof Float) { return AbstractValue.create(token); } if (token instanceof String) { String str = (String) token; // String value char firstChar = str.charAt(0); if (firstChar == '^') return new StringValue(str.substring(1)); else if (firstChar == '\n' && str.length() == 1) return new StringValue("\n"); // Glue if ("<>".equals(str)) return new Glue(); for (int i = 0; i < controlCommandNames.length; ++i) { // Control commands (would looking up in a hash set be faster?) String cmdName = controlCommandNames[i]; if (str.equals(cmdName)) { return new ControlCommand(CommandType.values()[i + 1]); } } // Native functions // "^" conflicts with the way to identify strings, so now // we know it's not a string, we can convert back to the proper // symbol for the operator. if ("L^".equals(str)) str = "^"; if (NativeFunctionCall.callExistsWithName(str)) return NativeFunctionCall.callWithName(str); // Pop if ("->->".equals(str)) return ControlCommand.popTunnel(); else if ("~ret".equals(str)) return ControlCommand.popFunction(); // Void if ("void".equals(str)) return new Void(); } if (token instanceof HashMap<?, ?>) { HashMap<String, Object> obj = (HashMap<String, Object>) token; Object propValue; // Divert target value to path propValue = obj.get("^->"); if (propValue != null) { return new DivertTargetValue(new Path((String) propValue)); } // VariablePointerValue propValue = obj.get("^var"); if (propValue != null) { VariablePointerValue varPtr = new VariablePointerValue((String) propValue); propValue = obj.get("ci"); if (propValue != null) varPtr.setContextIndex((Integer) propValue); return varPtr; } // Divert boolean isDivert = false; boolean pushesToStack = false; PushPopType divPushType = PushPopType.Function; boolean external = false; propValue = obj.get("->"); if (propValue != null) { isDivert = true; } else { propValue = obj.get("f()"); if (propValue != null) { isDivert = true; pushesToStack = true; divPushType = PushPopType.Function; } else { propValue = obj.get("->t->"); if (propValue != null) { isDivert = true; pushesToStack = true; divPushType = PushPopType.Tunnel; } else { propValue = obj.get("x()"); if (propValue != null) { isDivert = true; external = true; pushesToStack = false; divPushType = PushPopType.Function; } } } } if (isDivert) { Divert divert = new Divert(); divert.setPushesToStack(pushesToStack); divert.setStackPushType(divPushType); divert.setExternal(external); String target = propValue.toString(); propValue = obj.get("var"); if (propValue != null) { divert.setVariableDivertName(target); } else { divert.setTargetPathString(target); } propValue = obj.get("c"); divert.setConditional(propValue != null); if (external) { propValue = obj.get("exArgs"); if (propValue != null) { divert.setExternalArgs((Integer) propValue); } } return divert; } // Choice propValue = obj.get("*"); if (propValue != null) { ChoicePoint choice = new ChoicePoint(); choice.setPathStringOnChoice(propValue.toString()); propValue = obj.get("flg"); if (propValue != null) { choice.setFlags((Integer) propValue); } return choice; } // Variable reference propValue = obj.get("VAR?"); if (propValue != null) { return new VariableReference(propValue.toString()); } else { propValue = obj.get("CNT?"); if (propValue != null) { VariableReference readCountVarRef = new VariableReference(); readCountVarRef.setPathStringForCount(propValue.toString()); return readCountVarRef; } } // Variable assignment boolean isVarAss = false; boolean isGlobalVar = false; propValue = obj.get("VAR="); if (propValue != null) { isVarAss = true; isGlobalVar = true; } else { propValue = obj.get("temp="); if (propValue != null) { isVarAss = true; isGlobalVar = false; } } if (isVarAss) { String varName = propValue.toString(); propValue = obj.get("re"); boolean isNewDecl = propValue == null; VariableAssignment varAss = new VariableAssignment(varName, isNewDecl); varAss.setIsGlobal(isGlobalVar); return varAss; } // Tag propValue = obj.get("#"); if (propValue != null) { return new Tag((String) propValue); } // List value propValue = obj.get("list"); if (propValue != null) { HashMap<String, Object> listContent = (HashMap<String, Object>) propValue; InkList rawList = new InkList(); propValue = obj.get("origins"); if (propValue != null) { List<String> namesAsObjs = (List<String>) propValue; rawList.setInitialOriginNames(namesAsObjs); } for (Entry<String, Object> nameToVal : listContent.entrySet()) { InkListItem item = new InkListItem(nameToVal.getKey()); int val = (int) nameToVal.getValue(); rawList.put(item, val); } return new ListValue(rawList); } // Used when serialising save state only if (obj.get("originalChoicePath") != null) return jObjectToChoice(obj); } // Array is always a Runtime.Container if (token instanceof List<?>) { return jArrayToContainer((List<Object>) token); } if (token == null) return null; throw new Exception("Failed to convert token to runtime RTObject: " + token); } public static void writeRuntimeContainer(SimpleJson.Writer writer, Container container) throws Exception { writeRuntimeContainer(writer, container, false); } public static void writeRuntimeContainer(SimpleJson.Writer writer, Container container, boolean withoutName) throws Exception { writer.writeArrayStart(); for (RTObject c : container.getContent()) writeRuntimeObject(writer, c); // Container is always an array [...] // But the final element is always either: // - a dictionary containing the named content, as well as possibly // the key "#" with the count flags // - null, if neither of the above HashMap<String, RTObject> namedOnlyContent = container.getNamedOnlyContent(); int countFlags = container.getCountFlags(); boolean hasNameProperty = container.getName() != null && !withoutName; boolean hasTerminator = namedOnlyContent != null || countFlags > 0 || hasNameProperty; if (hasTerminator) writer.writeObjectStart(); if (namedOnlyContent != null) { for (Entry<String, RTObject> namedContent : namedOnlyContent.entrySet()) { String name = namedContent.getKey(); Container namedContainer = namedContent.getValue() instanceof Container ? (Container) namedContent.getValue() : null; writer.writePropertyStart(name); writeRuntimeContainer(writer, namedContainer, true); writer.writePropertyEnd(); } } if (countFlags > 0) writer.writeProperty("#f", countFlags); if (hasNameProperty) writer.writeProperty("#n", container.getName()); if (hasTerminator) writer.writeObjectEnd(); else writer.writeNull(); writer.writeArrayEnd(); } @SuppressWarnings("unchecked") static Container jArrayToContainer(List<Object> jArray) throws Exception { Container container = new Container(); container.setContent(jArrayToRuntimeObjList(jArray, true)); // Final RTObject in the array is always a combination of // - named content // - a "#" key with the countFlags // (if either exists at all, otherwise null) HashMap<String, Object> terminatingObj = (HashMap<String, Object>) jArray.get(jArray.size() - 1); if (terminatingObj != null) { HashMap<String, RTObject> namedOnlyContent = new HashMap<>(terminatingObj.size()); for (Entry<String, Object> keyVal : terminatingObj.entrySet()) { if ("#f".equals(keyVal.getKey())) { container.setCountFlags((int) keyVal.getValue()); } else if ("#n".equals(keyVal.getKey())) { container.setName(keyVal.getValue().toString()); } else { RTObject namedContentItem = jTokenToRuntimeObject(keyVal.getValue()); Container namedSubContainer = namedContentItem instanceof Container ? (Container) namedContentItem : (Container) null; if (namedSubContainer != null) namedSubContainer.setName(keyVal.getKey()); namedOnlyContent.put(keyVal.getKey(), namedContentItem); } } container.setNamedOnlyContent(namedOnlyContent); } return container; } static Choice jObjectToChoice(HashMap<String, Object> jObj) throws Exception { Choice choice = new Choice(); choice.setText(jObj.get("text").toString()); choice.setIndex((int) jObj.get("index")); choice.sourcePath = jObj.get("originalChoicePath").toString(); choice.originalThreadIndex = (int) jObj.get("originalThreadIndex"); choice.setPathStringOnChoice(jObj.get("targetPath").toString()); return choice; } public static void writeChoice(SimpleJson.Writer writer, Choice choice) throws Exception { writer.writeObjectStart(); writer.writeProperty("text", choice.getText()); writer.writeProperty("index", choice.getIndex()); writer.writeProperty("originalChoicePath", choice.sourcePath); writer.writeProperty("originalThreadIndex", choice.originalThreadIndex); writer.writeProperty("targetPath", choice.getPathStringOnChoice()); writer.writeObjectEnd(); } static void writeInkList(SimpleJson.Writer writer, ListValue listVal) throws Exception { InkList rawList = listVal.getValue(); writer.writeObjectStart(); writer.writePropertyStart("list"); writer.writeObjectStart(); for (Entry<InkListItem, Integer> itemAndValue : rawList.entrySet()) { InkListItem item = itemAndValue.getKey(); int itemVal = itemAndValue.getValue(); writer.writePropertyNameStart(); writer.writePropertyNameInner(item.getOriginName() != null ? item.getOriginName() : "?"); writer.writePropertyNameInner("."); writer.writePropertyNameInner(item.getItemName()); writer.writePropertyNameEnd(); writer.write(itemVal); writer.writePropertyEnd(); } writer.writeObjectEnd(); writer.writePropertyEnd(); if (rawList.size() == 0 && rawList.getOriginNames() != null && rawList.getOriginNames().size() > 0) { writer.writePropertyStart("origins"); writer.writeArrayStart(); for (String name : rawList.getOriginNames()) writer.write(name); writer.writeArrayEnd(); writer.writePropertyEnd(); } writer.writeObjectEnd(); } public static HashMap<String, Object> listDefinitionsToJToken(ListDefinitionsOrigin origin) { HashMap<String, Object> result = new HashMap<>(); for (ListDefinition def : origin.getLists()) { HashMap<String, Object> listDefJson = new HashMap<>(); for (Entry<InkListItem, Integer> itemToVal : def.getItems().entrySet()) { InkListItem item = itemToVal.getKey(); int val = itemToVal.getValue(); listDefJson.put(item.getItemName(), val); } result.put(def.getName(), listDefJson); } return result; } @SuppressWarnings("unchecked") public static ListDefinitionsOrigin jTokenToListDefinitions(Object obj) { HashMap<String, Object> defsObj = (HashMap<String, Object>) obj; List<ListDefinition> allDefs = new ArrayList<>(); for (Entry<String, Object> kv : defsObj.entrySet()) { String name = kv.getKey(); HashMap<String, Object> listDefJson = (HashMap<String, Object>) kv.getValue(); // Cast (string, object) to (string, int) for items HashMap<String, Integer> items = new HashMap<>(); for (Entry<String, Object> nameValue : listDefJson.entrySet()) items.put(nameValue.getKey(), (int) nameValue.getValue()); ListDefinition def = new ListDefinition(name, items); allDefs.add(def); } return new ListDefinitionsOrigin(allDefs); } private final static String[] controlCommandNames; static { controlCommandNames = new String[CommandType.values().length - 1]; controlCommandNames[CommandType.EvalStart.ordinal() - 1] = "ev"; controlCommandNames[CommandType.EvalOutput.ordinal() - 1] = "out"; controlCommandNames[CommandType.EvalEnd.ordinal() - 1] = "/ev"; controlCommandNames[CommandType.Duplicate.ordinal() - 1] = "du"; controlCommandNames[CommandType.PopEvaluatedValue.ordinal() - 1] = "pop"; controlCommandNames[CommandType.PopFunction.ordinal() - 1] = "~ret"; controlCommandNames[CommandType.PopTunnel.ordinal() - 1] = "->->"; controlCommandNames[CommandType.BeginString.ordinal() - 1] = "str"; controlCommandNames[CommandType.EndString.ordinal() - 1] = "/str"; controlCommandNames[CommandType.NoOp.ordinal() - 1] = "nop"; controlCommandNames[CommandType.ChoiceCount.ordinal() - 1] = "choiceCnt"; controlCommandNames[CommandType.Turns.ordinal() - 1] = "turn"; controlCommandNames[CommandType.TurnsSince.ordinal() - 1] = "turns"; controlCommandNames[CommandType.ReadCount.ordinal() - 1] = "readc"; controlCommandNames[CommandType.Random.ordinal() - 1] = "rnd"; controlCommandNames[CommandType.SeedRandom.ordinal() - 1] = "srnd"; controlCommandNames[CommandType.VisitIndex.ordinal() - 1] = "visit"; controlCommandNames[CommandType.SequenceShuffleIndex.ordinal() - 1] = "seq"; controlCommandNames[CommandType.StartThread.ordinal() - 1] = "thread"; controlCommandNames[CommandType.Done.ordinal() - 1] = "done"; controlCommandNames[CommandType.End.ordinal() - 1] = "end"; controlCommandNames[CommandType.ListFromInt.ordinal() - 1] = "listInt"; controlCommandNames[CommandType.ListRange.ordinal() - 1] = "range"; controlCommandNames[CommandType.ListRandom.ordinal() - 1] = "lrnd"; for (int i = 0; i < CommandType.values().length - 1; ++i) { if (controlCommandNames[i] == null) throw new ExceptionInInitializerError("Control command not accounted for in serialisation"); } } }