-
-
Notifications
You must be signed in to change notification settings - Fork 18
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.
TODO: Simplify this. We're just re-explaining what we explain below.
InkCPP preserves the concept of containers from InkJSON because the ink runtime expects to be able to ask, for a given container, how many times it has been visited via the visits
command (turns
is similar).
One way to preserve this would be to simply insert a new, InkCPP exclusive START_CONTAINER_MARKER
command at the beginning of each container which incremented its visit count. Unfortunately, the Ink runtime counts not just entering a container as a visit, but also jumping anywhere inside a container. Such a jump would skip right over the START_CONTAINER_MARKER
command.
We could compensate for that by scanning all commands from the start of the jump to the jump's destination and test for START_CONTAINER commands, but since each InkCPP command has a different size (depending on its arguments), this would involve quite a bit of logic that would need to be updated anytime the command sizes update, and it's also just a lot of iteration in general for what should be a fast command.
InkCPP compromises with two approaches. One is with new START_CONTAINER_MARKER
and END_CONTAINER_MARKER
commands. The former increments the visit count for the container and the last visited turn count, the latter does some bookkeeping to keep track of which containers we're currently in (and executes a DONE command if we are actually no longer in a container).
For jumps, the InkCPP consults the "Container Map" stored in the compiled InkCPP binary file. This data structure maps each container to a start and end point in the instruction block. A jump can thus iterate this map in order, starting from the current instruction pointer all the way to the destination, and quickly discover which containers it is leaving and entering and update their visit counts accordingly.
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.