diff --git a/ink-engine-runtime/JsonSerialisation.cs b/ink-engine-runtime/JsonSerialisation.cs index 9ff1ddad..967fa71f 100644 --- a/ink-engine-runtime/JsonSerialisation.cs +++ b/ink-engine-runtime/JsonSerialisation.cs @@ -689,7 +689,7 @@ public static object RuntimeObjectToJToken(Runtime.Object obj) throw new System.Exception ("Failed to convert runtime object to Json token: " + obj); } - static void WriteRuntimeContainer(SimpleJson.Writer writer, Container container, bool withoutName = false) + public static void WriteRuntimeContainer(SimpleJson.Writer writer, Container container, bool withoutName = false) { writer.WriteArrayStart(); @@ -917,6 +917,7 @@ static void WriteInkList(SimpleJson.Writer writer, ListValue listVal) return dict; } +#warning Remove me! public static Dictionary ListDefinitionsToJToken (ListDefinitionsOrigin origin) { var result = new Dictionary (); diff --git a/ink-engine-runtime/SimpleJson.cs b/ink-engine-runtime/SimpleJson.cs index 8370be57..bc9a0e8c 100644 --- a/ink-engine-runtime/SimpleJson.cs +++ b/ink-engine-runtime/SimpleJson.cs @@ -11,9 +11,9 @@ namespace Ink.Runtime /// internal static class SimpleJson { - public static string DictionaryToText (Dictionary rootObject) + public static string DictionaryToTextOld (Dictionary rootObject) { - return new Writer (rootObject).ToString (); + return new WriterOld (rootObject).ToString (); } public static Dictionary TextToDictionary (string text) @@ -291,15 +291,137 @@ void SkipWhitespace () object _rootObject; } - public class Writer + public class WriterOld { - public Writer (object rootObject) + public WriterOld(object rootObject) { - _sb = new StringBuilder (); + _sb = new StringBuilder(); - WriteObject (rootObject); + WriteObject(rootObject); } + void WriteObject(object obj) + { + if (obj is int) + { + _sb.Append((int)obj); + } + else if (obj is float) + { + string floatStr = ((float)obj).ToString(System.Globalization.CultureInfo.InvariantCulture); + _sb.Append(floatStr); + if (!floatStr.Contains(".")) _sb.Append(".0"); + } + else if (obj is bool) + { + _sb.Append((bool)obj == true ? "true" : "false"); + } + else if (obj == null) + { + _sb.Append("null"); + } + else if (obj is string) + { + string str = (string)obj; + _sb.EnsureCapacity(_sb.Length + str.Length + 2); + _sb.Append('"'); + + foreach (var c in str) + { + if (c < ' ') + { + // Don't write any control characters except \n and \t + switch (c) + { + case '\n': + _sb.Append("\\n"); + break; + case '\t': + _sb.Append("\\t"); + break; + } + } + else + { + switch (c) + { + case '\\': + case '"': + _sb.Append('\\').Append(c); + break; + default: + _sb.Append(c); + break; + } + } + } + + _sb.Append('"'); + } + else if (obj is Dictionary) + { + WriteDictionary((Dictionary)obj); + } + else if (obj is List) + { + WriteList((List)obj); + } + else + { + throw new System.Exception("ink's SimpleJson writer doesn't currently support this object: " + obj); + } + } + + void WriteDictionary(Dictionary dict) + { + _sb.Append("{"); + + bool isFirst = true; + foreach (var keyValue in dict) + { + + if (!isFirst) _sb.Append(","); + + _sb.Append("\""); + _sb.Append(keyValue.Key); + _sb.Append("\":"); + + WriteObject(keyValue.Value); + + isFirst = false; + } + + _sb.Append("}"); + } + + void WriteList(List list) + { + _sb.Append("["); + + bool isFirst = true; + foreach (var obj in list) + { + if (!isFirst) _sb.Append(","); + + WriteObject(obj); + + isFirst = false; + } + + _sb.Append("]"); + } + + public override string ToString() + { + return _sb.ToString(); + } + + + StringBuilder _sb; + } + + public class Writer + { public Writer() { _writer = new StringWriter(); @@ -307,7 +429,7 @@ public Writer() public Writer(Stream stream) { - _writer = new StreamWriter(stream, Encoding.UTF8); + _writer = new System.IO.StreamWriter(stream, Encoding.UTF8); } public void WriteObject(Action inner) @@ -319,7 +441,7 @@ public void WriteObject(Action inner) public void WriteObjectStart() { - StartNewObject(container:true); + StartNewObject(container: true); _stateStack.Push(new StateElement { type = State.Object }); _writer.Write("{"); } @@ -379,7 +501,8 @@ public void WritePropertyEnd() _stateStack.Pop(); } - public void WritePropertyNameStart() { + public void WritePropertyNameStart() + { Assert(state == State.Object); if (childCount > 0) @@ -398,7 +521,7 @@ public void WritePropertyNameEnd() Assert(state == State.PropertyName); _writer.Write("\":"); - + // Pop PropertyName, leaving Property state _stateStack.Pop(); } @@ -436,8 +559,9 @@ void WriteProperty(T name, Action inner) WritePropertyEnd(); } - public void WriteArrayStart() { - StartNewObject(container:true); + public void WriteArrayStart() + { + StartNewObject(container: true); _stateStack.Push(new StateElement { type = State.Array }); _writer.Write("["); } @@ -467,7 +591,7 @@ public void Write(float f) if (!floatStr.Contains(".")) _writer.Write(".0"); } - public void Write(string str, bool escape=true) + public void Write(string str, bool escape = true) { StartNewObject(container: false); @@ -487,7 +611,7 @@ public void Write(bool b) public void WriteNull() { - StartNewObject(container:false); + StartNewObject(container: false); _writer.Write("null"); } @@ -505,7 +629,7 @@ public void WriteStringEnd() _stateStack.Pop(); } - public void WriteStringInner(string str, bool escape=true) + public void WriteStringInner(string str, bool escape = true) { Assert(state == State.String); if (escape) @@ -516,13 +640,42 @@ public void WriteStringInner(string str, bool escape=true) void WriteEscapedString(string str) { - // TODO: Escape the string - _writer.Write(str); + foreach (var c in str) + { + if (c < ' ') + { + // Don't write any control characters except \n and \t + switch (c) + { + case '\n': + _writer.Write("\\n"); + break; + case '\t': + _writer.Write("\\t"); + break; + } + } + else + { + switch (c) + { + case '\\': + case '"': + _writer.Write("\\"); + _writer.Write(c); + break; + default: + _writer.Write(c); + break; + } + } + } } - void StartNewObject(bool container) { + void StartNewObject(bool container) + { - if(container) + if (container) Assert(state == State.None || state == State.Property || state == State.Array); else Assert(state == State.Property || state == State.Array); @@ -534,15 +687,19 @@ void WriteEscapedString(string str) IncrementChildCount(); } - State state { - get { + State state + { + get + { if (_stateStack.Count > 0) return _stateStack.Peek().type; else return State.None; } } - int childCount { - get { + int childCount + { + get + { if (_stateStack.Count > 0) return _stateStack.Peek().childCount; else return 0; } @@ -562,6 +719,11 @@ void Assert(bool condition) throw new System.Exception("Assert failed while writing JSON"); } + public override string ToString() + { + return _writer.ToString(); + } + enum State { None, @@ -572,122 +734,17 @@ enum State String }; - struct StateElement { + struct StateElement + { public State type; public int childCount; } Stack _stateStack = new Stack(); - - - - - void WriteObject (object obj) - { - if (obj is int) { - _sb.Append ((int)obj); - } else if (obj is float) { - string floatStr = ((float)obj).ToString(System.Globalization.CultureInfo.InvariantCulture); - _sb.Append (floatStr); - if (!floatStr.Contains (".")) _sb.Append (".0"); - } else if( obj is bool) { - _sb.Append ((bool)obj == true ? "true" : "false"); - } else if (obj == null) { - _sb.Append ("null"); - } else if (obj is string) { - string str = (string)obj; - _sb.EnsureCapacity(_sb.Length + str.Length + 2); - _sb.Append('"'); - - foreach (var c in str) - { - if (c < ' ') - { - // Don't write any control characters except \n and \t - switch (c) - { - case '\n': - _sb.Append("\\n"); - break; - case '\t': - _sb.Append("\\t"); - break; - } - } - else - { - switch (c) - { - case '\\': - case '"': - _sb.Append('\\').Append(c); - break; - default: - _sb.Append(c); - break; - } - } - } - - _sb.Append('"'); - } else if (obj is Dictionary) { - WriteDictionary ((Dictionary)obj); - } else if (obj is List) { - WriteList ((List)obj); - }else { - throw new System.Exception ("ink's SimpleJson writer doesn't currently support this object: " + obj); - } - } - - void WriteDictionary (Dictionary dict) - { - _sb.Append ("{"); - - bool isFirst = true; - foreach (var keyValue in dict) { - - if (!isFirst) _sb.Append (","); - - _sb.Append ("\""); - _sb.Append (keyValue.Key); - _sb.Append ("\":"); - - WriteObject (keyValue.Value); - - isFirst = false; - } - - _sb.Append ("}"); - } - - void WriteList (List list) - { - _sb.Append ("["); - - bool isFirst = true; - foreach (var obj in list) { - if (!isFirst) _sb.Append (","); - - WriteObject (obj); - - isFirst = false; - } - - _sb.Append ("]"); - } - - public override string ToString () - { - if (_writer != null) - return _writer.ToString(); - else - return _sb.ToString (); - } - - - StringBuilder _sb; TextWriter _writer; } + + } } diff --git a/ink-engine-runtime/Story.cs b/ink-engine-runtime/Story.cs index f4afe74c..c0442f0e 100644 --- a/ink-engine-runtime/Story.cs +++ b/ink-engine-runtime/Story.cs @@ -192,7 +192,24 @@ public Story(string jsonString) : this((Container)null) /// /// The Story itself in JSON representation. /// - public string ToJsonString() + public string ToJson() + { + //return ToJsonOld(); + var writer = new SimpleJson.Writer(); + ToJson(writer); + return writer.ToString(); + } + + /// + /// The Story itself in JSON representation. + /// + public void ToJson(Stream stream) + { + var writer = new SimpleJson.Writer(stream); + ToJson(writer); + } + + string ToJsonOld() { var rootContainerJsonList = (List) Json.RuntimeObjectToJToken (_mainContentContainer); @@ -203,7 +220,45 @@ public string ToJsonString() if (_listDefinitions != null) rootObject ["listDefs"] = Json.ListDefinitionsToJToken (_listDefinitions); - return SimpleJson.DictionaryToText (rootObject); + return SimpleJson.DictionaryToTextOld (rootObject); + } + + void ToJson(SimpleJson.Writer writer) + { + writer.WriteObjectStart(); + + writer.WriteProperty("inkVersion", inkVersionCurrent); + + // Main container content + writer.WriteProperty("root", w => Json.WriteRuntimeContainer(w, _mainContentContainer)); + + // List definitions + if (_listDefinitions != null) { + + writer.WritePropertyStart("listDefs"); + writer.WriteObjectStart(); + + foreach (ListDefinition def in _listDefinitions.lists) + { + writer.WritePropertyStart(def.name); + writer.WriteObjectStart(); + + foreach (var itemToVal in def.items) + { + InkListItem item = itemToVal.Key; + int val = itemToVal.Value; + writer.WriteProperty(item.itemName, val); + } + + writer.WriteObjectEnd(); + writer.WritePropertyEnd(); + } + + writer.WriteObjectEnd(); + writer.WritePropertyEnd(); + } + + writer.WriteObjectEnd(); } /// diff --git a/ink-engine-runtime/StoryState.cs b/ink-engine-runtime/StoryState.cs index 28309c05..b45f9e66 100755 --- a/ink-engine-runtime/StoryState.cs +++ b/ink-engine-runtime/StoryState.cs @@ -21,16 +21,7 @@ public class StoryState public const int kInkSaveStateVersion = 8; const int kMinCompatibleLoadVersion = 8; - /// - /// Exports the current state to json format, in order to save the game. - /// - /// The save state in json format. public string ToJson() { - return SimpleJson.DictionaryToText (jsonToken); - - } - - public string ToNewJson() { var writer = new SimpleJson.Writer(); WriteJson(writer); return writer.ToString(); @@ -41,77 +32,14 @@ public class StoryState WriteJson(writer); } - void WriteJson(SimpleJson.Writer writer) - { - writer.WriteObjectStart(); - - - bool hasChoiceThreads = false; - foreach (Choice c in _currentChoices) - { - c.originalThreadIndex = c.threadAtGeneration.threadIndex; - - if (callStack.ThreadWithIndex(c.originalThreadIndex) == null) - { - if (!hasChoiceThreads) - { - hasChoiceThreads = true; - writer.WritePropertyStart("choiceThreads"); - writer.WriteObjectStart(); - } - - writer.WritePropertyStart(c.originalThreadIndex); - c.threadAtGeneration.WriteJson(writer); - writer.WritePropertyEnd(); - } - } - - if (hasChoiceThreads) - { - writer.WriteObjectEnd(); - writer.WritePropertyEnd(); - } - - writer.WriteProperty("callstackThreads", callStack.WriteJson); - - writer.WriteProperty("variablesState", variablesState.WriteJson); - - writer.WriteProperty("evalStack", w => Json.WriteListRuntimeObjs(w, evaluationStack)); - - writer.WriteProperty("outputStream", w => Json.WriteListRuntimeObjs(w, _outputStream)); - - writer.WriteProperty("currentChoices", w => { - w.WriteArrayStart(); - foreach (var c in _currentChoices) - Json.WriteChoice(w, c); - w.WriteArrayEnd(); - }); - - if (!divertedPointer.isNull) - writer.WriteProperty("currentDivertTarget", divertedPointer.path.componentsString); - - writer.WriteProperty("visitCounts", w => Json.WriteIntDictionary(w, visitCounts)); - writer.WriteProperty("turnIndices", w => Json.WriteIntDictionary(w, turnIndices)); - - writer.WriteProperty("turnIdx", currentTurnIndex); - writer.WriteProperty("storySeed", storySeed); - writer.WriteProperty("previousRandom", previousRandom); - - writer.WriteProperty("inkSaveVersion", kInkSaveStateVersion); - - // Not using this right now, but could do in future. - writer.WriteProperty("inkFormatVersion", Story.inkVersionCurrent); - - writer.WriteObjectEnd(); - } - /// /// Loads a previously saved state in JSON format. /// /// The JSON string to load. public void LoadJson(string json) { - jsonToken = SimpleJson.TextToDictionary (json); + var jObject = SimpleJson.TextToDictionary (json); + LoadJsonObj(jObject); } /// @@ -391,108 +319,116 @@ internal StoryState Copy() return copy; } - - /// - /// Object representation of full JSON state. Usually you should use - /// LoadJson and ToJson since they serialise directly to string for you. - /// But it may be useful to get the object representation so that you - /// can integrate it into your own serialisation system. - /// - public Dictionary jsonToken + + void WriteJson(SimpleJson.Writer writer) { - get { - - var obj = new Dictionary (); + writer.WriteObjectStart(); - Dictionary choiceThreads = null; - foreach (Choice c in _currentChoices) { - c.originalThreadIndex = c.threadAtGeneration.threadIndex; - if( callStack.ThreadWithIndex(c.originalThreadIndex) == null ) { - if( choiceThreads == null ) - choiceThreads = new Dictionary (); + bool hasChoiceThreads = false; + foreach (Choice c in _currentChoices) + { + c.originalThreadIndex = c.threadAtGeneration.threadIndex; - choiceThreads[c.originalThreadIndex.ToString()] = c.threadAtGeneration.jsonToken; - } + if (callStack.ThreadWithIndex(c.originalThreadIndex) == null) + { + if (!hasChoiceThreads) + { + hasChoiceThreads = true; + writer.WritePropertyStart("choiceThreads"); + writer.WriteObjectStart(); + } + + writer.WritePropertyStart(c.originalThreadIndex); + c.threadAtGeneration.WriteJson(writer); + writer.WritePropertyEnd(); } - if( choiceThreads != null ) - obj["choiceThreads"] = choiceThreads; + } - - obj ["callstackThreads"] = callStack.GetJsonToken(); - obj ["variablesState"] = variablesState.jsonToken; + if (hasChoiceThreads) + { + writer.WriteObjectEnd(); + writer.WritePropertyEnd(); + } + + writer.WriteProperty("callstackThreads", callStack.WriteJson); - obj ["evalStack"] = Json.ListToJArray (evaluationStack); + writer.WriteProperty("variablesState", variablesState.WriteJson); - obj ["outputStream"] = Json.ListToJArray (_outputStream); + writer.WriteProperty("evalStack", w => Json.WriteListRuntimeObjs(w, evaluationStack)); - obj ["currentChoices"] = Json.ListToJArray (_currentChoices); + writer.WriteProperty("outputStream", w => Json.WriteListRuntimeObjs(w, _outputStream)); - if( !divertedPointer.isNull ) - obj ["currentDivertTarget"] = divertedPointer.path.componentsString; + writer.WriteProperty("currentChoices", w => { + w.WriteArrayStart(); + foreach (var c in _currentChoices) + Json.WriteChoice(w, c); + w.WriteArrayEnd(); + }); - obj ["visitCounts"] = Json.IntDictionaryToJObject (visitCounts); - obj ["turnIndices"] = Json.IntDictionaryToJObject (turnIndices); - obj ["turnIdx"] = currentTurnIndex; - obj ["storySeed"] = storySeed; - obj ["previousRandom"] = previousRandom; + if (!divertedPointer.isNull) + writer.WriteProperty("currentDivertTarget", divertedPointer.path.componentsString); - obj ["inkSaveVersion"] = kInkSaveStateVersion; + writer.WriteProperty("visitCounts", w => Json.WriteIntDictionary(w, visitCounts)); + writer.WriteProperty("turnIndices", w => Json.WriteIntDictionary(w, turnIndices)); - // Not using this right now, but could do in future. - obj ["inkFormatVersion"] = Story.inkVersionCurrent; + writer.WriteProperty("turnIdx", currentTurnIndex); + writer.WriteProperty("storySeed", storySeed); + writer.WriteProperty("previousRandom", previousRandom); - return obj; - } - set { + writer.WriteProperty("inkSaveVersion", kInkSaveStateVersion); - var jObject = value; + // Not using this right now, but could do in future. + writer.WriteProperty("inkFormatVersion", Story.inkVersionCurrent); - object jSaveVersion = null; - if (!jObject.TryGetValue("inkSaveVersion", out jSaveVersion)) { - throw new StoryException ("ink save format incorrect, can't load."); - } - else if ((int)jSaveVersion < kMinCompatibleLoadVersion) { - throw new StoryException("Ink save format isn't compatible with the current version (saw '"+jSaveVersion+"', but minimum is "+kMinCompatibleLoadVersion+"), so can't load."); - } + writer.WriteObjectEnd(); + } - callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstackThreads"], story); - variablesState.jsonToken = (Dictionary < string, object> )jObject["variablesState"]; + void LoadJsonObj(Dictionary jObject) + { + object jSaveVersion = null; + if (!jObject.TryGetValue("inkSaveVersion", out jSaveVersion)) { + throw new StoryException ("ink save format incorrect, can't load."); + } + else if ((int)jSaveVersion < kMinCompatibleLoadVersion) { + throw new StoryException("Ink save format isn't compatible with the current version (saw '"+jSaveVersion+"', but minimum is "+kMinCompatibleLoadVersion+"), so can't load."); + } - evaluationStack = Json.JArrayToRuntimeObjList ((List)jObject ["evalStack"]); + callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstackThreads"], story); + variablesState.jsonToken = (Dictionary < string, object> )jObject["variablesState"]; - _outputStream = Json.JArrayToRuntimeObjList ((List)jObject ["outputStream"]); - OutputStreamDirty(); + evaluationStack = Json.JArrayToRuntimeObjList ((List)jObject ["evalStack"]); - _currentChoices = Json.JArrayToRuntimeObjList((List)jObject ["currentChoices"]); + _outputStream = Json.JArrayToRuntimeObjList ((List)jObject ["outputStream"]); + OutputStreamDirty(); - object currentDivertTargetPath; - if (jObject.TryGetValue("currentDivertTarget", out currentDivertTargetPath)) { - var divertPath = new Path (currentDivertTargetPath.ToString ()); - divertedPointer = story.PointerAtPath (divertPath); - } - - visitCounts = Json.JObjectToIntDictionary ((Dictionary)jObject ["visitCounts"]); - turnIndices = Json.JObjectToIntDictionary ((Dictionary)jObject ["turnIndices"]); - currentTurnIndex = (int)jObject ["turnIdx"]; - storySeed = (int)jObject ["storySeed"]; - previousRandom = (int)jObject ["previousRandom"]; - - object jChoiceThreadsObj = null; - jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj); - var jChoiceThreads = (Dictionary)jChoiceThreadsObj; - - foreach (var c in _currentChoices) { - var foundActiveThread = callStack.ThreadWithIndex(c.originalThreadIndex); - if( foundActiveThread != null ) { - c.threadAtGeneration = foundActiveThread.Copy (); - } else { - var jSavedChoiceThread = (Dictionary ) jChoiceThreads[c.originalThreadIndex.ToString()]; - c.threadAtGeneration = new CallStack.Thread(jSavedChoiceThread, story); - } - } + _currentChoices = Json.JArrayToRuntimeObjList((List)jObject ["currentChoices"]); + object currentDivertTargetPath; + if (jObject.TryGetValue("currentDivertTarget", out currentDivertTargetPath)) { + var divertPath = new Path (currentDivertTargetPath.ToString ()); + divertedPointer = story.PointerAtPath (divertPath); } + + visitCounts = Json.JObjectToIntDictionary ((Dictionary)jObject ["visitCounts"]); + turnIndices = Json.JObjectToIntDictionary ((Dictionary)jObject ["turnIndices"]); + currentTurnIndex = (int)jObject ["turnIdx"]; + storySeed = (int)jObject ["storySeed"]; + previousRandom = (int)jObject ["previousRandom"]; + + object jChoiceThreadsObj = null; + jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj); + var jChoiceThreads = (Dictionary)jChoiceThreadsObj; + + foreach (var c in _currentChoices) { + var foundActiveThread = callStack.ThreadWithIndex(c.originalThreadIndex); + if( foundActiveThread != null ) { + c.threadAtGeneration = foundActiveThread.Copy (); + } else { + var jSavedChoiceThread = (Dictionary ) jChoiceThreads[c.originalThreadIndex.ToString()]; + c.threadAtGeneration = new CallStack.Thread(jSavedChoiceThread, story); + } + } } internal void ResetErrors() diff --git a/inklecate/CommandLineTool.cs b/inklecate/CommandLineTool.cs index 02e0acfe..5c931bc2 100644 --- a/inklecate/CommandLineTool.cs +++ b/inklecate/CommandLineTool.cs @@ -149,7 +149,7 @@ void ExitWithUsageInstructions() // Compile mode else { - var jsonStr = story.ToJsonString (); + var jsonStr = story.ToJson (); try { File.WriteAllText (opts.outputFile, jsonStr, System.Text.Encoding.UTF8); diff --git a/tests/Tests.cs b/tests/Tests.cs index d4a1dd62..375f8ed7 100644 --- a/tests/Tests.cs +++ b/tests/Tests.cs @@ -3708,7 +3708,7 @@ protected Story CompileString(string str, bool countAllVisits = false, bool test // Convert to json and back again if (_mode == TestMode.JsonRoundTrip && story != null) { - var jsonStr = story.ToJsonString(); + var jsonStr = story.ToJson(); story = new Story(jsonStr); }