Skip to content

Commit

Permalink
Match features with C# Ink v1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
bladecoder committed Jun 25, 2024
1 parent ea3efb5 commit d0d70be
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 62 deletions.
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Matching Ink v1.1.1
version=1.1.2
# Matching Ink v1.2.0
version=1.2.0

22 changes: 16 additions & 6 deletions src/main/java/com/bladecoder/ink/runtime/CallStack.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,24 @@ public Thread(HashMap<String, Object> jThreadObj, Story storyContext) throws Exc
pointer.container = threadPointerResult.getContainer();
pointer.index = (int) jElementObj.get("idx");

if (threadPointerResult.obj == null)
if (threadPointerResult.obj == null) {
throw new Exception("When loading state, internal story location couldn't be found: "
+ currentContainerPathStr
+ ". Has the story changed since this save data was created?");
else if (threadPointerResult.approximate)
storyContext.warning("When loading state, exact internal story location couldn't be found: '"
+ currentContainerPathStr + "', so it was approximated to '"
+ pointer.container.getPath().toString()
+ "' to recover. Has the story changed since this save data was created?");
} else if (threadPointerResult.approximate) {
if (pointer.container != null) {
storyContext.warning(
"When loading state, exact internal story location couldn't be found: '"
+ currentContainerPathStr + "', so it was approximated to '"
+ pointer.container.getPath().toString()
+ "' to recover. Has the story changed since this save data was created?");
} else {
storyContext.warning(
"When loading state, exact internal story location couldn't be found: '"
+ currentContainerPathStr
+ "' and it may not be recoverable. Has the story changed since this save data was created?");
}
}
}

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

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

Element contextElement = getCallStack().get(contextIndex - 1);
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/bladecoder/ink/runtime/Choice.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,16 @@ public void setText(String value) {
public void setThreadAtGeneration(CallStack.Thread value) {
threadAtGeneration = value;
}

public Choice clone() {
Choice copy = new Choice();
copy.text = text;
copy.sourcePath = sourcePath;
copy.index = index;
copy.targetPath = targetPath;
copy.originalThreadIndex = originalThreadIndex;
copy.isInvisibleDefault = isInvisibleDefault;
if (threadAtGeneration != null) copy.threadAtGeneration = threadAtGeneration.copy();
return copy;
}
}
12 changes: 11 additions & 1 deletion src/main/java/com/bladecoder/ink/runtime/Container.java
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,18 @@ public SearchResult contentAtPath(Path path, int partialPathStart, int partialPa
break;
}

// Are we about to loop into another container?
// Is the object a container as expected? It might
// no longer be if the content has shuffled around, so what
// was originally a container no longer is.
Container nextContainer = foundObj instanceof Container ? (Container) foundObj : null;
if (i < partialPathLength - 1 && nextContainer == null) {
result.approximate = true;
break;
}

currentObj = foundObj;
currentContainer = foundObj instanceof Container ? (Container) foundObj : null;
currentContainer = nextContainer;
}

result.obj = currentObj;
Expand Down
68 changes: 48 additions & 20 deletions src/main/java/com/bladecoder/ink/runtime/InkList.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ public InkList(InkList otherList) {
/**
* Converts a string to an ink list and returns for use in the story.
*/
public static InkList FromString(String myListItem, Story originStory) throws Exception {
public static InkList fromString(String myListItem, Story originStory) throws Exception {
if (myListItem == null || myListItem.isEmpty()) return new InkList();

ListValue listValue = originStory.getListDefinitions().findSingleItemListWithName(myListItem);
if (listValue != null) return new InkList(listValue.value);
else
Expand Down Expand Up @@ -321,7 +323,7 @@ public InkList listWithSubRange(Object minBound, Object maxBound) throws StoryEx

if (maxBound instanceof Integer) maxValue = (int) maxBound;
else {
if (minBound instanceof InkList && ((InkList) minBound).size() > 0)
if (maxBound instanceof InkList && ((InkList) maxBound).size() > 0)
maxValue = ((InkList) maxBound).getMaxItem().getValue();
}

Expand Down Expand Up @@ -355,6 +357,17 @@ public String getSingleOriginListName() {
return name;
}

/**
* If you have an InkList that's known to have one single item, this is a convenient way to get it.
*/
public InkListItem getSingleItem() {
for (Map.Entry<InkListItem, Integer> item : this.entrySet()) {
return item.getKey();
}

return null;
}

/**
* The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink
*/
Expand Down Expand Up @@ -398,7 +411,8 @@ public InkList getAll() {
/**
* Adds the given item to the ink list. Note that the item must come from a list
* definition that is already "known" to this list, so that the item's value can
* be looked up. By "known", we mean that it already has items in it from that
* be looked up.
* By "known", we mean that it already has items in it from that
* source, or it did at one point - it can't be a completely fresh empty list,
* or a list that only contains items from a different list definition.
*
Expand Down Expand Up @@ -438,39 +452,53 @@ public void addItem(InkListItem item) throws Exception {
* be looked up. By "known", we mean that it already has items in it from that
* source, or it did at one point - it can't be a completely fresh empty list,
* or a list that only contains items from a different list definition.
* You can also provide the Story object, so in the case of an unknown element, it can be created fresh.
*
* @throws Exception
*/
public void addItem(String itemName) throws Exception {
public void addItem(String itemName, Story storyObject) throws Exception {
ListDefinition foundListDef = null;

for (ListDefinition origin : origins) {
if (origin.containsItemWithName(itemName)) {
if (foundListDef != null) {
throw new Exception(
"Could not add the item " + itemName + " to this list because it could come from either "
+ origin.getName() + " or " + foundListDef.getName());
} else {
foundListDef = origin;
if (origins != null) {
for (ListDefinition origin : origins) {
if (origin.containsItemWithName(itemName)) {
if (foundListDef != null) {
throw new Exception("Could not add the item " + itemName
+ " to this list because it could come from either " + origin.getName() + " or "
+ foundListDef.getName());
} else {
foundListDef = origin;
}
}
}
}

if (foundListDef == null)
throw new Exception("Could not add the item " + itemName
+ " to this list because it isn't known to any list definitions previously associated with this "
+ "list.");
if (foundListDef == null) {
if (storyObject == null) {
throw new Exception("Could not add the item " + itemName
+ " to this list because it isn't known to any list definitions previously associated with this "
+ "list.");
} else {
Entry<InkListItem, Integer> newItem =
fromString(itemName, storyObject).getOrderedItems().get(0);
this.put(newItem.getKey(), newItem.getValue());
}
} else {
InkListItem item = new InkListItem(foundListDef.getName(), itemName);
Integer itemVal = foundListDef.getValueForItem(item);
this.put(item, itemVal != null ? itemVal : 0);
}
}

InkListItem item = new InkListItem(foundListDef.getName(), itemName);
Integer itemVal = foundListDef.getValueForItem(item);
this.put(item, itemVal != null ? itemVal : 0);
public void addItem(String itemName) throws Exception {
addItem(itemName, null);
}

/**
* Returns true if this ink list contains an item with the given short name
* (ignoring the original list where it was defined).
*/
public boolean ContainsItemNamed(String itemName) {
public boolean containsItemNamed(String itemName) {
for (Map.Entry<InkListItem, Integer> itemWithValue : this.entrySet()) {
if (itemWithValue.getKey().getItemName().equals(itemName)) return true;
}
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/bladecoder/ink/runtime/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -596,19 +596,45 @@ static Choice jObjectToChoice(HashMap<String, Object> jObj) throws Exception {
choice.sourcePath = jObj.get("originalChoicePath").toString();
choice.originalThreadIndex = (int) jObj.get("originalThreadIndex");
choice.setPathStringOnChoice(jObj.get("targetPath").toString());
choice.tags = jArrayToTags(jObj, choice);
return choice;
}

private static List<String> jArrayToTags(HashMap<String, Object> jObj, Choice choice) {
Object jArray = jObj.get("tags");
if (jArray == null) return null;

List<String> tags = new ArrayList<>();
for (Object stringValue : (List<Object>) jArray) {
tags.add(stringValue.toString());
}

return tags;
}

public static void writeChoice(SimpleJson.Writer writer, Choice choice) throws Exception {
writer.writeObjectStart();
writer.writeProperty("text", choice.getText());
writer.writeProperty("index", choice.getIndex());
writer.writeProperty("originalChoicePath", choice.sourcePath);
writer.writeProperty("originalThreadIndex", choice.originalThreadIndex);
writer.writeProperty("targetPath", choice.getPathStringOnChoice());
writeChoiceTags(writer, choice);
writer.writeObjectEnd();
}

private static void writeChoiceTags(SimpleJson.Writer writer, Choice choice) throws Exception {
if (choice.tags == null || choice.tags.isEmpty()) return;
writer.writePropertyStart("tags");
writer.writeArrayStart();
for (String tag : choice.tags) {
writer.write(tag);
}

writer.writeArrayEnd();
writer.writePropertyEnd();
}

static void writeInkList(SimpleJson.Writer writer, ListValue listVal) throws Exception {
InkList rawList = listVal.getValue();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public List<ListDefinition> getLists() {
ListValue findSingleItemListWithName(String name) {
ListValue val = null;

val = allUnambiguousListValueCache.get(name);
if (name != null && !name.trim().isEmpty()) val = allUnambiguousListValueCache.get(name);

return val;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,8 +670,8 @@ public RTObject call(List<RTObject> parameters) throws Exception {

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

if (p instanceof ListValue) hasList = true;
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/bladecoder/ink/runtime/SimpleJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,16 @@ public void writeObject(InnerWriter inner) throws Exception {
writeObjectEnd();
}

public void clear() {
StringWriter stringWriter = writer instanceof StringWriter ? (StringWriter) writer : null;
if (stringWriter == null) {
throw new UnsupportedOperationException(
"Writer.Clear() is only supported for the StringWriter variant, not for streams");
}

stringWriter.getBuffer().setLength(0);
}

public void writeObjectStart() throws Exception {
startNewObject(true);
stateStack.push(new StateElement(State.Object, 0));
Expand All @@ -278,6 +288,7 @@ public void writeObjectEnd() throws Exception {
Assert(getState() == State.Object);
writer.write("}");
stateStack.pop();
if (getState() == State.None) writer.flush();
}

public void writeProperty(String name, InnerWriter inner) throws Exception {
Expand Down
48 changes: 44 additions & 4 deletions src/main/java/com/bladecoder/ink/runtime/Story.java
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,36 @@ void callExternalFunction(String funcName, int numberOfArguments) throws Excepti

funcDef = externals.get(funcName);

if (funcDef != null && funcDef.lookaheadSafe && state.inStringEvaluation()) {
// 16th Jan 2023: Example ink that was failing:
//
// A line above
// ~ temp text = "{theFunc()}"
// {text}
//
// === function theFunc()
// { external():
// Boom
// }
//
// EXTERNAL external()
//
// What was happening: The external() call would exit out early due to
// _stateSnapshotAtLastNewline having a value, leaving the evaluation stack
// without a return value on it. When the if-statement tried to pop a value,
// the evaluation stack would be empty, and there would be an exception.
//
// The snapshot rewinding code is only designed to work when outside of
// string generation code (there's a check for that in the snapshot rewinding code),
// hence these things are incompatible, you can't have unsafe functions that
// cause snapshot rewinding in the middle of string generation.
//
error("External function " + funcName
+ " 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 = "
+ funcName + "()");
return;
}

// Should this function break glue? Abort run if we've already seen a newline.
// Set a bool to tell it to restore the snapshot at the end of this instruction.
if (funcDef != null && !funcDef.lookaheadSafe && stateSnapshotAtLastNewline != null) {
Expand Down Expand Up @@ -801,7 +831,9 @@ void continueInternal(float millisecsLimitAsync) throws Exception {
// It's possible for ink to call game to call ink to call game etc
// In this case, we only want to batch observe variable changes
// for the outermost call.
if (recursiveContinueCount == 1) state.getVariablesState().setbatchObservingVariableChanges(true);
if (recursiveContinueCount == 1) state.getVariablesState().startVariableObservation();
} else if (asyncContinueActive && !isAsyncTimeLimited) {
asyncContinueActive = false;
}

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

durationStopwatch.stop();

HashMap<String, RTObject> changedVariablesToObserve = null;

// 4 outcomes:
// - got newline (so finished this line of text)
// - can't continue (e.g. choices or ending)
Expand Down Expand Up @@ -863,7 +897,8 @@ else if (!state.getCallStack().canPop())
state.setDidSafeExit(false);
sawLookaheadUnsafeFunctionAfterNewline = false;

if (recursiveContinueCount == 1) state.getVariablesState().setbatchObservingVariableChanges(false);
if (recursiveContinueCount == 1)
changedVariablesToObserve = state.getVariablesState().completeVariableObservation();
asyncContinueActive = false;
}

Expand Down Expand Up @@ -925,6 +960,11 @@ else if (!state.getCallStack().canPop())
throw new StoryException(sb.toString());
}
}

// Send out variable observation events at the last second, since it might trigger new ink to be run
if (changedVariablesToObserve != null && changedVariablesToObserve.size() > 0) {
state.getVariablesState().notifyObservers(changedVariablesToObserve);
}
}

boolean continueSingleStep() throws Exception {
Expand Down Expand Up @@ -2802,7 +2842,7 @@ public Object evaluateFunction(String functionName, StringBuilder textOutput, Ob
// - _state (current, being patched)
void stateSnapshot() {
stateSnapshotAtLastNewline = state;
state = state.copyAndStartPatching();
state = state.copyAndStartPatching(false);
}

void restoreStateSnapshot() {
Expand Down Expand Up @@ -2853,7 +2893,7 @@ public StoryState copyStateForBackgroundThreadSave() throws Exception {
throw new Exception(
"Story is already in background saving mode, can't call CopyStateForBackgroundThreadSave again!");
StoryState stateToSave = state;
state = state.copyAndStartPatching();
state = state.copyAndStartPatching(true);
asyncSaving = true;
return stateToSave;
}
Expand Down
Loading

0 comments on commit d0d70be

Please sign in to comment.