Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/inkle/ink
Browse files Browse the repository at this point in the history
  • Loading branch information
joningold committed Nov 14, 2018
2 parents 27a7d49 + 347eaaa commit 1297771
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 45 deletions.
6 changes: 3 additions & 3 deletions Documentation/WritingWithInk.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ You can't use too much glue: multiple glues next to each other have no additiona

Combining knots, options and diverts gives us the basic structure of a choose-your-own game.

== paragraph_1 ===
=== paragraph_1 ===
You stand by the wall of Analand, sword in hand.
* [Open the gate] -> paragraph_2
* [Smash down the gate] -> paragraph_3
Expand Down Expand Up @@ -1790,7 +1790,7 @@ But this flat structure makes certain things difficult: for example, imagine a g
=== outside_honolulu ===
We arrived at the large island of Honolulu.
- (postscript)
{crossing_the_date_line(-> done)}
-> crossing_the_date_line(-> done)
- (done)
-> END

Expand All @@ -1799,7 +1799,7 @@ But this flat structure makes certain things difficult: for example, imagine a g
=== outside_pitcairn_island ===
The boat sailed along the water towards the tiny island.
- (postscript)
{crossing_the_date_line(-> done)}
-> crossing_the_date_line(-> done)
- (done)
-> END

Expand Down
6 changes: 3 additions & 3 deletions build_for_inky.command
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ mkbundle --static --sdk /Library/Frameworks/Mono.framework/Versions/Current --de

cp inklecate_mac ../../../BuildForInky/
cp inklecate.exe ../../../BuildForInky/inklecate_win.exe
cp inklecate.exe.mdb ../../../BuildForInky
cp inklecate.pdb ../../../BuildForInky
cp ink-engine-runtime.dll ../../../BuildForInky
cp ink-engine-runtime.dll.mdb ../../../BuildForInky
cp ink-engine-runtime.pdb ../../../BuildForInky
cp ink_compiler.dll ../../../BuildForInky
cp ink_compiler.dll.mdb ../../../BuildForInky
cp ink_compiler.pdb ../../../BuildForInky
3 changes: 3 additions & 0 deletions compiler/InkParser/InkParser_Logic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ protected Parsed.Object LogicLine()

var result = Expect(afterTilda, "expression after '~'", recoveryRule: SkipToNextLine) as Parsed.Object;

// Prevent further errors, already reported expected expression and have skipped to next line.
if (result == null) return new ContentList();

// Parse all expressions, but tell the writer off if they did something useless like:
// ~ 5 + 4
// And even:
Expand Down
12 changes: 9 additions & 3 deletions compiler/ParsedHierarchy/Object.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,15 @@ public T AddContent<T>(T subContent) where T : Parsed.Object
if (content == null) {
content = new List<Parsed.Object> ();
}

subContent.parent = this;
content.Add (subContent);

// Make resilient to content not existing, which can happen
// in the case of parse errors where we've already reported
// an error but still want a valid structure so we can
// carry on parsing.
if( subContent ) {
subContent.parent = this;
content.Add(subContent);
}

return subContent;
}
Expand Down
156 changes: 131 additions & 25 deletions compiler/ParsedHierarchy/Weave.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,6 @@ public override Runtime.Object GenerateRuntimeObject ()
else {
AddGeneralRuntimeContent (obj.runtimeObject);
}

// Keep track of nested choices within this (possibly complex) object,
// so that the next Gather knows whether to auto-enter
// (it auto-enters when there are no choices)
var innerChoices = obj.FindAll<Choice> ();
if (innerChoices.Count > 0)
hasSeenChoiceInSection = true;

}
}

Expand Down Expand Up @@ -357,25 +349,123 @@ void AddGeneralRuntimeContent(Runtime.Object content)

void PassLooseEndsToAncestors()
{
if (looseEnds.Count > 0) {
if (looseEnds.Count == 0) return;

// Search for Weave ancestor to pass loose ends to for gathering.
// There are two types depending on whether the current weave
// is separated by a conditional or sequence.
// - An "inner" weave is one that is directly connected to the current
// weave - i.e. you don't have to pass through a conditional or
// sequence to get to it. We're allowed to pass all loose ends to
// one of these.
// - An "outer" weave is one that is outside of a conditional/sequence
// that the current weave is nested within. We're only allowed to
// pass gathers (i.e. 'normal flow') loose ends up there, not normal
// choices. The rule is that choices have to be diverted explicitly
// by the author since it's ambiguous where flow should go otherwise.
//
// e.g.:
//
// - top <- e.g. outer weave
// {true:
// * choice <- e.g. inner weave
// * * choice 2
// more content <- e.g. current weave
// * choice 2
// }
// - more of outer weave
//
Weave closestInnerWeaveAncestor = null;
Weave closestOuterWeaveAncestor = null;

// Find inner and outer ancestor weaves as defined above.
bool nested = false;
for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
{

// Found ancestor?
var weaveAncestor = ancestor as Weave;
if (weaveAncestor != null)
{
if (!nested && closestInnerWeaveAncestor == null)
closestInnerWeaveAncestor = weaveAncestor;

if (nested && closestOuterWeaveAncestor == null)
closestOuterWeaveAncestor = weaveAncestor;
}


// Weaves nested within Sequences or Conditionals are
// "sealed" - any loose ends require explicit diverts.
if (ancestor is Sequence || ancestor is Conditional)
nested = true;
}

// No weave to pass loose ends to at all?
if (closestInnerWeaveAncestor == null && closestOuterWeaveAncestor == null)
return;

// Follow loose end passing logic as defined above
for (int i = looseEnds.Count - 1; i >= 0; i--) {
var looseEnd = looseEnds[i];

bool received = false;

var weaveAncestor = closestWeaveAncestor;
if (weaveAncestor) {
weaveAncestor.ReceiveLooseEnds (looseEnds);
looseEnds = null;
// This weave is nested within a conditional or sequence:
// - choices can only be passed up to direct ancestor ("inner") weaves
// - gathers can be passed up to either, but favour the closer (inner) weave
// if there is one
if(nested) {
if( looseEnd is Choice && closestInnerWeaveAncestor != null) {
closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
received = true;
}

else if( !(looseEnd is Choice) ) {
var receivingWeave = closestInnerWeaveAncestor ?? closestOuterWeaveAncestor;
if(receivingWeave != null) {
receivingWeave.ReceiveLooseEnd(looseEnd);
received = true;
}
}
}

// No nesting, all loose ends can be safely passed up
else {
closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
received = true;
}

if(received) looseEnds.RemoveAt(i);
}
}

public void ReceiveLooseEnds(List<IWeavePoint> childWeaveLooseEnds)
void ReceiveLooseEnd(IWeavePoint childWeaveLooseEnd)
{
looseEnds.AddRange (childWeaveLooseEnds);
looseEnds.Add(childWeaveLooseEnd);
}

public override void ResolveReferences(Story context)
{
base.ResolveReferences (context);

// Check that choices nested within conditionals and sequences are terminated
if( looseEnds != null && looseEnds.Count > 0 ) {
var isNestedWeave = false;
for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
{
if (ancestor is Sequence || ancestor is Conditional)
{
isNestedWeave = true;
break;
}
}
if (isNestedWeave)
{
ValidateTermination(BadNestedTerminationHandler);
}
}

foreach(var gatherPoint in gatherPointsToResolve) {
gatherPoint.divert.targetPath = gatherPoint.targetRuntimeObj.path;
}
Expand Down Expand Up @@ -496,6 +586,32 @@ public void ValidateTermination (BadTerminationHandler badTerminationHandler)
}
}

void BadNestedTerminationHandler(Parsed.Object terminatingObj)
{
Conditional conditional = null;
for (var ancestor = terminatingObj.parent; ancestor != null; ancestor = ancestor.parent) {
if( ancestor is Sequence || ancestor is Conditional ) {
conditional = ancestor as Conditional;
break;
}
}

var errorMsg = "Choices nested in conditionals or sequences need to explicitly divert afterwards.";

// Tutorialise proper choice syntax if this looks like a single choice within a condition, e.g.
// { condition:
// * choice
// }
if (conditional != null) {
var numChoices = conditional.FindAll<Choice>().Count;
if( numChoices == 1 ) {
errorMsg = "Choices with conditions should be written: '* {condition} choice'. Otherwise, "+ errorMsg.ToLower();
}
}

Error(errorMsg, terminatingObj);
}

void ValidateFlowOfObjectsTerminates (IEnumerable<Parsed.Object> objFlow, Parsed.Object defaultObj, BadTerminationHandler badTerminationHandler)
{
bool terminated = false;
Expand Down Expand Up @@ -527,16 +643,6 @@ void ValidateFlowOfObjectsTerminates (IEnumerable<Parsed.Object> objFlow, Parsed
badTerminationHandler (terminatingObj);
}
}

Weave closestWeaveAncestor {
get {
var ancestor = this.parent;
while (ancestor && !(ancestor is Weave)) {
ancestor = ancestor.parent;
}
return (Weave)ancestor;
}
}

bool WeavePointHasLooseEnd(IWeavePoint weavePoint)
{
Expand Down
4 changes: 4 additions & 0 deletions ink-engine-runtime/Story.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,10 @@ bool TryFollowDefaultInvisibleChoice()

var choice = invisibleChoices [0];

// Invisible choice may have been generated on a different thread,
// in which case we need to restore it before we continue
state.callStack.currentThread = choice.threadAtGeneration;

ChoosePath (choice.targetPath, incrementingTurnIndex: false);

return true;
Expand Down
53 changes: 42 additions & 11 deletions tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ public void TestConditionalChoiceInWeave()
{
- true: * [go to a stitch] -> a_stitch
}
- gather shouldn't be seen
-> END
- gather should be seen
-> DONE
= a_stitch
result
Expand All @@ -366,9 +366,7 @@ public void TestConditionalChoiceInWeave()

Story story = CompileString(storyStr);

// Extra newline is because there's a choice object sandwiched there,
// so it can't be absorbed :-/
Assert.AreEqual("start\n", story.Continue());
Assert.AreEqual("start\ngather should be seen\n", story.ContinueMaximally());
Assert.AreEqual(1, story.currentChoices.Count);

story.ChooseChoiceIndex(0);
Expand All @@ -382,14 +380,13 @@ public void TestConditionalChoiceInWeave2()
var storyStr =
@"
- first gather
* option 1
* option 2
* [option 1]
* [option 2]
- the main gather
{false:
* unreachable option
* unreachable option -> END
}
- unrechable gather
";
- bottom gather";

Story story = CompileString(storyStr);

Expand All @@ -399,7 +396,7 @@ public void TestConditionalChoiceInWeave2()

story.ChooseChoiceIndex(0);

Assert.AreEqual("option 1\nthe main gather\n", story.ContinueMaximally());
Assert.AreEqual("the main gather\nbottom gather\n", story.ContinueMaximally());
Assert.AreEqual(0, story.currentChoices.Count);
}

Expand Down Expand Up @@ -2854,6 +2851,7 @@ public void TestWeaveWithinSequence ()
{ shuffle:
- * choice
nextline
-> END
}
";
var story = CompileString (storyStr);
Expand All @@ -2868,6 +2866,20 @@ public void TestWeaveWithinSequence ()
}


[Test()]
public void TestNestedChoiceError()
{
var storyStr =
@"
{ true:
* choice
}
";
CompileString(storyStr, testingErrors:true);
Assert.IsTrue(HadError("need to explicitly divert"));
}


[Test ()]
public void TestStitchNamingCollision ()
{
Expand Down Expand Up @@ -3627,6 +3639,25 @@ public void TestChoiceThreadForking()
Assert.IsFalse(story.hasWarning);
}


[Test()]
public void TestFallbackChoiceOnThread()
{
var storyStr =
@"
<- knot
== knot
~ temp x = 1
* ->
Should be 1 not 0: {x}.
-> DONE
";

var story = CompileString(storyStr);
Assert.AreEqual("Should be 1 not 0: 1.\n", story.Continue());
}

// Helper compile function
protected Story CompileString(string str, bool countAllVisits = false, bool testingErrors = false)
{
Expand Down

0 comments on commit 1297771

Please sign in to comment.