-
-
Notifications
You must be signed in to change notification settings - Fork 16
Containers
Containers are an organization unit used by Inkle's compiled JSON format. You should read their documentation on containers first before tackling this document. InkCPP preserves the concept of containers from InkJSON but without their hierarchical structure.
Containers are a hierarchical organizational grouping of instructions in InkJSON. If you are familiar with the Ink language, but not their compiled JSON format, you won't have heard of this concept before. They are used to group instructions, provide labels for jumps, and track visit counts.
Each InkJSON file starts with a root container, of which every other container is some descendant.
Each container has:
- Flags, which describe how and if they should track visit and turn counts when they are entered.
- Instructions which are executed sequentially in the containe
- Inline child containers intermixed with their instructions. They are entered and executed in the course of executing the container
- (Optionally) A name to identify it in jump commands
- (Optionally) Named children, which are child containers that are not automatically executed but are named, and thus can be used in jumps in this container.
InkCPP almost entirely eliminates the concept of containers when compiling InkJSON into InkBIN. The instructions from all containers are amalgamated into one big, sequential instruction block with extra jumps inserted to simulate the flow behaviour of the original container structure.
However, features like visit counting and user requested jumps to containers by name require some information about the original container structure in order to function. We need to somehow still know
- Which containers we are currently in (stored in a container ID stack where the top element is the deepest container we are currently executing)
- What containers we are entering and exiting anytime we jump, and
- Where in the instruction block each container starts
These are accomplished with three features:
- Surrounding each container's instructions with a
START_CONTAINER_MARKER
instruction and aEND_CONTAINER_MARKER
instruction. - Storing a Container Instruction Map which marks the start and end instruction offsets of each container, and
- Storing a Container Hash Map which maps the hashes of each container's fully resolved name to the offset of their first instruction
See the InkBIN File Structure for more information on where these are in the compiled InkBin file. For more information on the structures themselves, keep reading here.
To compile a JSON container into binary...
- Write a
START_CONTAINER_MARKER
command to signal the beginning of a container - For each child element...
- If it's a command, compile it into its binary form (see InkCPP Commands).
- If it's an inline container, recurse to compile it in-place.
- Write out a
DIVERT
command with theDIVERT_IS_FALLTHROUGH
flag which jumps to the finalEND_CONTAINER_MARKER
written below. - For each named child container in this container's metadata (again, see the InkJSON documentation)...
- Recurse to compile it in-place
- Write out a
DIVERT
command with theDIVERT_IS_FALLTHROUGH
flag which jumps to the finalEND_CONTAINER_MARKER
written below.
- Write out an
END_CONTAINER_MARKER
command. This instruction is where the above.
The special fall-through diverts written out above are meant to mimic InkJSON container behavior even though the entire hierarchy has been flattened. For example, if you get to the end of a container, you don't want to start executing its first child. Similarly, if you get to the end of a container's child container, you don't want the interpreter to begin executing its sibling. The diverts are sandwiched between each child and just before the child list to move the instruction pointer to the end of the container.
Imagine an InkJSON container hierarchy like this
- Container Beginning
- Container Beginning.AteApple
- Container Beginning.RefusedApple
- Container Beginning.PrayedToApple
- Container NextStoryBeat
When we flatten this hierarchy, we get this
- Container Beginning
- Container Beginning.AteApple
- Container Beginning.RefusedApple
- Container Beginning.PrayedToApple
- Container NextStoryBeat
If we were to enter the Beginning.RefusedApple container and reached its end, the next instruction would naturally be the first instruction of the Beginning.PrayedApple container. Looking back to the hierarchy, we'd actually want to fall into NextStoryBeat. Adding the diverts using the compiling container algorithm above, we get
- Container Beginning
DIVERT TO NextStoryBeat
- Container Beginning.AteApple
DIVERT TO NextStoryBeat
- Container Beginning.RefusedApple
DIVERT TO NextStoryBeat
- Container Beginning.PrayedToApple
DIVERT TO NextStoryBeat
- Container NextStoryBeat
They're called "fallthrough" diverts in InkCPP because they represent the interpreter "falling out" of the end of a container and dropping down to the next container.
There's a special quirk concerning choices and falling through containers as above. In Ink, choices are added by CHOICE
commands until a DONE
is reached and the choices are displayed to the player. However, there is a special case: no DONE
is required if, after the last choice, we simply run out of content. The DONE
in this case is implied. However, in order to keep accurate count of which container we're in and which we've visited, we need to execute the JUMP triggered by selecting a choice from the point where the choices were done being collected. If there was an explicit DONE
, that's just the position of that DONE
command. In the implicit case, it's a little more tricky. We use the first point at which we started falling out of containers before running out of content.
The special DIVERT_IS_FALLTHROUGH
flag just records the position of the current instruction in case its required for this purpose. However, the moment we hit any instruction that isn't a DIVERT_IS_FALLTHROUGH
DIVERT
, this value is cleared since are clearly not out of content.
OPTIMIZATION: This fallthrough flag is also used to optimize the Container Map algorithm detailed below.
WARNING: There may be another reason I added this that I can't figure out because I have honestly forgotten and it's a bit complicated. But those seem to be the only two places the _is_falling
boolean in runner_impl
is referenced in code.
In order to implement the VISITS
command, we not only need to record how many times each container is visited, but also the current container. This is partly accomplished using the START_CONTAINER_MARKER
and END_CONTAINER_MARKER
commands, but Ink considers any entry into a container, even if it's jumping into one of its children from a completely different container, to be a visit. We need to be able to figure out which containers we are leaving and entering anytime we make a jump.
This is accomplished with the container map, a data block in the InkCPP Binary File. For each container, it stores the offset of its first instruction and last instruction. These markers are sorted by instruction offset. To find out what containers you are entering and exiting as a result of a jump, you simply iterate this map starting from the current instruction pointer to the destination and mark visits appropriately.
This algorithm is implemented in runner_impl::jump
in runner_impl.cpp.
For more details on how the container map is stored, see the relevant section in the Ink CPP Binary File structure document.