diff --git a/inkcpp/globals_impl.cpp b/inkcpp/globals_impl.cpp index 17be9185..d062be8d 100644 --- a/inkcpp/globals_impl.cpp +++ b/inkcpp/globals_impl.cpp @@ -285,20 +285,27 @@ const unsigned char* globals_impl::snap_load(const unsigned char* ptr, const loa ); for (size_t i = 0; i < old_capacity; ++i) { hash_t path; - ptr = snap_read(ptr, path); + ptr = snap_read(ptr, path); + if (! loader.migratable) { + // Not migrating, so this is the story the snapshot was taken from and the counts were loaded + // in container order. The stored hash only confirms that; it cannot be used to look the + // container up again. A tracked container without a name of its own (a sequence) stores + // hash 0, and named containers below an indexed one can share a name, so the lookup finds + // nothing or a different container. + inkAssert( + _owner->container_data(i)._hash == path, + "Snapshot does not match this story: tracked container %u differs.", ( unsigned ) i + ); + continue; + } container_t c_id = ~0U; ip_t container_ip = _owner->find_offset_for(path); bool found = container_ip != nullptr && _owner->find_container_id( static_cast(container_ip - _owner->instructions()), c_id ); - if (! loader.migratable) { - inkAssert(found, "Invalid container id reference."); - inkAssert(c_id == i, "tracked containere are not allowed to move, expect we migrate"); - } else { - if (found) { - _visit_counts.set(c_id, _visit_counts.get_old(i)); - } + if (found) { + _visit_counts.set(c_id, _visit_counts.get_old(i)); } } if (loader.migratable) { diff --git a/inkcpp_test/Fixes.cpp b/inkcpp_test/Fixes.cpp index 510e78ad..437408db 100644 --- a/inkcpp_test/Fixes.cpp +++ b/inkcpp_test/Fixes.cpp @@ -767,3 +767,38 @@ SCENARIO( } } } + +SCENARIO( + "snapshot restores visit counts of unnamed and same-named containers", + "[regression][snapshot][runtime]" +) +{ + // The sequence's container is tracked but has no name of its own, and the labelled gather sits + // below a choice's indexed container. Restoring a snapshot used to look each tracked container up + // again by name, which found nothing for the first and could find another container for the + // second, and threw. + GIVEN("a story with a sequence and a labelled gather inside a choice") + { + std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "SnapshotVisitCounts.bin")}; + runner thread = ink->new_runner(); + + WHEN("it is played to the first choice and a snapshot is taken") + { + std::string text = thread->getall(); + REQUIRE(text == "First visit.\n"); + REQUIRE(thread->num_choices() == 2); + std::unique_ptr snap{thread->create_snapshot()}; + + THEN("the snapshot loads into a new runner, which continues the sequence") + { + runner loaded; + REQUIRE_NOTHROW(loaded = ink->new_runner_from_snapshot(*snap)); + REQUIRE(loaded->num_choices() == 2); + loaded->choose(0); + REQUIRE(loaded->getall() == "Inside.\n"); + loaded->choose(0); + REQUIRE(loaded->getall() == "Later visit.\n"); + } + } + } +} diff --git a/inkcpp_test/ink/SnapshotVisitCounts.ink b/inkcpp_test/ink/SnapshotVisitCounts.ink new file mode 100644 index 00000000..56fd0e7a --- /dev/null +++ b/inkcpp_test/ink/SnapshotVisitCounts.ink @@ -0,0 +1,7 @@ +-> knot +=== knot === +{First visit.|Later visit.} +* [Again] + - - (inner) Inside. + ** [Back] -> knot +* [Leave] -> END