Skip to content

Commit d0d70be

Browse files
committed
Match features with C# Ink v1.2.0
1 parent ea3efb5 commit d0d70be

File tree

13 files changed

+234
-62
lines changed

13 files changed

+234
-62
lines changed

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
# Matching Ink v1.1.1
2-
version=1.1.2
1+
# Matching Ink v1.2.0
2+
version=1.2.0
33

src/main/java/com/bladecoder/ink/runtime/CallStack.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,24 @@ public Thread(HashMap<String, Object> jThreadObj, Story storyContext) throws Exc
7979
pointer.container = threadPointerResult.getContainer();
8080
pointer.index = (int) jElementObj.get("idx");
8181

82-
if (threadPointerResult.obj == null)
82+
if (threadPointerResult.obj == null) {
8383
throw new Exception("When loading state, internal story location couldn't be found: "
8484
+ currentContainerPathStr
8585
+ ". Has the story changed since this save data was created?");
86-
else if (threadPointerResult.approximate)
87-
storyContext.warning("When loading state, exact internal story location couldn't be found: '"
88-
+ currentContainerPathStr + "', so it was approximated to '"
89-
+ pointer.container.getPath().toString()
90-
+ "' to recover. Has the story changed since this save data was created?");
86+
} else if (threadPointerResult.approximate) {
87+
if (pointer.container != null) {
88+
storyContext.warning(
89+
"When loading state, exact internal story location couldn't be found: '"
90+
+ currentContainerPathStr + "', so it was approximated to '"
91+
+ pointer.container.getPath().toString()
92+
+ "' to recover. Has the story changed since this save data was created?");
93+
} else {
94+
storyContext.warning(
95+
"When loading state, exact internal story location couldn't be found: '"
96+
+ currentContainerPathStr
97+
+ "' and it may not be recoverable. Has the story changed since this save data was created?");
98+
}
99+
}
91100
}
92101

93102
boolean inExpressionEvaluation = (boolean) jElementObj.get("exp");
@@ -282,6 +291,7 @@ public RTObject getTemporaryVariableWithName(String name) {
282291

283292
// Get variable value, dereferencing a variable pointer if necessary
284293
public RTObject getTemporaryVariableWithName(String name, int contextIndex) {
294+
// contextIndex 0 means global, so index is actually 1-based
285295
if (contextIndex == -1) contextIndex = getCurrentElementIndex() + 1;
286296

287297
Element contextElement = getCallStack().get(contextIndex - 1);

src/main/java/com/bladecoder/ink/runtime/Choice.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,16 @@ public void setText(String value) {
7272
public void setThreadAtGeneration(CallStack.Thread value) {
7373
threadAtGeneration = value;
7474
}
75+
76+
public Choice clone() {
77+
Choice copy = new Choice();
78+
copy.text = text;
79+
copy.sourcePath = sourcePath;
80+
copy.index = index;
81+
copy.targetPath = targetPath;
82+
copy.originalThreadIndex = originalThreadIndex;
83+
copy.isInvisibleDefault = isInvisibleDefault;
84+
if (threadAtGeneration != null) copy.threadAtGeneration = threadAtGeneration.copy();
85+
return copy;
86+
}
7587
}

src/main/java/com/bladecoder/ink/runtime/Container.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,18 @@ public SearchResult contentAtPath(Path path, int partialPathStart, int partialPa
224224
break;
225225
}
226226

227+
// Are we about to loop into another container?
228+
// Is the object a container as expected? It might
229+
// no longer be if the content has shuffled around, so what
230+
// was originally a container no longer is.
231+
Container nextContainer = foundObj instanceof Container ? (Container) foundObj : null;
232+
if (i < partialPathLength - 1 && nextContainer == null) {
233+
result.approximate = true;
234+
break;
235+
}
236+
227237
currentObj = foundObj;
228-
currentContainer = foundObj instanceof Container ? (Container) foundObj : null;
238+
currentContainer = nextContainer;
229239
}
230240

231241
result.obj = currentObj;

src/main/java/com/bladecoder/ink/runtime/InkList.java

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ public InkList(InkList otherList) {
7070
/**
7171
* Converts a string to an ink list and returns for use in the story.
7272
*/
73-
public static InkList FromString(String myListItem, Story originStory) throws Exception {
73+
public static InkList fromString(String myListItem, Story originStory) throws Exception {
74+
if (myListItem == null || myListItem.isEmpty()) return new InkList();
75+
7476
ListValue listValue = originStory.getListDefinitions().findSingleItemListWithName(myListItem);
7577
if (listValue != null) return new InkList(listValue.value);
7678
else
@@ -321,7 +323,7 @@ public InkList listWithSubRange(Object minBound, Object maxBound) throws StoryEx
321323

322324
if (maxBound instanceof Integer) maxValue = (int) maxBound;
323325
else {
324-
if (minBound instanceof InkList && ((InkList) minBound).size() > 0)
326+
if (maxBound instanceof InkList && ((InkList) maxBound).size() > 0)
325327
maxValue = ((InkList) maxBound).getMaxItem().getValue();
326328
}
327329

@@ -355,6 +357,17 @@ public String getSingleOriginListName() {
355357
return name;
356358
}
357359

360+
/**
361+
* If you have an InkList that's known to have one single item, this is a convenient way to get it.
362+
*/
363+
public InkListItem getSingleItem() {
364+
for (Map.Entry<InkListItem, Integer> item : this.entrySet()) {
365+
return item.getKey();
366+
}
367+
368+
return null;
369+
}
370+
358371
/**
359372
* The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink
360373
*/
@@ -398,7 +411,8 @@ public InkList getAll() {
398411
/**
399412
* Adds the given item to the ink list. Note that the item must come from a list
400413
* definition that is already "known" to this list, so that the item's value can
401-
* be looked up. By "known", we mean that it already has items in it from that
414+
* be looked up.
415+
* By "known", we mean that it already has items in it from that
402416
* source, or it did at one point - it can't be a completely fresh empty list,
403417
* or a list that only contains items from a different list definition.
404418
*
@@ -438,39 +452,53 @@ public void addItem(InkListItem item) throws Exception {
438452
* be looked up. By "known", we mean that it already has items in it from that
439453
* source, or it did at one point - it can't be a completely fresh empty list,
440454
* or a list that only contains items from a different list definition.
455+
* You can also provide the Story object, so in the case of an unknown element, it can be created fresh.
441456
*
442457
* @throws Exception
443458
*/
444-
public void addItem(String itemName) throws Exception {
459+
public void addItem(String itemName, Story storyObject) throws Exception {
445460
ListDefinition foundListDef = null;
446461

447-
for (ListDefinition origin : origins) {
448-
if (origin.containsItemWithName(itemName)) {
449-
if (foundListDef != null) {
450-
throw new Exception(
451-
"Could not add the item " + itemName + " to this list because it could come from either "
452-
+ origin.getName() + " or " + foundListDef.getName());
453-
} else {
454-
foundListDef = origin;
462+
if (origins != null) {
463+
for (ListDefinition origin : origins) {
464+
if (origin.containsItemWithName(itemName)) {
465+
if (foundListDef != null) {
466+
throw new Exception("Could not add the item " + itemName
467+
+ " to this list because it could come from either " + origin.getName() + " or "
468+
+ foundListDef.getName());
469+
} else {
470+
foundListDef = origin;
471+
}
455472
}
456473
}
457474
}
458475

459-
if (foundListDef == null)
460-
throw new Exception("Could not add the item " + itemName
461-
+ " to this list because it isn't known to any list definitions previously associated with this "
462-
+ "list.");
476+
if (foundListDef == null) {
477+
if (storyObject == null) {
478+
throw new Exception("Could not add the item " + itemName
479+
+ " to this list because it isn't known to any list definitions previously associated with this "
480+
+ "list.");
481+
} else {
482+
Entry<InkListItem, Integer> newItem =
483+
fromString(itemName, storyObject).getOrderedItems().get(0);
484+
this.put(newItem.getKey(), newItem.getValue());
485+
}
486+
} else {
487+
InkListItem item = new InkListItem(foundListDef.getName(), itemName);
488+
Integer itemVal = foundListDef.getValueForItem(item);
489+
this.put(item, itemVal != null ? itemVal : 0);
490+
}
491+
}
463492

464-
InkListItem item = new InkListItem(foundListDef.getName(), itemName);
465-
Integer itemVal = foundListDef.getValueForItem(item);
466-
this.put(item, itemVal != null ? itemVal : 0);
493+
public void addItem(String itemName) throws Exception {
494+
addItem(itemName, null);
467495
}
468496

469497
/**
470498
* Returns true if this ink list contains an item with the given short name
471499
* (ignoring the original list where it was defined).
472500
*/
473-
public boolean ContainsItemNamed(String itemName) {
501+
public boolean containsItemNamed(String itemName) {
474502
for (Map.Entry<InkListItem, Integer> itemWithValue : this.entrySet()) {
475503
if (itemWithValue.getKey().getItemName().equals(itemName)) return true;
476504
}

src/main/java/com/bladecoder/ink/runtime/Json.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,19 +596,45 @@ static Choice jObjectToChoice(HashMap<String, Object> jObj) throws Exception {
596596
choice.sourcePath = jObj.get("originalChoicePath").toString();
597597
choice.originalThreadIndex = (int) jObj.get("originalThreadIndex");
598598
choice.setPathStringOnChoice(jObj.get("targetPath").toString());
599+
choice.tags = jArrayToTags(jObj, choice);
599600
return choice;
600601
}
601602

603+
private static List<String> jArrayToTags(HashMap<String, Object> jObj, Choice choice) {
604+
Object jArray = jObj.get("tags");
605+
if (jArray == null) return null;
606+
607+
List<String> tags = new ArrayList<>();
608+
for (Object stringValue : (List<Object>) jArray) {
609+
tags.add(stringValue.toString());
610+
}
611+
612+
return tags;
613+
}
614+
602615
public static void writeChoice(SimpleJson.Writer writer, Choice choice) throws Exception {
603616
writer.writeObjectStart();
604617
writer.writeProperty("text", choice.getText());
605618
writer.writeProperty("index", choice.getIndex());
606619
writer.writeProperty("originalChoicePath", choice.sourcePath);
607620
writer.writeProperty("originalThreadIndex", choice.originalThreadIndex);
608621
writer.writeProperty("targetPath", choice.getPathStringOnChoice());
622+
writeChoiceTags(writer, choice);
609623
writer.writeObjectEnd();
610624
}
611625

626+
private static void writeChoiceTags(SimpleJson.Writer writer, Choice choice) throws Exception {
627+
if (choice.tags == null || choice.tags.isEmpty()) return;
628+
writer.writePropertyStart("tags");
629+
writer.writeArrayStart();
630+
for (String tag : choice.tags) {
631+
writer.write(tag);
632+
}
633+
634+
writer.writeArrayEnd();
635+
writer.writePropertyEnd();
636+
}
637+
612638
static void writeInkList(SimpleJson.Writer writer, ListValue listVal) throws Exception {
613639
InkList rawList = listVal.getValue();
614640

src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public List<ListDefinition> getLists() {
4545
ListValue findSingleItemListWithName(String name) {
4646
ListValue val = null;
4747

48-
val = allUnambiguousListValueCache.get(name);
48+
if (name != null && !name.trim().isEmpty()) val = allUnambiguousListValueCache.get(name);
4949

5050
return val;
5151
}

src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,8 @@ public RTObject call(List<RTObject> parameters) throws Exception {
670670

671671
for (RTObject p : parameters) {
672672
if (p instanceof Void)
673-
throw new StoryException(
674-
"Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?");
673+
throw new StoryException("Attempting to perform " + this.name
674+
+ " on a void value. Did you forget to 'return' a value from a function you called here?");
675675

676676
if (p instanceof ListValue) hasList = true;
677677
}

src/main/java/com/bladecoder/ink/runtime/SimpleJson.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,16 @@ public void writeObject(InnerWriter inner) throws Exception {
268268
writeObjectEnd();
269269
}
270270

271+
public void clear() {
272+
StringWriter stringWriter = writer instanceof StringWriter ? (StringWriter) writer : null;
273+
if (stringWriter == null) {
274+
throw new UnsupportedOperationException(
275+
"Writer.Clear() is only supported for the StringWriter variant, not for streams");
276+
}
277+
278+
stringWriter.getBuffer().setLength(0);
279+
}
280+
271281
public void writeObjectStart() throws Exception {
272282
startNewObject(true);
273283
stateStack.push(new StateElement(State.Object, 0));
@@ -278,6 +288,7 @@ public void writeObjectEnd() throws Exception {
278288
Assert(getState() == State.Object);
279289
writer.write("}");
280290
stateStack.pop();
291+
if (getState() == State.None) writer.flush();
281292
}
282293

283294
public void writeProperty(String name, InnerWriter inner) throws Exception {

src/main/java/com/bladecoder/ink/runtime/Story.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,36 @@ void callExternalFunction(String funcName, int numberOfArguments) throws Excepti
505505

506506
funcDef = externals.get(funcName);
507507

508+
if (funcDef != null && funcDef.lookaheadSafe && state.inStringEvaluation()) {
509+
// 16th Jan 2023: Example ink that was failing:
510+
//
511+
// A line above
512+
// ~ temp text = "{theFunc()}"
513+
// {text}
514+
//
515+
// === function theFunc()
516+
// { external():
517+
// Boom
518+
// }
519+
//
520+
// EXTERNAL external()
521+
//
522+
// What was happening: The external() call would exit out early due to
523+
// _stateSnapshotAtLastNewline having a value, leaving the evaluation stack
524+
// without a return value on it. When the if-statement tried to pop a value,
525+
// the evaluation stack would be empty, and there would be an exception.
526+
//
527+
// The snapshot rewinding code is only designed to work when outside of
528+
// string generation code (there's a check for that in the snapshot rewinding code),
529+
// hence these things are incompatible, you can't have unsafe functions that
530+
// cause snapshot rewinding in the middle of string generation.
531+
//
532+
error("External function " + funcName
533+
+ " could not be called because 1) it wasn't marked as lookaheadSafe when BindExternalFunction was called and 2) the story is in the middle of string generation, either because choice text is being generated, or because you have ink like \"hello {func()}\". You can work around this by generating the result of your function into a temporary variable before the string or choice gets generated: ~ temp x = "
534+
+ funcName + "()");
535+
return;
536+
}
537+
508538
// Should this function break glue? Abort run if we've already seen a newline.
509539
// Set a bool to tell it to restore the snapshot at the end of this instruction.
510540
if (funcDef != null && !funcDef.lookaheadSafe && stateSnapshotAtLastNewline != null) {
@@ -801,7 +831,9 @@ void continueInternal(float millisecsLimitAsync) throws Exception {
801831
// It's possible for ink to call game to call ink to call game etc
802832
// In this case, we only want to batch observe variable changes
803833
// for the outermost call.
804-
if (recursiveContinueCount == 1) state.getVariablesState().setbatchObservingVariableChanges(true);
834+
if (recursiveContinueCount == 1) state.getVariablesState().startVariableObservation();
835+
} else if (asyncContinueActive && !isAsyncTimeLimited) {
836+
asyncContinueActive = false;
805837
}
806838

807839
// Start timing
@@ -830,6 +862,8 @@ void continueInternal(float millisecsLimitAsync) throws Exception {
830862

831863
durationStopwatch.stop();
832864

865+
HashMap<String, RTObject> changedVariablesToObserve = null;
866+
833867
// 4 outcomes:
834868
// - got newline (so finished this line of text)
835869
// - can't continue (e.g. choices or ending)
@@ -863,7 +897,8 @@ else if (!state.getCallStack().canPop())
863897
state.setDidSafeExit(false);
864898
sawLookaheadUnsafeFunctionAfterNewline = false;
865899

866-
if (recursiveContinueCount == 1) state.getVariablesState().setbatchObservingVariableChanges(false);
900+
if (recursiveContinueCount == 1)
901+
changedVariablesToObserve = state.getVariablesState().completeVariableObservation();
867902
asyncContinueActive = false;
868903
}
869904

@@ -925,6 +960,11 @@ else if (!state.getCallStack().canPop())
925960
throw new StoryException(sb.toString());
926961
}
927962
}
963+
964+
// Send out variable observation events at the last second, since it might trigger new ink to be run
965+
if (changedVariablesToObserve != null && changedVariablesToObserve.size() > 0) {
966+
state.getVariablesState().notifyObservers(changedVariablesToObserve);
967+
}
928968
}
929969

930970
boolean continueSingleStep() throws Exception {
@@ -2802,7 +2842,7 @@ public Object evaluateFunction(String functionName, StringBuilder textOutput, Ob
28022842
// - _state (current, being patched)
28032843
void stateSnapshot() {
28042844
stateSnapshotAtLastNewline = state;
2805-
state = state.copyAndStartPatching();
2845+
state = state.copyAndStartPatching(false);
28062846
}
28072847

28082848
void restoreStateSnapshot() {
@@ -2853,7 +2893,7 @@ public StoryState copyStateForBackgroundThreadSave() throws Exception {
28532893
throw new Exception(
28542894
"Story is already in background saving mode, can't call CopyStateForBackgroundThreadSave again!");
28552895
StoryState stateToSave = state;
2856-
state = state.copyAndStartPatching();
2896+
state = state.copyAndStartPatching(true);
28572897
asyncSaving = true;
28582898
return stateToSave;
28592899
}

0 commit comments

Comments
 (0)