Skip to content

Commit 4864a86

Browse files
committed
Integrate all changes made during A Highland Song development. See description for more details.
- Choices were storing thread references in a non-safe way, meaning that it could cause corruption during a background save. Not sure why we didn't see this issue on Heaven's Vault! - Move variable observation events to be sent out at the last second, since they may trigger new ink to be run (this was primarily to prevent ForceCompleteAsync not to start running new stuff unnecessarily) - More resilience when loading saves. Specifically, if content has shifted around, then a container at a specific indexed address may no longer be a container, which caused ink to crash - Support for using Continue() to completely a previously active ContinueAsync(). Not sure how this was ever working before on HV 🤪 - Other minor improvements and convenience stuff
1 parent 6a51219 commit 4864a86

File tree

10 files changed

+175
-51
lines changed

10 files changed

+175
-51
lines changed

ink-engine-runtime/CallStack.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ public void Pop(PushPopType? type = null)
342342
// Get variable value, dereferencing a variable pointer if necessary
343343
public Runtime.Object GetTemporaryVariableWithName(string name, int contextIndex = -1)
344344
{
345+
// contextIndex 0 means global, so index is actually 1-based
345346
if (contextIndex == -1)
346347
contextIndex = currentElementIndex+1;
347348

ink-engine-runtime/Choice.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ public string pathStringOnChoice {
5151
public Choice()
5252
{
5353
}
54+
55+
public Choice Clone() {
56+
var copy = new Choice();
57+
copy.text = text;
58+
copy.sourcePath = sourcePath;
59+
copy.index = index;
60+
copy.targetPath = targetPath;
61+
copy.originalThreadIndex = originalThreadIndex;
62+
copy.isInvisibleDefault = isInvisibleDefault;
63+
if( threadAtGeneration != null ) copy.threadAtGeneration = threadAtGeneration.Copy();
64+
return copy;
65+
}
5466
}
5567
}
5668

ink-engine-runtime/Container.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,18 @@ public SearchResult ContentAtPath(Path path, int partialPathStart = 0, int parti
254254
break;
255255
}
256256

257+
// Are we about to loop into another container?
258+
// Is the object a container as expected? It might
259+
// no longer be if the content has shuffled around, so what
260+
// was originally a container no longer is.
261+
var nextContainer = foundObj as Container;
262+
if( i < partialPathLength-1 && nextContainer == null ) {
263+
result.approximate = true;
264+
break;
265+
}
266+
257267
currentObj = foundObj;
258-
currentContainer = foundObj as Container;
268+
currentContainer = nextContainer;
259269
}
260270

261271
result.obj = currentObj;

ink-engine-runtime/InkList.cs

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,24 @@ public override string ToString ()
7575
/// </summary>
7676
public override bool Equals (object obj)
7777
{
78-
if (obj is InkListItem) {
79-
var otherItem = (InkListItem)obj;
80-
return otherItem.itemName == itemName
81-
&& otherItem.originName == originName;
82-
}
83-
78+
if (obj is InkListItem)
79+
return Equals((InkListItem)obj);
8480
return false;
8581
}
8682

83+
public bool Equals (InkListItem otherItem)
84+
{
85+
return otherItem.itemName == itemName && otherItem.originName == originName;
86+
}
87+
88+
public static bool operator == (InkListItem left, InkListItem right) {
89+
return left.Equals(right);
90+
}
91+
92+
public static bool operator != (InkListItem left, InkListItem right) {
93+
return !(left == right);
94+
}
95+
8796
/// <summary>
8897
/// Get the hashcode for an item.
8998
/// </summary>
@@ -154,6 +163,8 @@ public InkList (KeyValuePair<InkListItem, int> singleElement)
154163
/// <param name="itemKey">Item key.</param>
155164
/// <param name="originStory">Origin story.</param>
156165
public static InkList FromString(string myListItem, Story originStory) {
166+
if (string.IsNullOrEmpty(myListItem))
167+
return new InkList();
157168
var listValue = originStory.listDefinitions.FindSingleItemListWithName (myListItem);
158169
if (listValue)
159170
return new InkList (listValue.value);
@@ -193,30 +204,44 @@ public void AddItem (InkListItem item)
193204
/// <summary>
194205
/// Adds the given item to the ink list, attempting to find the origin list definition that it belongs to.
195206
/// The item must therefore come from a list definition that is already "known" to this list, so that the
196-
/// item's value can be looked up. By "known", we mean that it already has items in it from that source, or
207+
/// item's value can be looked up.
208+
/// By "known", we mean that it already has items in it from that source, or
197209
/// it did at one point - it can't be a completely fresh empty list, or a list that only contains items from
198210
/// a different list definition.
211+
/// You can also provide the Story object, so in the case of an unknown element, it can be created fresh
199212
/// </summary>
200-
public void AddItem (string itemName)
213+
public void AddItem(string itemName, Story storyObject = null)
201214
{
202215
ListDefinition foundListDef = null;
203216

204-
foreach (var origin in origins) {
205-
if (origin.ContainsItemWithName (itemName)) {
206-
if (foundListDef != null) {
207-
throw new System.Exception ("Could not add the item " + itemName + " to this list because it could come from either " + origin.name + " or " + foundListDef.name);
208-
} else {
209-
foundListDef = origin;
217+
if (origins != null) {
218+
foreach (var origin in origins) {
219+
if (origin.ContainsItemWithName(itemName)) {
220+
if (foundListDef != null) {
221+
throw new System.Exception("Could not add the item " + itemName + " to this list because it could come from either " + origin.name + " or " + foundListDef.name);
222+
} else {
223+
foundListDef = origin;
224+
}
210225
}
211226
}
212227
}
213228

214229
if (foundListDef == null)
215-
throw new System.Exception ("Could not add the item " + itemName + " to this list because it isn't known to any list definitions previously associated with this list.");
216-
217-
var item = new InkListItem (foundListDef.name, itemName);
218-
var itemVal = foundListDef.ValueForItem(item);
219-
this [item] = itemVal;
230+
{
231+
if (storyObject == null)
232+
throw new System.Exception("Could not add the item " + itemName + " to this list because it isn't known to any list definitions previously associated with this list, and no ink Story object was provided to create it from.");
233+
else
234+
{
235+
var newItem = FromString(itemName, storyObject).orderedItems[0];
236+
this[newItem.Key] = newItem.Value;
237+
}
238+
}
239+
else
240+
{
241+
var item = new InkListItem(foundListDef.name, itemName);
242+
var itemVal = foundListDef.ValueForItem(item);
243+
this[item] = itemVal;
244+
}
220245
}
221246

222247
/// <summary>
@@ -591,6 +616,17 @@ List<KeyValuePair<InkListItem, int>> orderedItems {
591616
}
592617
}
593618

619+
/// <summary>
620+
/// If you have an InkList that's known to have one single item, this is a convenient way to get it.
621+
/// </summary>
622+
public InkListItem singleItem {
623+
get {
624+
foreach(var item in this)
625+
return item.Key;
626+
return default;
627+
}
628+
}
629+
594630
/// <summary>
595631
/// Returns a string in the form "a, b, c" with the names of the items in the list, without
596632
/// the origin list definition names. Equivalent to writing {list} in ink.

ink-engine-runtime/ListDefinitionsOrigin.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public bool TryListGetDefinition (string name, out ListDefinition def)
4343
public ListValue FindSingleItemListWithName (string name)
4444
{
4545
ListValue val = null;
46-
_allUnambiguousListValueCache.TryGetValue(name, out val);
46+
if (!string.IsNullOrWhiteSpace(name))
47+
_allUnambiguousListValueCache.TryGetValue(name, out val);
4748
return val;
4849
}
4950

ink-engine-runtime/NativeFunctionCall.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public Runtime.Object Call(List<Runtime.Object> parameters)
9696
bool hasList = false;
9797
foreach (var p in parameters) {
9898
if (p is Void)
99-
throw new StoryException ("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?");
99+
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?");
100100
if (p is ListValue)
101101
hasList = true;
102102
}

ink-engine-runtime/SimpleJson.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,16 @@ public Writer(Stream stream)
314314
_writer = new System.IO.StreamWriter(stream, Encoding.UTF8);
315315
}
316316

317+
public void Clear()
318+
{
319+
var stringWriter = _writer as StringWriter;
320+
if( stringWriter == null ) {
321+
throw new NotSupportedException("Writer.Clear() is only supported for the StringWriter variant, not for streams");
322+
}
323+
324+
stringWriter.GetStringBuilder().Clear();
325+
}
326+
317327
public void WriteObject(Action<Writer> inner)
318328
{
319329
WriteObjectStart();

ink-engine-runtime/Story.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,12 @@ void ContinueInternal (float millisecsLimitAsync = 0)
447447
// In this case, we only want to batch observe variable changes
448448
// for the outermost call.
449449
if (_recursiveContinueCount == 1)
450-
_state.variablesState.batchObservingVariableChanges = true;
450+
_state.variablesState.StartVariableObservation();
451+
}
452+
453+
// Async was previously active, but now we want to finish synchronously
454+
else if( _asyncContinueActive && !isAsyncTimeLimited ) {
455+
_asyncContinueActive = false;
451456
}
452457

453458
// Start timing
@@ -477,6 +482,8 @@ void ContinueInternal (float millisecsLimitAsync = 0)
477482

478483
durationStopwatch.Stop ();
479484

485+
Dictionary<string, Object> changedVariablesToObserve = null;
486+
480487
// 4 outcomes:
481488
// - got newline (so finished this line of text)
482489
// - can't continue (e.g. choices or ending)
@@ -512,7 +519,7 @@ void ContinueInternal (float millisecsLimitAsync = 0)
512519
_sawLookaheadUnsafeFunctionAfterNewline = false;
513520

514521
if (_recursiveContinueCount == 1)
515-
_state.variablesState.batchObservingVariableChanges = false;
522+
changedVariablesToObserve = _state.variablesState.CompleteVariableObservation();
516523

517524
_asyncContinueActive = false;
518525
if(onDidContinue != null) onDidContinue();
@@ -573,6 +580,11 @@ void ContinueInternal (float millisecsLimitAsync = 0)
573580
throw new StoryException(sb.ToString());
574581
}
575582
}
583+
584+
// Send out variable observation events at the last second, since it might trigger new ink to be run
585+
if( changedVariablesToObserve != null && changedVariablesToObserve.Count > 0 ) {
586+
_state.variablesState.NotifyObservers(changedVariablesToObserve);
587+
}
576588
}
577589

578590
bool ContinueSingleStep ()
@@ -776,7 +788,7 @@ public Pointer PointerAtPath (Path path)
776788
void StateSnapshot()
777789
{
778790
_stateSnapshotAtLastNewline = _state;
779-
_state = _state.CopyAndStartPatching();
791+
_state = _state.CopyAndStartPatching(forBackgroundSave:false);
780792
}
781793

782794
void RestoreStateSnapshot()
@@ -828,7 +840,7 @@ public StoryState CopyStateForBackgroundThreadSave()
828840
IfAsyncWeCant("start saving on a background thread");
829841
if (_asyncSaving) throw new System.Exception("Story is already in background saving mode, can't call CopyStateForBackgroundThreadSave again!");
830842
var stateToSave = _state;
831-
_state = _state.CopyAndStartPatching();
843+
_state = _state.CopyAndStartPatching(forBackgroundSave:true);
832844
_asyncSaving = true;
833845
return stateToSave;
834846
}
@@ -1912,6 +1924,10 @@ public Runtime.Object EvaluateExpression(Runtime.Container exprContainer)
19121924
/// </summary>
19131925
public bool allowExternalFunctionFallbacks { get; set; }
19141926

1927+
/// <summary>
1928+
/// Try to get a reference to an external function. This was originally exposed for the
1929+
/// Ink-Unity integration plugin so that the inspector can list them.
1930+
/// </summary>
19151931
public bool TryGetExternalFunction(string functionName, out ExternalFunction externalFunction) {
19161932
ExternalFunctionDef externalFunctionDef;
19171933
if(_externals.TryGetValue (functionName, out externalFunctionDef)) {
@@ -2528,7 +2544,6 @@ void VariableStateDidChangeEvent(string variableName, Runtime.Object newValueObj
25282544

25292545
VariableObserver observers = null;
25302546
if (_variableObservers.TryGetValue (variableName, out observers)) {
2531-
25322547
if (!(newValueObj is Value)) {
25332548
throw new System.Exception ("Tried to get the value of a variable that isn't a standard type");
25342549
}

ink-engine-runtime/StoryState.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,21 @@ public string currentPathString {
231231
}
232232
}
233233

234+
/// <summary>
235+
/// Get the previous state of currentPathString, which can be helpful
236+
/// for finding out where the story was before it ended (when the path
237+
/// string becomes null)
238+
/// </summary>
239+
public string previousPathString {
240+
get {
241+
var pointer = previousPointer;
242+
if (pointer.isNull)
243+
return null;
244+
else
245+
return pointer.path.ToString();
246+
}
247+
}
248+
234249
public Runtime.Pointer currentPointer {
235250
get {
236251
return callStack.currentElement.currentPointer;
@@ -530,7 +545,7 @@ internal void RemoveFlow_Internal(string flowName)
530545
// Runtime.Objects are treated as immutable after they've been set up.
531546
// (e.g. we don't edit a Runtime.StringValue after it's been created an added.)
532547
// I wonder if there's a sensible way to enforce that..??
533-
public StoryState CopyAndStartPatching()
548+
public StoryState CopyAndStartPatching(bool forBackgroundSave)
534549
{
535550
var copy = new StoryState(story);
536551

@@ -540,10 +555,21 @@ public StoryState CopyAndStartPatching()
540555
// If the patch is applied, then this new flow will replace the old one in _namedFlows
541556
copy._currentFlow.name = _currentFlow.name;
542557
copy._currentFlow.callStack = new CallStack (_currentFlow.callStack);
543-
copy._currentFlow.currentChoices.AddRange(_currentFlow.currentChoices);
544558
copy._currentFlow.outputStream.AddRange(_currentFlow.outputStream);
545559
copy.OutputStreamDirty();
546560

561+
// When background saving we need to make copies of choices since they each have
562+
// a snapshot of the thread at the time of generation since the game could progress
563+
// significantly and threads modified during the save process.
564+
// However, when doing internal saving and restoring of snapshots this isn't an issue,
565+
// and we can simply ref-copy the choices with their existing threads.
566+
if( forBackgroundSave ) {
567+
foreach(var choice in _currentFlow.currentChoices)
568+
copy._currentFlow.currentChoices.Add(choice.Clone());
569+
} else {
570+
copy._currentFlow.currentChoices.AddRange(_currentFlow.currentChoices);
571+
}
572+
547573
// The copy of the state has its own copy of the named flows dictionary,
548574
// except with the current flow replaced with the copy above
549575
// (Assuming we're in multi-flow mode at all. If we're not then

0 commit comments

Comments
 (0)