diff --git a/Documentation/ink_JSON_runtime_format.md b/Documentation/ink_JSON_runtime_format.md index ed9f486e..77e2a20a 100644 --- a/Documentation/ink_JSON_runtime_format.md +++ b/Documentation/ink_JSON_runtime_format.md @@ -4,14 +4,16 @@ When ink is compiled to JSON, it is converted to a low level format for use by t ## Top level -At the top level of the JSON file are two properties. `inkVersion` is an integer that denotes the format version, and `root`, which is the outer-most Container for the entire story. +At the top level of the JSON file are two properties. `inkVersion` is an integer that denotes the format version, and `root`, which is the outer-most Container for the entire story. Additionally, there may also be a `listDefs` property, which contains the definitions of any lists used in the story. ```json { - "inkVersion": 10, - "root": + "inkVersion": 21, + "root": , + "listDefs": } ``` +The `root` Container may have a named sub-container named `"global decl"`. This sub-container contains any global declarations that are a part of the story. Broadly speaking, the entire format is composed of Containers, and individual sub-elements of the Story, within those Containers. @@ -39,6 +41,21 @@ Examples: A container with the text object "test", flags 1 and 2, and a nested container named "subContainer" that resembles the first example. +## List Definitions +The `listDefs` property at the top level of the JSON file contains the definitions for any lists defined in the story. The format of `listDefs` is as follows: +```json +{ + : { + : , + ... + }, + ... +} +``` +With `list name` and `entry name`s being `strings` and `entry value`s being `int`s + +The property names at the top level of `listDefs` are the names of the lists, while the values are the contents of the list definition. The property names in the definitions are the names of the list items, while the values are the corresponding numerical values. + ## Values Values are the main content objects. The most useful for written content is the String Value, which is used for all the main text within the story flow. @@ -51,6 +68,7 @@ Supported types: * **string**: Represented with a leading `^` to differentiate from other string-based objects. e.g. `"^Hello world"` is used in JSON to represent the text `Hello world`, and `"^^ up there ^"` would be the text `^ up there ^`. No `^` is needed for a newline, so it's just `"\n"`. * **int** and **float**: these are represented using their standard JSON counterparts. e.g. `5`, `5.6`. +* **boolean**: Represented with JSON `true` and `false`. * **divert target**: represents a variable divert target, for example as used in the following ink: VAR x = -> somewhere @@ -67,6 +85,40 @@ Supported types: * **0** - Variable is a global * **1 or more** - variable is a local/temporary in the callstack element with the given index. +* **list**: List values. The initial values of `list` variables are created like this in the `global decl` sub-container of `root`. If the list contains any objects, or does not have any origin lists, it is represented as follows: + ```json + { + "list": { + : , + ... + } + } + ``` + With `qualified value name` being a dot-separated `string` containing the name of the origin list and the item name, and `numeric value` being the numeric value of the item from its origin list. + + If the list does not contain any objects, but does have origin lists, it is instead represented as follows: + ```json + { + "list": {}, + "origins": [ + , + ... + ] + } + ``` + With `origin list name` being the name of one of the lists that are origins of this list. + +### Deprecated Values + +Legacy tags are recognized by the runtime engine and deserializer, but are not output by the serializer. They should therefore only appear in old runtime JSON files. They take the following form: +```json +{ + "#": +} +``` + +Legacy tags exist only for backwards compatibility, and should be ignored in favour of dynamic tags, which use the `"#"` and `"/#"` control commands (see [Control Commands](#control_commands)). Tag objects still exist within the runtime, but only for flattening tags down when evaluating `ControlCommand.EndString` (`"/str"`). + ## Void Represented by `"void"`, this is used to place an object on the evaluation stack when a function returns without a value. @@ -86,20 +138,102 @@ Control commands are special instructions to the text engine to perform various * `"nop"` - No-operation. Does nothing, but is useful as an addressable piece of content to divert to. * `"choiceCnt"` - Pushes an integer with the current number of choices to the evaluation stack. * `"turn"` - Pushes an integer with the current turn number to the evaluation stack. -* `"turns"` - Pops from the evaluation stack, expecting to see a divert target for a knot, stitch, gather or choice. Pushes an integer with the number of turns since that target was last visited by the story engine. +* `"turns"` - Pops from the evaluation stack, expecting to see a divert target for a knot, stitch, gather, or choice. Pushes an integer with the number of turns since that target was last visited by the story engine. * `"visit"` - Pushes an integer with the number of visits to the current container by the story engine. * `"seq"` - Pops an integer, expected to be the number of elements in a sequence that's being entered. In return, it pushes an integer with the next sequence shuffle index to the evaluation stack. This shuffle index is derived from the number of elements in the sequence, the number of elements in it, and the story's random seed from when it was first begun. * `"thread"` - Clones/starts a new thread, as used with the `<- knot` syntax in ink. This essentially clones the entire callstack, branching it. * `"done"` - Tries to close/pop the active thread, otherwise marks the story flow safe to exit without a loose end warning. * `"end"` - Ends the story flow immediately, closes all active threads, unwinds the callstack, and removes any choices that were previously created. +* `"readc"` - Pops from the evaluation stack, expecting to see a divert target for a knot, stitch, gather, or choice. Pushes an integer with the number of times that target has been visited by the story engine. +* `"rnd"` - Pops two values from the evaluation stack, expecting to see two `int`s. Generates a random value between the two integers, with the first value popped as the maximum and the second as the minimum, and pushes the random value to the stack. +* `"srnd"` - Pops one value from the evaluation stack, expecting to see an `int`. Sets the seed for the random generator to the given value and resets any additional randomness factors, then pushes `void` to the stack. +* `"listInt"` - Pops two values from the evaluation stack, expecting to see an `int` and then a `string`. Pushes a `list` containing only the list item with the given value (from the `int`) from the specified list (from the `string`). +* `"range"` - Pops three values from the evaluation stack. The first two are either `int`s or `list`s, the third is a `list`. If the first argument is a `list`, the value of the lowest item will be used. If the second argument is a `list`, the value of the highest item will be used. Generates a `list` containing every item from the third value that is between the bounds of the first and second values, inclusive. +* `"lrnd"` - Pops one value from the evaluation stack, expecting to see a `list`. Pushes a `list` containing one random item from the argument to the stack. +* `"#"` - Adds a marker to the output stream to indicate that the following `string` values belong to a tag, until a `"/#"` or another `"#"` is found. If another `"#"` marker is found, it marks the beginning of a new tag. +* `"/#"` - Adds a marker to the output stream indicating that the `string` values between it and the preceding `"#"` are all part of a tag. Should only be encountered in string evaluation when generating text for a choice. In that case, the `string` values in the output stream since the last `"#"` are removed and added as a tag to the choice. ## Native functions -These are mathematical and logical functions that pop 1 or 2 arguments from the evaluation stack, evaluate the result, and push the result back onto the evaluation stack. The following operators are supported: - -`"+"`, `"-"`, `"/"`, `"*"`, `"%"` (mod), `"_"` (unary negate), `"=="`, `">"`, `"<"`, `">="`, `"<="`, `"!="`, `"!"` (unary 'not'), `"&&"`, `"||"`, `"MIN"`, `"MAX"` - -Booleans are supported only in the C-style - i.e. as integers where non-zero is treated as "true" and zero as "false". The true result of a boolean operation is pushed to the evaluation stack as `1`. +These are mathematical and logical functions that pop 1 or 2 arguments from the evaluation stack, evaluate the result, and push the result back onto the evaluation stack. Arguments are popped as a group, so the bottommost value is the first argument and the topmost is the second argument. Types are coerced so that both arguments are of the same type, see [Type coercion](#type-coercion). This is why `bool`s are not listed as an argument type. + +The following operators are supported: + +| Operator | Supported Types | No. Arguments | Effects | +| -------- | --------------- | ------------- | ----- | +| `"+"` | `int`, `float`, `string`, `list` | 2 | Addition on `int`s and `float`s, concatination on `string`s, set union on `list`s. | +| `"-"` | `int`, `float`, `list` | 2 | Subtraction on `int`s and `float`s, set difference on `list`s. | +| `"*"` | `int`, `float` | 2 | Standard multiplication. | +| `"/"` | `int`, `float` | 2 | Standard division. | +| `"%"` | `int`, `float` | 2 | Modulo operator (`fmod` for `float`s). | +| `"_"` | `int`, `float` | 1 | Negation. | +| `"=="` | `int`, `float`, `string`, `list`, `divert target` | 2 | Equal. *Returns a `bool`.* | +| `"!="` | `int`, `float`, `string`, `list`, `divert target` | 2 | Not Equal. *Returns a `bool`.* | +| `">"` | `int`, `float`, `list` | 2 | Greater than. [See below for lists](#comparison-operators-and-lists). *Returns a `bool`.* | +| `"<"` | `int`, `float`, `list` | 2 | Less than. [See below for lists](#comparison-operators-and-lists). *Returns a `bool`.* | +| `">="` | `int`, `float`, `list` | 2 | Greater than or equals. [See below for lists](#comparison-operators-and-lists). *Returns a `bool`.* | +| `"<="` | `int`, `float`, `list` | 2 | Less than or equals. [See below for lists](#comparison-operators-and-lists). *Returns a `bool`.* | +| `"!"` | `int`, `float`, `list` | 1 | Unary not. Returns `true` for `int`s and `float`s if the argument is equal to 0. Returns 1 for `list`s if the provided list has at least one element, otherwise returns 0. | +| `"&&"` | `int`, `float`, `list` | 2 | Logical and. Returns `true` if both arguments are truthy. For `int`s and `float`s, this means nonzero. For `list`s, this means that the list contains at least one element. | +| `"\|\|"` | `int`, `float`, `list` | 2 | Logical or. Returns `true` if either argument is truthy. +| `"MIN"` | `int`, `float` | 2 | Minimum. Returns the lowest of the two arguments. | +| `"MAX"` | `int`, `float` | 2 | Maximum. Returns the highest of the two arguments. | +| `"POW"` | `int`, `float` | 2 | Exponentiation. Returns the first argument raised to the power of the second argument. Always casts to floats to handle negative exponents. | +| `"FLOOR"`| `int`, `float` | 1 | Floor. Returns the argument rounded *down* to the nearest whole number. | +| `"CEILING"` | `int`, `float` | 1 | Ceiling. Returns the argument rounded *up* to the nearest whole number. | +| `"INT"` | `int`, `float` | 1 | Returns the argument as an `int`, rounded towards zero. | +| `"FLOAT"` | `int`, `float` | 1 | Returns the argument as a `float`. | +| `"?"` | `string`, `list` | 2 | If `string`: Returns `true` if the second argument is a substring of the first argument. If `list`: Returns `true` if the first argument contains every element of the second argument. If either list is empty, returns `false`. | +| `"!?"` | `string`, `list` | 2 | If `string`: Returns `true` if the second argument is *not* a substring of the first argument. If `list`: returns `true` if the first argument does *not* contain every element in the second agument. If either list is empty, returns `true`. | +| `"L^"` | `list` | 2 | Intersection. Returns a list that contains only elements that are in *both* of the argument lists. Represented as `"^"` within the runtime, but not the JSON representation in order to not be confused with strings. | +| `"LIST_MIN"` | `list` | 1 | Returns a list containing only the minimum item in the list. | +| `"LIST_MAX"` | `list` | 1 | Returns a list containing only the maximum item in the list. | +| `"LIST_ALL"` | `list` | 1 | Returns a list with every item from the original list definition to which the argument belongs. | +| `"LIST_COUNT"` | `list` | 1 | Returns the number of elements in the argument. | +| `"LIST_VALUE"` | `list` | 1 | Returns the value of the maximum item in the list. | +| `"LIST_INVERT"` | `list` | 1 | Returns a list containing every item from the original list definition that is not in the argument list. | + +### Comparison operators and Lists +When comparing lists, comparison operators (excluding equality and inequality, so `">"`, `"<"`, `">="`, `"<="`) compare the highest and lowest values in the lists. + +`">"` returns `true` if the *minimum* value in the first argument is larger than the *maximum* value in the second argument. +`"<"` does the same, but with *maximum* value of the first argument being compared with the *minimum* value of the second argument. + +`">="` and `"<="` both check if their relation holds true with the minimum and maximum values of both lists. +`">="` returns `true` if the *minimum* value of the first argument is greater than or equal to the *minimum* value of the second argument, *and* if the *maximum* value of the first argument is greater than or equal to the *maximum* value of the second argument. +`"<="` does the same, but checking if the values in the first argument are less than or equal to the values in the second argument. + +### Type coercion +When a native function is called, its arguments may be coerced. This is done so that binary operations only have one type for their arguments and for specific unary operators. + +Types exist in the following hierarchy: +`bool` < `int` < `float` < `list` < `string`. + +If a native function is called with two different types, the lower type in the hierarchy is coerced to the higher type. `list`s are an exception to this. If one of the arguments is a `list`, and the other argument is not above `list` in the hierarchy, the other argument must be another `list` or an `int`, otherwise an unrecoverable error occurs. + +Diverts and variable pointers are not a part of this hierarchy. + +Coercions work as follows: +* `bool` - `bool` is *always* coerced to another type. + * `int` or `float`: `true` becomes `1` and `false` becomes `0`. + * `string`: `true` becomes `"true"` and `false` becomes `"false"`. +* `int` + * `bool`: `0` becomes `false`, all other values become `true`. + * `float`: Remains the same value, but a `float`. + * `string`: Becomes a `string` representation of the value. + * `list`: Becomes a `list` containing only the item that is equivalent to the value of the `int` from the definition of the `list`. +* `float` + * `bool`: `0` becomes `false`, all other values become `true`. + * `int`: Becomes an int, rounded towards 0. + * `string`: Becomes a `string` representation of the value, serialized according to .NET's InvariantCulture format. +* `list` + * `int` or `float`: If `list` is empty, becomes `0`. Otherwise becomes the value of the maximum item in the list. + * `string`: If `list` is empty, becomes `""`. Otherwise becomes the full dot-seperated name of the maximum item in the list. +* `string` + * `int`: Tries to parse the `string` value as an `int`. + * `float`: Tries to parse the `string` value as a `float`, deserialized according to .NET's InvariantCulture format and Float number style. + +Any types not listed cannot be coerced. Diverts and variable pointers can't be coerced to or from at all. ## Divert @@ -125,6 +259,7 @@ Examples: ## Variable reference Obtain the current value of a named variable, and push it to the evaluation stack. +If the reference is to the name of an item from a list, a list containing only the named list item will be pushed. If multiple lists have items with the same name, a fully-qualified name must be used. Example: @@ -164,7 +299,7 @@ The `flg` field is a bitfield of flags: Example of the full JSON output, including the ChoicePoint object, when generating an actual ink choice from `* Hello[.], world.`. Most of the complexity is derived from the fact that content can be dynamic, and the square bracket notation that requires repetition. -```jsonc +```json // Outer container [ @@ -253,3 +388,6 @@ Examples: * `building.entrance.3.0` - the first element of a Container at the fourth element of a Container named `entrance` within a Container named `building` of the root Container. * `.^.1` - the second element of the parent Container. +## Glue + +Glue is represented as `"<>"`. Glue removes all whitespace from the end of the output stream until no newlines remain. Any whitespace before the first newline remains as is. Glue also causes all future whitespace to not be appended to the output stream until non-whitespace text is encountered. diff --git a/ink-engine-runtime/JsonSerialisation.cs b/ink-engine-runtime/JsonSerialisation.cs index b6ca4ab3..5629475f 100644 --- a/ink-engine-runtime/JsonSerialisation.cs +++ b/ink-engine-runtime/JsonSerialisation.cs @@ -286,17 +286,22 @@ public static Dictionary JObjectToIntDictionary(Dictionary", "G<", "G>" + // Glue: "<>" // // ControlCommand: "ev", "out", "/ev", "du" "pop", "->->", "~ret", "str", "/str", "nop", - // "choiceCnt", "turns", "visit", "seq", "thread", "done", "end" + // "choiceCnt", "turns", "visit", "seq", "thread" "turn", "readc", "rnd", + // "srnd", "listInt", "range", "lrnd", "#", "/#", "done", "end" // - // NativeFunction: "+", "-", "/", "*", "%" "~", "==", ">", "<", ">=", "<=", "!=", "!"... etc + // NativeFunction: "+", "-", "/", "*", "%" "_", "==", ">", "<", ">=", "<=", "!=", "!", + // "&&", "||", "MIN", "MAX", "POW", "FLOOR", "CEILING", "INT", "FLOAT", + // "?", "!?", "L^", "LIST_MIN", "LIST_MAX", "LIST_ALL", "LIST_COUNT", + // "LIST_VALUE", "LIST_INVERT" // // Void: "void" // // Value: "^string value", "^^string value beginning with ^" // 5, 5.2 + // true, false // {"^->": "path.target"} // {"^var": "varname", "ci": 0} // @@ -328,6 +333,9 @@ public static Dictionary JObjectToIntDictionary(Dictionary