diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de6e7de2..59974295 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -304,6 +304,7 @@ jobs: git fetch origin master - name: Check clang-format run: | + clang-format-18 --version git clang-format-18 --extensions c,cpp,h,hpp --style file -q --diff origin/master diff=$(git clang-format-18 --extensions c,cpp,h,hpp --style file -q --diff origin/master) echo "::error Format diff >$diff<" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8be6abaf..0a68d7fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: url: https://pypi.org/p/inkcpp-py permissions: id-token: write - contents: write + contents: write steps: - uses: actions/checkout@v4 - name: Download artifacts @@ -46,4 +46,3 @@ jobs: --title="${GITHUB_REPOSITORY#*/} ${tag#v}" \ --generate-notes \ "$tag" "linux-cl.zip" "linux-lib.zip" "linux-clib.zip" "unreal_5_7.zip" "unreal_5_6.zip" "unreal_5_5.zip" "unreal_5_4.zip" "macos-cl.zip" "macos-lib.zip" "macos-clib.zip" "win64-cl.zip" "macos-arm-cl.zip" "macos-arm-lib.zip" "macos-arm-clib.zip" "win64-lib.zip" "win64-clib.zip" - diff --git a/README.md b/README.md index 64478826..5ab46ef8 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Adapt `TargetPlatforms` as nessesarry. You might also want to install the Plugin Nice features for testing: + predefined choice selection `echo 1 2 1 | inkpp-cl -p story.(ink|json|bin)` + create snapshots to shorten testing: - + create snapshot by entering `-1` as choice `echo 1 2 -1 | inkcpp-cl -p story.ink` + + create snapshot by entering `-1` as choice `echo 1 2 -1 1 | inkcpp-cl -p story.ink` + load snapshot as an additional argument `echo 1 | inkcpp-cl -p story.snap story.ink` ## Including in C++ Code diff --git a/inkcpp/CMakeLists.txt b/inkcpp/CMakeLists.txt index 1785fdc2..942a517e 100644 --- a/inkcpp/CMakeLists.txt +++ b/inkcpp/CMakeLists.txt @@ -46,6 +46,8 @@ list( string_operations.cpp string_operations.cpp numeric_operations.cpp + hungarian_solver.h + hungarian_solver.cpp casting.h executioner.h string_utils.h diff --git a/inkcpp/array.h b/inkcpp/array.h index bdca6673..5eba1c88 100644 --- a/inkcpp/array.h +++ b/inkcpp/array.h @@ -20,7 +20,7 @@ namespace ink::runtime::internal * @tparam simple if the object has a trivial destructor, so delete[](char*) can be used instead of * calling the constructor. * @tparam dynamic if the memory should be allocated on the heap and grow if needed - * @tparam initialCapacitiy number of elements to allocate at construction, if !dynamic, this is + * @tparam initial capacity number of elements to allocate at construction, if !dynamic, this is * allocated in place and can not be changed. */ template @@ -157,6 +157,8 @@ class managed_array : public snapshot_interface void extend(size_t capacity = 0); + bool can_be_migrated() const { return true; } + size_t snap(unsigned char* data, const snapper& snapper) const { inkAssert(! is_pointer{}(), "here is a special case oversight"); @@ -229,6 +231,8 @@ class managed_restorable_array : public managed_arraysize(); } void forgett() { _last_size = 0; } @@ -237,6 +241,8 @@ class managed_restorable_array : public managed_array&) = delete; basic_restorable_array& operator=(const basic_restorable_array&) = delete; @@ -315,10 +321,19 @@ class basic_restorable_array : public snapshot_interface // get value by index const T& get(size_t index) const; + const T& get_old(size_t index) const; // size of the array inline size_t capacity() const { return _capacity; } + inline size_t loaded_capacity() const + { + inkAssert( + _loaded_capacity != static_cast(~0), "This object was not loaded from a snapshot." + ); + return _loaded_capacity; + } + // only const indexing is supported due to save/restore system inline const T& operator[](size_t index) const { return get(index); } @@ -331,6 +346,7 @@ class basic_restorable_array : public snapshot_interface void clear(const T& value); // snapshot interface + virtual bool can_be_migrated() const; virtual size_t snap(unsigned char* data, const snapper&) const; virtual const unsigned char* snap_load(const unsigned char* data, const loader&); @@ -358,12 +374,15 @@ class basic_restorable_array : public snapshot_interface // real values live here T* _array; - // we store values here when we're in save mode + // we store values here when we're in safe mode // they're copied on a call to forget() T* _temp; // size of both _array and _temp size_t _capacity; + // if loaded with snap_load, this value was the original size, the current capacity might be + // higher + size_t _loaded_capacity = static_cast(~0); // null const T _null; @@ -389,7 +408,7 @@ inline const T& basic_restorable_array::get(size_t index) const { check_index(index); - // If we're in save mode and we have a value at that index, return that instead + // If we're in safe mode, and we have a value at that index, return that instead if (_saved && _temp[index] != _null) { return _temp[index]; } @@ -398,6 +417,15 @@ inline const T& basic_restorable_array::get(size_t index) const return _array[index]; } +template +inline const T& basic_restorable_array::get_old(size_t index) const +{ + check_index(index); + inkAssert(_saved, "Use old only on saved arrays."); + + return _array[index]; +} + template inline void basic_restorable_array::save() { @@ -419,7 +447,7 @@ inline void basic_restorable_array::forget() { // Run through the _temp array for (size_t i = 0; i < _capacity; i++) { - // Copy if there's values + // Copy if there are values if (_temp[i] != _null) { _array[i] = _temp[i]; } @@ -521,6 +549,12 @@ class allocated_restorable_array : public basic_restorable_array T* _buffer; }; +template +inline bool basic_restorable_array::can_be_migrated() const +{ + return ! _saved; +} + template inline size_t basic_restorable_array::snap(unsigned char* data, const snapper&) const { @@ -542,18 +576,18 @@ inline const unsigned char* { auto ptr = data; ptr = snap_read(ptr, _saved); - decltype(_capacity) capacity; - ptr = snap_read(ptr, capacity); + ptr = snap_read(ptr, _loaded_capacity); if (buffer() == nullptr) { - static_cast&>(*this).resize(capacity); + static_cast&>(*this).resize(_loaded_capacity); } inkAssert( - _capacity >= capacity, "New config does not allow for necessary size used by this snapshot!" + _capacity >= _loaded_capacity, + "New config does not allow for necessary size used by this snapshot!" ); T null; ptr = snap_read(ptr, null); inkAssert(null == _null, "null value is different to snapshot!"); - for (size_t i = 0; i < _capacity; ++i) { + for (size_t i = 0; i < _loaded_capacity; ++i) { ptr = snap_read(ptr, _array[i]); ptr = snap_read(ptr, _temp[i]); } diff --git a/inkcpp/collections/restorable.h b/inkcpp/collections/restorable.h index 7f8b9b7a..9dda466a 100644 --- a/inkcpp/collections/restorable.h +++ b/inkcpp/collections/restorable.h @@ -356,6 +356,8 @@ class restorable : public snapshot_interface virtual size_t snap(unsigned char* data, const snapper&) const; const unsigned char* snap_load(const unsigned char* data, const loader&); + bool can_be_migrated() const { return ! is_saved(); } + protected: // Called when we run out of space in buffer. virtual void overflow(ElementType*&, size_t&) { inkFail("Restorable run out of memory!"); } diff --git a/inkcpp/functional.cpp b/inkcpp/functional.cpp index 735e7fe7..343d11f7 100644 --- a/inkcpp/functional.cpp +++ b/inkcpp/functional.cpp @@ -53,6 +53,13 @@ float function_base::pop(basic_eval_stack* stack, list_table&) return casting::numeric_cast(val); } +template<> +double function_base::pop(basic_eval_stack* stack, list_table&) +{ + value val = stack->pop(); + return casting::numeric_cast(val); +} + template<> const char* function_base::pop(basic_eval_stack* stack, list_table&) { @@ -79,6 +86,12 @@ void function_base::push(basic_eval_stack* stack, const float& v) stack->push(value{}.set(v)); } +template<> +void function_base::push(basic_eval_stack* stack, const double& v) +{ + stack->push(value{}.set(static_cast(v))); +} + template<> void function_base::push(basic_eval_stack* stack, const bool& v) { diff --git a/inkcpp/globals_impl.cpp b/inkcpp/globals_impl.cpp index 6003b107..57cadc5a 100644 --- a/inkcpp/globals_impl.cpp +++ b/inkcpp/globals_impl.cpp @@ -17,33 +17,36 @@ namespace ink::runtime::internal globals_impl::globals_impl(const story_impl* story) : _num_containers(story->num_containers()) , _turn_cnt{0} - , _visit_counts() - , _visit_counts_backup() + , _visit_counts(visit_count(), visit_count_null_value) , _owner(story) , _runners_start(nullptr) , _lists(story->list_meta(), story->get_header()) , _globals_initialized(false) { _visit_counts.resize(_num_containers); - _visit_counts_backup.resize(_num_containers); if (_lists) { // initialize static lists - const list_flag* flags = story->lists(); + init_static_list_flags(); + } +} + +void globals_impl::init_static_list_flags() +{ + const list_flag* flags = _owner->lists(); + while (*flags != null_flag) { + list_table::list l = _lists.create_permament(); while (*flags != null_flag) { - list_table::list l = _lists.create_permament(); - while (*flags != null_flag) { - list_flag flag = _lists.external_fvalue_to_internal(*flags); - _lists.add_inplace(l, flag); - ++flags; - } + list_flag flag = _lists.external_fvalue_to_internal(*flags); + _lists.add_inplace(l, flag); ++flags; } - for (const auto& flag : _lists.named_flags()) { - set_variable( - hash_string(flag.name), - value{}.set(list_flag{flag.flag.list_id, flag.flag.flag}) - ); - } + ++flags; + } + for (const auto& flag : _lists.named_flags()) { + set_variable( + hash_string(flag.name), + value{}.set(list_flag{flag.flag.list_id, flag.flag.flag}) + ); } } @@ -51,8 +54,7 @@ void globals_impl::visit(uint32_t container_id, bool entering_at_start) { if ((! (_owner->container_flag(container_id) & CommandFlag::CONTAINER_MARKER_ONLY_FIRST)) || entering_at_start) { - _visit_counts[container_id].visits += 1; - _visit_counts[container_id].turns = 0; + _visit_counts.set(container_id, {_visit_counts[container_id].visits + 1, 0}); } } @@ -66,9 +68,11 @@ uint32_t globals_impl::turns() const { return _turn_cnt; } void globals_impl::turn() { ++_turn_cnt; - for (size_t i = 0; i < _visit_counts.size(); ++i) { - if (_visit_counts[i].turns != -1) { - _visit_counts[i].turns += 1; + for (size_t i = 0; i < _visit_counts.capacity(); ++i) { + visit_count visits = _visit_counts[i]; + if (visits.turns != -1) { + visits.turns += 1; + _visit_counts.set(i, visits); } } } @@ -237,35 +241,43 @@ void globals_impl::gc() void globals_impl::save() { - for (uint32_t i = 0; i < _num_containers; ++i) { - _visit_counts_backup[i] = _visit_counts[i]; - } + _visit_counts.save(); _variables.save(); } void globals_impl::restore() { - for (uint32_t i = 0; i < _num_containers; ++i) { - _visit_counts[i] = _visit_counts_backup[i]; - } + _visit_counts.restore(); _variables.restore(); } -void globals_impl::forget() { _variables.forget(); } +void globals_impl::forget() +{ + _visit_counts.forget(); + _variables.forget(); +} snapshot* globals_impl::create_snapshot() const { return new snapshot_impl(*this); } +bool globals_impl::can_be_migrated() const +{ + return _visit_counts.can_be_migrated() && _strings.can_be_migrated() && _lists.can_be_migrated() + && _variables.can_be_migrated(); +} + size_t globals_impl::snap(unsigned char* data, const snapper& snapper) const { unsigned char* ptr = data; - inkAssert(_num_containers == _visit_counts.size(), "Should be equal!"); + inkAssert(_num_containers == _visit_counts.capacity(), "Should be equal!"); inkAssert( _globals_initialized, "Only support snapshot of globals with runner! or you don't need a snapshot for this state" ); ptr = snap_write(ptr, _turn_cnt, data != nullptr); ptr += _visit_counts.snap(data ? ptr : nullptr, snapper); - ptr += _visit_counts_backup.snap(data ? ptr : nullptr, snapper); + for (unsigned i = 0; i < _visit_counts.capacity(); ++i) { + ptr = snap_write(ptr, _owner->container_hash(i), data != nullptr); + } ptr += _strings.snap(data ? ptr : nullptr, snapper); ptr += _lists.snap(data ? ptr : nullptr, snapper); ptr += _variables.snap(data ? ptr : nullptr, snapper); @@ -277,10 +289,39 @@ const unsigned char* globals_impl::snap_load(const unsigned char* ptr, const loa _globals_initialized = true; ptr = snap_read(ptr, _turn_cnt); ptr = _visit_counts.snap_load(ptr, loader); - ptr = _visit_counts_backup.snap_load(ptr, loader); - inkAssert(_visit_counts.size() == _visit_counts_backup.size(), "Data inconsitency"); + size_t old_capacity = _visit_counts.loaded_capacity(); + // shuffle values if needed + if (loader.migratable) { + // extend array if needed + if (_visit_counts.capacity() < _owner->num_containers()) { + _visit_counts.resize(_owner->num_containers()); + } + _visit_counts.save(); + } + + inkAssert( + _visit_counts.capacity() >= _owner->num_containers(), + "Missmatching number of tracked containers." + ); + for (size_t i = 0; i < old_capacity; ++i) { + hash_t path; + ptr = snap_read(ptr, path); + container_t c_id; + bool found = _owner->get_container_id(_owner->find_offset_for(path), 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 (loader.migratable) { + _visit_counts.forget(); + } inkAssert( - _num_containers == _visit_counts.size(), + _num_containers == _visit_counts.capacity(), "errer when loading visit counts, story file dont match snapshot!" ); ptr = _strings.snap_load(ptr, loader); @@ -289,6 +330,19 @@ const unsigned char* globals_impl::snap_load(const unsigned char* ptr, const loa return ptr; } +bool globals_impl::migrate_new_globals(globals_impl& new_globals, const char* list_metadata) +{ + bool success = _variables.migrate(new_globals._variables) + && ((! _lists) || _lists.migrate(list_metadata, _owner->get_header())); + if (! success) { + return false; + } + if (_lists) { + init_static_list_flags(); + } + return true; +} + config::statistics::global globals_impl::statistics() const { return { diff --git a/inkcpp/globals_impl.h b/inkcpp/globals_impl.h index 06c787d6..6b87f76d 100644 --- a/inkcpp/globals_impl.h +++ b/inkcpp/globals_impl.h @@ -30,9 +30,22 @@ class globals_impl final { friend snapshot_impl; + void init_static_list_flags(); + public: size_t snap(unsigned char* data, const snapper&) const; const unsigned char* snap_load(const unsigned char* data, const loader&); + bool can_be_migrated() const; + /** Merges a global snapshot with new global definition. + * new global variables are taken from new_global. + * already existing ones are ignored + * no longer existing ones are deleted. + * @retval true on success + * @param[in] new_globals to read current relevant variables from. It is modified to be equal to + * @param[in] list_metadata old list metadata to migrate list + * the globals stored inside. + */ + bool migrate_new_globals(globals_impl& new_globals, const char* list_metadata); // Initializes a new global store from the given story globals_impl(const story_impl*); @@ -121,8 +134,9 @@ class globals_impl final bool operator!=(const visit_count& vc) const { return ! (*this == vc); } }; - managed_array _visit_counts; - managed_array _visit_counts_backup; + static constexpr visit_count visit_count_null_value{~0U, -2}; + + internal::allocated_restorable_array _visit_counts; // Pointer back to owner story. const story_impl* const _owner; diff --git a/inkcpp/header.cpp b/inkcpp/header.cpp index e99d08cc..96d1e836 100644 --- a/inkcpp/header.cpp +++ b/inkcpp/header.cpp @@ -7,38 +7,35 @@ #include "header.h" #include "version.h" -namespace ink::internal { +namespace ink::internal +{ - header header::parse_header(const char *data) - { - header res; - const char* ptr = data; - res.endien = *reinterpret_cast(ptr); - ptr += sizeof(header::endian_types); +header header::parse_header(const char* data) +{ + header res; + const char* ptr = data; + res.endien = *reinterpret_cast(ptr); + ptr += sizeof(header::endian_types); - using v_t = decltype(header::ink_version_number); - using vcpp_t = decltype(header::ink_bin_version_number); + using v_t = decltype(header::ink_version_number); + using vcpp_t = decltype(header::ink_bin_version_number); - if (res.endien == header::endian_types::same) { - res.ink_version_number = - *reinterpret_cast(ptr); - ptr += sizeof(v_t); - res.ink_bin_version_number = - *reinterpret_cast(ptr); + if (res.endien == header::endian_types::same) { + res.ink_version_number = *reinterpret_cast(ptr); + ptr += sizeof(v_t); + res.ink_bin_version_number = *reinterpret_cast(ptr); - } else if (res.endien == header::endian_types::differ) { - res.ink_version_number = - swap_bytes(*reinterpret_cast(ptr)); - ptr += sizeof(v_t); - res.ink_bin_version_number = - swap_bytes(*reinterpret_cast(ptr)); - } else { - inkFail("Failed to parse endian encoding!"); - } + } else if (res.endien == header::endian_types::differ) { + res.ink_version_number = swap_bytes(*reinterpret_cast(ptr)); + ptr += sizeof(v_t); + res.ink_bin_version_number = swap_bytes(*reinterpret_cast(ptr)); + } else { + inkFail("Failed to parse endian encoding! %#04x", res.endien); + } - if (res.ink_bin_version_number != InkBinVersion) { - inkFail("InkCpp-version mismatch: file was compiled with different InkCpp-version!"); - } - return res; + if (res.ink_bin_version_number != InkBinVersion) { + inkFail("InkCpp-version mismatch: file was compiled with different InkCpp-version!"); } + return res; } +} // namespace ink::internal diff --git a/inkcpp/hungarian_solver.cpp b/inkcpp/hungarian_solver.cpp new file mode 100644 index 00000000..edda38da --- /dev/null +++ b/inkcpp/hungarian_solver.cpp @@ -0,0 +1,206 @@ +#include "hungarian_solver.h" + +#include "system.h" +#include +#include +#include +#include +#include + +class HungarienCtx +{ + const int n; + const float* cost; + + struct { + float* row; + float* col; + } pot; + + float* slack; + int* col_2_row; + int* path; + bool* visit_col; + +public: + HungarienCtx(const float* cost, int n) + : n{n} + , cost{cost} + { + pot.row = new float[n + 1]; + memset(pot.row, 0, sizeof(float) * (n + 1)); + pot.col = new float[n + 1]; + memset(pot.col, 0, sizeof(float) * (n + 1)); + slack = new float[n + 1]; + col_2_row = new int[n + 1]; + memset(col_2_row, 0, sizeof(int) * (n + 1)); + path = new int[n + 1]; + visit_col = new bool[n + 1]; + } + + ~HungarienCtx() + { + delete[] pot.row; + delete[] pot.col; + delete[] slack; + delete[] col_2_row; + delete[] path; + delete[] visit_col; + } + + int operator[](int col) { return col_2_row[col]; } + + void init_search(int row) + { + col_2_row[0] = row; + for (size_t i = 0; i <= static_cast(n); ++i) { + visit_col[i] = false; + slack[i] = std::numeric_limits::max(); + path[i] = 0; + } + } + + int find_augmenting_path() + { + int current_col = 0; + do { + visit_col[current_col] = true; + int current_row = col_2_row[current_col]; + float delta = std::numeric_limits::max(); + int next_col = 0; + for (int col = 1; col <= n; ++col) { + if (! visit_col[col]) { + float reduced + = cost[(current_row - 1) * n + (col - 1)] - pot.row[current_row] - pot.col[col]; + if (reduced < slack[col]) { + slack[col] = reduced; + path[col] = current_col; + } + if (slack[col] < delta) { + delta = slack[col]; + next_col = col; + } + } + } + + for (size_t col = 0; col <= static_cast(n); ++col) { + if (visit_col[col]) { + pot.row[col_2_row[col]] += delta; + pot.col[col] -= delta; + } else { + slack[col] -= delta; + } + } + current_col = next_col; + } while (col_2_row[current_col]); + return current_col; + } + + // end_col = 0 -> no augmenting + void augment_matching(int end_col) + { + int col = end_col; + do { + int prev = path[col]; + col_2_row[col] = col_2_row[prev]; + col = prev; + } while (col != 0); + } +}; + +namespace ink::algorithms +{ +float hungarian_solver(const float* cost, int* matches, size_t n, float threshold) +{ + HungarienCtx ctx(cost, n); + for (size_t row = 1; row <= n; ++row) { + ctx.init_search(static_cast(row)); + int end_col = ctx.find_augmenting_path(); + ctx.augment_matching(end_col); + } + + float total_cost = 0; + for (size_t col = 1; col <= n; ++col) { + int row = ctx[col] - 1; + matches[row] = col - 1; + total_cost += cost[row * n + matches[row]]; + if (threshold != 0 && cost[row * n + matches[row]] >= threshold) { + matches[row] = -1; + } + } + return total_cost; +} + +float jaro_simularity(const char* lh, const char* rh) +{ + const size_t lh_len = static_cast(strlen(lh)); + uint8_t lh_matched[256] = {}; + if (lh_len > sizeof(lh_matched) * 8) { + return 0; + } + const size_t rh_len = static_cast(strlen(rh)); + uint8_t rh_matched[256] = {}; + if (rh_len > sizeof(rh_matched) * 8) { + return 0; + } + + if ((lh_len == 1 && rh_len == 1) || lh_len == 0 || rh_len == 0) { + return 0; + } + size_t max_offset = (std::max(lh_len, rh_len) / 2) - 1; + float m = 0; + + for (int lh_idx = 0; static_cast(lh_idx) < lh_len; ++lh_idx) { + for (int rh_idx = std::max(lh_idx - static_cast(max_offset), 0); + static_cast(rh_idx) <= std::min(rh_len, lh_idx + max_offset); ++rh_idx) { + if (! (rh_matched[rh_idx / 8] & (1 << (rh_idx & 7))) + && tolower(rh[rh_idx]) == tolower(lh[lh_idx])) { + lh_matched[lh_idx / 8] |= 1 << (lh_idx & 7); + rh_matched[rh_idx / 8] |= 1 << (rh_idx & 7); + m += 1.; + break; + } + } + } + + if (m == 0) { + return 0; + } + int rh_idx = 0; + float t = 0; + for (int lh_idx = 0; static_cast(lh_idx) < lh_len; ++lh_idx) { + if (lh_matched[lh_idx / 8] & (1 << (lh_idx & 7))) { + int next_idx = rh_idx; + while (static_cast(next_idx) < rh_len) { + if (rh_matched[next_idx / 8] & 1 << (next_idx & 7)) { + rh_idx = next_idx + 1; + break; + } + next_idx += 1; + } + if (tolower(lh[lh_idx]) != tolower(rh[next_idx])) { + t += 1.; + } + } + } + t /= 2.; + return ((m / lh_len) + (m / rh_len) + ((m - t) / m)) / 3.f; +} + +static constexpr float P = 0.1f; + +float jaro_winkler_simularity(const char* lh, const char* rh) +{ + float j = jaro_simularity(lh, rh); + int l = 0; + const char *l_iter, *r_iter; + // calculate length of common prefix + for (l_iter = lh, r_iter = rh; *l_iter && *r_iter && *l_iter == *r_iter; ++lh, ++rh) { + l += 1; + if (l == 4) { + break; + } + } + return j + l * P * (1 - j); +} +} // namespace ink::algorithms diff --git a/inkcpp/hungarian_solver.h b/inkcpp/hungarian_solver.h new file mode 100644 index 00000000..e4a53467 --- /dev/null +++ b/inkcpp/hungarian_solver.h @@ -0,0 +1,38 @@ +#pragma once + +#include "system.h" + +namespace ink::algorithms +{ + +/** Jaro Similarity of two null terminated byte strings. + * supports ASCII encoding, UTF-8 might be broken. + * ignores case. + * https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance#Jaro_similarity + * @param lh,rh null terminated byte strings to compare + * @return similarity between lh and rh + * @retval 1 if equal + */ +float jaro_simularity(const char* lh, const char* rh); + +/** Jaro Winkler Similarity of two null terminated byte strings. + * supports ASCII encoding, UTF-8 might be broken. + * ignores case. + * https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance#Jaro%E2%80%93Winkler_similarity + * @param lh,rh null terminated byte strings to compare + * @sa jaro_simularity + * @return similarity between lh and rh + * @retval 1 if equal + */ +float jaro_winkler_simularity(const char* lh, const char* rh); + +/** Hungarian Algorithm to solve an assignment problem in O(N3). + * https://en.wikipedia.org/wiki/Hungarian_algorithm + * @param[in] cost matrix m x n + * @param[out] matches optimal mapping m -> n + * @param threshold matches with a value higher than threshold will be set to `-1`, use 0 to ignore. + * @param n number of jobs/assignments + * @return total cost of assigment + */ +float hungarian_solver(const float* cost, int* matches, size_t n, float threshold = 0); +} // namespace ink::algorithms diff --git a/inkcpp/include/list.h b/inkcpp/include/list.h index 7a3ee7ee..5f7cf42c 100644 --- a/inkcpp/include/list.h +++ b/inkcpp/include/list.h @@ -48,6 +48,8 @@ class list_interface { } + virtual list_interface& operator=(const list_interface&) = default; + virtual ~list_interface() {} /** iterater for flags in a list diff --git a/inkcpp/include/snapshot.h b/inkcpp/include/snapshot.h index 6cede405..5b8879f9 100644 --- a/inkcpp/include/snapshot.h +++ b/inkcpp/include/snapshot.h @@ -19,6 +19,35 @@ namespace ink::runtime * will be identical. If multiple runners are associated to the same globals all will be contained, * and cann be reconsrtucted with the id parameter of @ref * ink::runtime::story::new_runner_from_snapshot() + * A snapshot can be applied to an identical story file or an simulare if the snapshot is @ref + * ink::runtime::snapshot::can_be_migrated() "@c can_be_migrated()". + * A not migrated snapshot contiouse at exactly the place you are currently at. + * + * **A migrated one will "snap bag" to the last knot.** + * + * + Global variables which (name) still exist will be transfared. + * + New ones will be initelized with its default value + * + Old ones will be droped + * + Temp variables which (name) still exist will be tranfared + * + new ones will be initelized with its default vaule (possible missing transformations) + * + old ones will be kept + * + **attention** declarations in Tunnels will be missed + * + Stack/Threads/Tunnels must not be used in the moment of the snapshot for it to be migratable. + * + best practice is to create hub knots which names do not change in the progress of the update + * and which do not have local variables. Then only store after you stepped inside this knot. + * + Lists definitions are matched after best knowladge + * + for each pair of old list value and new list value the best good matching is used + * + the similarty is calculated based on the jaro-winkler similiarty of the value names, and + * the normalized difference of the values + * + for each pair of old and new list (definition) the best good match is taken + * + the similiraty is calculated based on the jaccard similiraty of the contained flags, and + * the jaro-winkler similarty of the lists names + * + visit counts (e.g. used for once only choices) + * + existing ones (exact name match) are kept + * + !! a choice with no explicit label is tracked via its position in the list, reordering + * choices can therfore break your visit counts + * + new ones are zero + * + old ones are discarded * * @todo Currently the id is equal to the creation order, a way to name the single runner/threads is * WIP @@ -39,11 +68,13 @@ class snapshot static snapshot* from_binary(const unsigned char* data, size_t length, bool freeOnDestroy = true); /** access blob inside snapshot */ - virtual const unsigned char* get_data() const = 0; + virtual const unsigned char* get_data() const = 0; /** size of blob inside snapshot */ - virtual size_t get_data_len() const = 0; + virtual size_t get_data_len() const = 0; /** number of runners which are stored inside this snapshot */ - virtual size_t num_runners() const = 0; + virtual size_t num_runners() const = 0; + /** if this snapshot can be migrated, if the story file changes (slightly). */ + virtual bool can_be_migrated() const = 0; #ifdef INK_ENABLE_STL /** deserialize snapshot from file. diff --git a/inkcpp/include/story.h b/inkcpp/include/story.h index 06591779..bc3fba71 100644 --- a/inkcpp/include/story.h +++ b/inkcpp/include/story.h @@ -69,6 +69,12 @@ class story virtual runner new_runner_from_snapshot(const snapshot& obj, globals store = nullptr, unsigned runner_id = 0) = 0; + + /** + * @brief hash of binary/story. + * used to check for story changes. + */ + virtual hash_t hash() const = 0; #pragma endregion #pragma region Factory Methods diff --git a/inkcpp/list_table.cpp b/inkcpp/list_table.cpp index 703de027..b329c1f5 100644 --- a/inkcpp/list_table.cpp +++ b/inkcpp/list_table.cpp @@ -6,12 +6,14 @@ */ #include "list_table.h" #include "config.h" +#include "hungarian_solver.h" #include "system.h" #include "traits.h" #include "header.h" #include "random.h" #include "string_utils.h" #include "list_impl.h" +#include #ifdef INK_ENABLE_STL # include @@ -74,7 +76,7 @@ list_table::list list_table::create() } list new_entry(_entry_state.size()); - // TODO: initelized unused? + // TODO: initialized unused? _entry_state.push() = state::used; for (int i = 0; i < _entrySize; ++i) { _data.push() = 0; @@ -82,6 +84,24 @@ list_table::list list_table::create() return new_entry; } +list_table::list list_table::create_at(size_t idx) +{ + if (idx < _entry_state.size()) { + if (_entry_state[idx] == state::empty) { + _entry_state[idx] = state::used; + return list(idx); + } + return list(-1); + } + while (_entry_state.size() <= idx) { + _entry_state.push() = state::empty; + for (int i = 0; i < _entrySize; ++i) { + _data.push() = 0; + } + } + return list(idx); +} + void list_table::clear_usage() { for (state& s : _entry_state) { @@ -269,6 +289,15 @@ list_table::list list_table::create_permament() return res; } +list_table::list list_table::create_permament_at(size_t idx) +{ + list res = create_at(idx); + if (res.lid >= 0) { + _entry_state[res.lid] = state::permanent; + } + return res; +} + list_table::list& list_table::add_inplace(list& lh, list_flag rh) { if (rh.list_id < 0) @@ -858,4 +887,245 @@ config::statistics::list_table list_table::statistics() const }; } +/** Distance of two lists based on their contained values. + * https://en.wikipedia.org/wiki/Jaccard_index + * @param lh,rh flag indexes contained in the lists + * @param matches mapping from lh -> rh, -1 for dropped + */ +float d_contains(const size_t lh[2], const size_t rh[2], const int* matches) +{ + int n_union = (lh[1] - lh[0]) + (rh[1] - rh[0]); + int n_intersection = 0; + for (size_t i = lh[0]; i < lh[1]; ++i) { + if (matches[i] == -1) { + continue; + } + if (static_cast(matches[i]) >= rh[0] && static_cast(matches[i]) < rh[1]) { + n_intersection += 1; + } + } + n_union -= n_intersection; + return static_cast(n_intersection) / n_union; +} + +/** Distance function for string labels. + * @param lh,rh null terminated ASCII strings to compare + * @return 0 if identical + */ +float d_label(const char* lh, const char* rh) +{ + return 1.f - algorithms::jaro_winkler_simularity(lh, rh); +} + +/** Distance function for two values. + * @param lh,rh numeric values to compare + * @param lh_range,rh_range min/max value of the number + * @returns 0 if identical + */ +float d_value(int lh, int rh, int lh_range[2], int rh_range[2]) +{ + if (lh == rh) { + return 0; + } + float res = (static_cast(lh) - lh_range[0]) / (lh_range[1] - lh_range[0]); + res -= (static_cast(rh) - rh_range[0]) / (rh_range[1] - rh_range[0]); + if (res < 0) { + res = -res; + } + return res; +} + +struct MatchList { + const size_t* list_ends; + const char* const* names; + size_t length; +}; + +struct MatchListValues { + const char* const* names; + const int* values; + size_t length; +}; + +void get_range(const MatchListValues& values, int range[2]) +{ + range[0] = std::numeric_limits::max(); + range[1] = std::numeric_limits::min(); + for (size_t i = 0; i < values.length; ++i) { + if (values.values[i] < range[0]) { + range[0] = values.values[i]; + } + if (values.values[i] > range[1]) { + range[1] = values.values[i]; + } + } +} + +float* cost_matrix( + const MatchList& lh, const MatchList& rh, const int* value_matches, float drop_penalty +) +{ + size_t n_lists = lh.length > rh.length ? lh.length : rh.length; + float* matrix = new float[n_lists * n_lists]; + for (size_t i = 0; i < lh.length; ++i) { + for (size_t j = 0; j < rh.length; ++j) { + float dl = d_label(lh.names[i], rh.names[j]); + size_t lh_range[] = {i == 0 ? 0 : lh.list_ends[i - 1], lh.list_ends[i]}; + size_t rh_range[] = {j == 0 ? 0 : rh.list_ends[j - 1], rh.list_ends[j]}; + float dv = d_contains(lh_range, rh_range, value_matches); + matrix[i * n_lists + j] = dv * 0.8f + dl * 0.2f; + } + for (size_t j = rh.length; j < n_lists; ++j) { + matrix[i * n_lists + j] = drop_penalty; + } + } + for (size_t i = lh.length; i < n_lists; ++i) { + for (size_t j = 0; j < n_lists; ++j) { + matrix[i * n_lists + j] = drop_penalty; + } + } + return matrix; +} + +float* cost_matrix(const MatchListValues& lh, const MatchListValues& rh, float drop_penalty) +{ + size_t n_flags = lh.length > rh.length ? lh.length : rh.length; + float* matrix = new float[n_flags * n_flags]; + int lh_range[2], rh_range[2]; + get_range(lh, lh_range); + get_range(rh, rh_range); + + for (size_t i = 0; i < lh.length; ++i) { + for (size_t j = 0; j < rh.length; ++j) { + float dl = d_label(lh.names[i], rh.names[j]); + float dv = d_value(lh.values[i], rh.values[j], lh_range, rh_range); + matrix[i * n_flags + j] = dl * 0.8f + dv * 0.2f; + } + for (size_t j = rh.length; j < n_flags; ++j) { + matrix[i * n_flags + j] = drop_penalty; + } + } + for (size_t i = lh.length; i < n_flags; ++i) { + for (size_t j = 0; j < rh.length; ++j) { + matrix[i * n_flags + j] = drop_penalty; + } + } + return matrix; +} + +bool list_table::migrate(const char* old_list_metadata, const ink::internal::header& header) +{ + list_table old_ref_table(old_list_metadata, header); + for (const auto& x : _data) { + old_ref_table._data.push() = x; + } + for (const auto& x : _entry_state) { + old_ref_table._entry_state.push() = x; + } + _data.clear(); + _entry_state.clear(); + + // find best mapping between old and new list elements + // + c_ij(value) = min(|v_i - v_j|/Rv,1) + // + c_ij(name) = levenshtein, cosine n-grams, jaro-winkler + // + c_ij(position_in_list) = min(|p_i - p_j|/Rp, 1) + // find best mapping between lists + // + c_ij(name) = levenshtein, cosine n-grams, jaro-winkler + // + c_ij(entries) = entries existing in both + // 1. h_entry_map = high confidents mapping of list elements (value, name, position_in_list) + // 2. h_list_map = high confident mapping of lists (name, h_entry_map) + // 3. entry_map = mapping of list elements (value, name, position_in_list, + // h_list_map[list_name]) + // 4. list_map = mapping of lists (name, entry_map) + + + // high confidance list value matches + constexpr float HIGH_CONFIDANCE_DROP_PANELTY = 0.3f; + constexpr float LOW_CONFIDANCE_DROP_PANELTY = 0.6f; + float* value_matrix = cost_matrix( + MatchListValues{ + old_ref_table._flag_names.data(), old_ref_table._flag_values.data(), + old_ref_table.numFlags() + }, + MatchListValues{_flag_names.data(), _flag_values.data(), numFlags()}, + LOW_CONFIDANCE_DROP_PANELTY + ); + const int n_flags = std::max(numFlags(), old_ref_table.numFlags()); + int* value_matches = new int[n_flags]; + algorithms::hungarian_solver(value_matrix, value_matches, n_flags, HIGH_CONFIDANCE_DROP_PANELTY); + + // list matches + float* list_matrix = cost_matrix( + MatchList{ + old_ref_table._list_end.data(), old_ref_table._list_names.data(), old_ref_table.numLists() + }, + MatchList{_list_end.data(), _list_names.data(), numLists()}, value_matches, + LOW_CONFIDANCE_DROP_PANELTY + ); + const int n_lists = std::max(numLists(), old_ref_table.numLists()); + int* list_matches = new int[n_lists]; + algorithms::hungarian_solver(list_matrix, list_matches, n_lists, LOW_CONFIDANCE_DROP_PANELTY); + + // low confidence list_value matches + algorithms::hungarian_solver(value_matrix, value_matches, n_flags, LOW_CONFIDANCE_DROP_PANELTY); + + for (size_t idx = 0; idx < old_ref_table._entry_state.size(); ++idx) { + // migrate + list new_list{-1}; + switch (old_ref_table._entry_state[idx]) { + case state::permanent: new_list = create_permament_at(idx); break; + case state::used: new_list = create_at(idx); break; + default: continue; + } + inkAssert(new_list.lid >= 0, "Failed to create new list entry for migration."); + inkAssert( + static_cast(new_list.lid) == idx, + "At position list creation failed with different valid idx." + ); + const data_t* entry = old_ref_table.getPtr(idx); + data_t* new_entry = getPtr(idx); + bool migrated = false; + bool is_empty_list = true; + for (size_t i = 0; i < old_ref_table.numLists(); ++i) { + if (old_ref_table.hasList(entry, i)) { + bool hit = false; + is_empty_list = false; + for (size_t j = old_ref_table.listBegin(i); j < old_ref_table._list_end[i]; ++j) { + if (old_ref_table.hasFlag(entry, j) && old_ref_table._flag_names[j]) { + if (value_matches[j] != -1) { + hit = true; + migrated = true; + size_t k; + for (k = 0; _list_end[k] < static_cast(value_matches[j]); ++k) {} + setList(new_entry, k); + setFlag(new_entry, value_matches[j]); + } + } + } + // keep list if list has match but all values where dropped + if (! hit && list_matches[i] != -1) { + setList(new_entry, list_matches[i]); + migrated = true; + } + } + } + // drop list + if (! is_empty_list && ! migrated) { + // FIXME: remove list ? + // _entry_state [idx] = state::empty; + return false; + } + // FIXME: use Assert instead? + // inkAssert(migrated, "Migrating list @%d would lead to an empty list", idx); + } + + + delete[] list_matches; + delete[] value_matches; + delete[] value_matrix; + delete[] list_matrix; + return true; +} + + } // namespace ink::runtime::internal diff --git a/inkcpp/list_table.h b/inkcpp/list_table.h index d88a31af..7edbbb38 100644 --- a/inkcpp/list_table.h +++ b/inkcpp/list_table.h @@ -113,8 +113,9 @@ class list_table : public snapshot_interface list create_permament(); list& add_inplace(list& lh, list_flag rh); - // parse binary list meta data list_table(const char* data, const ink::internal::header&); + // binary list metadata of currently loaded list + bool migrate(const char* old_list_metadata, const ink::internal::header& header); explicit list_table() : _entrySize{0} @@ -147,9 +148,11 @@ class list_table : public snapshot_interface size_t snap(unsigned char* data, const snapper&) const; const unsigned char* snap_load(const unsigned char* data, const loader&); - /** special traitment when a list get assignet again - * when a list get assigned and would have no origin, it gets the origin of the base with origin - * eg. I072 + bool can_be_migrated() const { return _list_handouts.size() == 0; } + + /** special treatment when a list gets assigned again + * when a list gets assigned and would have no origin, it gets the origin of the base with origin + * e.g. I072 */ list redefine(list lh, list rh); @@ -263,6 +266,16 @@ class list_table : public snapshot_interface list_interface* handout_list(list); private: + /** create a list with id == idx. + * @attention used for migration only + * @sa create() + */ + list create_at(size_t idx); + /** create permanent list list id == idx. + * @attention used for migration only + * @se create_permenant_at() + */ + list create_permament_at(size_t idx); void copy_lists(const data_t* src, data_t* dst); static constexpr size_t bits_per_data = sizeof(data_t) * 8U; @@ -361,6 +374,11 @@ class list_table : public snapshot_interface // entries (created lists) managed_array _data; managed_array _entry_state; + // parse binary list metadata + list_table( + const char* data, const ink::internal::header&, const decltype(_data)& values, + const decltype(_entry_state)& state + ); // defined list (meta data) managed_array _list_end; @@ -378,13 +396,17 @@ class list_table : public snapshot_interface class named_flag_itr { + public: + struct position { + list_flag flag; + const char* name; + }; + + private: const list_table& _list; const data_t* _data; - struct { - list_flag flag; - const char* name; - } _pos; + position _pos; /** carry list change. * if the iterator incremented to the next flag, also increment the list if necessary @@ -449,7 +471,11 @@ class list_table : public snapshot_interface , _pos{null_flag, nullptr} {}; named_flag_itr(const list_table& list, const data_t* filter, int) - : _list{list}, _data{filter}, _pos{{0,0},list._flag_names[0]} + : _list{ + list + } + , _data{filter} + , _pos{{0, 0}, list._flag_names[0]} { goToValid(); } diff --git a/inkcpp/output.cpp b/inkcpp/output.cpp index f5e71632..164768f5 100644 --- a/inkcpp/output.cpp +++ b/inkcpp/output.cpp @@ -516,6 +516,18 @@ basic_stream& operator>>(basic_stream& in, FString& out) return in; } #endif +bool basic_stream::can_be_migrated() const +{ + if (saved()) { + return false; + } + for (size_t i = 0; i < _size; ++i) { + if (! _data[i].can_be_migrated()) { + return false; + } + } + return true; +} size_t basic_stream::snap(unsigned char* data, const snapper& snapper) const { diff --git a/inkcpp/output.h b/inkcpp/output.h index eb9b2e8e..ae2f58a9 100644 --- a/inkcpp/output.h +++ b/inkcpp/output.h @@ -127,6 +127,7 @@ namespace runtime char last_char() const { return _last_char; } // snapshot interface + bool can_be_migrated() const; size_t snap(unsigned char* data, const snapper&) const; const unsigned char* snap_load(const unsigned char* data, const loader&); diff --git a/inkcpp/runner_impl.cpp b/inkcpp/runner_impl.cpp index 996eec31..b39f91f5 100644 --- a/inkcpp/runner_impl.cpp +++ b/inkcpp/runner_impl.cpp @@ -137,6 +137,7 @@ void runner_impl::set_var( } else { if (is_redef) { value* src = _stack.get(variableName); + inkAssert(src != nullptr, "Tried to redefine a non existing local variable."); if (src->type() == value_type::value_pointer) { auto [name, ci] = src->get(); inkAssert(ci == 0, "Only global pointer are allowed on _stack!"); @@ -166,29 +167,32 @@ void runner_impl::set_var( } template -inline T runner_impl::read() +inline T runner_impl::read(optional pos) { using header = ink::internal::header; + ip_t ptr = pos.value_or(_ptr); // Sanity - inkAssert(_ptr + sizeof(T) <= _story->end(), "Unexpected EOF in Ink execution"); + inkAssert(ptr + sizeof(T) <= _story->end(), "Unexpected EOF in Ink execution"); // Read memory - T val = *( const T* ) _ptr; + T val = *( const T* ) ptr; if (_story->get_header().endien == header::endian_types::differ) { val = header::swap_bytes(val); } // Advance ip - _ptr += sizeof(T); + if (! pos.has_value()) { + _ptr += sizeof(T); + } // Return return val; } template<> -inline const char* runner_impl::read() +inline const char* runner_impl::read(optional pos) { - offset_t str = read(); + offset_t str = read(pos); return _story->string(str); } @@ -296,6 +300,26 @@ void runner_impl::clear_tags(tags_clear_level which) } } +void runner_impl::fetch_tags(ip_t begin) +{ + ip_t iter = begin; + if (read(iter) == Command::START_CONTAINER_MARKER) { + iter += 6; + } + while (read(iter) == Command::START_TAG) { + // skip non-trivial tags (constant string only) + if (read(iter + 6) != Command::STR || read(iter + 12) != Command::END_TAG) { + while (read(iter) != Command::END_TAG) { + iter += 6; + } + iter += 6; + continue; + } + add_tag(read(iter + 6 + 2), tags_level::UNKNOWN); + iter += 18; + } +} + void runner_impl::jump(ip_t dest, bool record_visits, bool track_knot_visit) { // Optimization: if we are _is_falling, then we can @@ -636,11 +660,30 @@ void runner_impl::getline_silent() snapshot* runner_impl::create_snapshot() const { return _globals->create_snapshot(); } +bool runner_impl::can_be_migrated() const +{ + if (_choices.size()) { + return false; + } + if (_entered_knot) { + return false; + } + hash_t c_hash = _story->container_hash(_ptr - 6); + if (c_hash == 0) { + return false; + } + return _output.can_be_migrated() && _stack.can_be_migrated() && _ref_stack.can_be_migrated() + && _eval.can_be_migrated() && _tags_begin.can_be_migrated() && _tags.can_be_migrated() + && _container.can_be_migrated() && _threads.can_be_migrated() && _choices.can_be_migrated(); +} + size_t runner_impl::snap(unsigned char* data, snapper& snapper) const { unsigned char* ptr = data; bool should_write = data != nullptr; std::uintptr_t offset = _ptr != nullptr ? _ptr - _story->instructions() : 0; + // TODO: remove + ptr = snap_write(ptr, _story->container_hash(_ptr - 6), should_write); ptr = snap_write(ptr, offset, should_write); offset = _backup - _story->instructions(); ptr = snap_write(ptr, offset, should_write); @@ -661,8 +704,13 @@ size_t runner_impl::snap(unsigned char* data, snapper& snapper) const snapper.runner_tags = _tags.data(); ptr = snap_write(ptr, _entered_global, should_write); ptr = snap_write(ptr, _entered_knot, should_write); - ptr = snap_write(ptr, _current_knot_id, should_write); - ptr = snap_write(ptr, _current_knot_id_backup, should_write); + ptr = snap_write(ptr, get_current_knot(), should_write); + if (_current_knot_id_backup != ~0U) { + ptr = snap_write(ptr, _story->container_hash(_current_knot_id_backup), should_write); + } else { + hash_t none = 0; + ptr = snap_write(ptr, none, should_write); + } ptr += _container.snap(data ? ptr : nullptr, snapper); ptr += _threads.snap(data ? ptr : nullptr, snapper); ptr = snap_write(ptr, _fallback_choice.has_value(), should_write); @@ -677,6 +725,9 @@ const unsigned char* runner_impl::snap_load(const unsigned char* data, loader& l { auto ptr = data; std::uintptr_t offset; + hash_t current_knot_name; + // TODO: remove + ptr = snap_read(ptr, current_knot_name); ptr = snap_read(ptr, offset); _ptr = offset == 0 ? nullptr : _story->instructions() + offset; ptr = snap_read(ptr, offset); @@ -700,10 +751,23 @@ const unsigned char* runner_impl::snap_load(const unsigned char* data, loader& l loader.runner_tags = _tags.data(); ptr = snap_read(ptr, _entered_global); ptr = snap_read(ptr, _entered_knot); - ptr = snap_read(ptr, _current_knot_id); - ptr = snap_read(ptr, _current_knot_id_backup); - ptr = _container.snap_load(ptr, loader); - ptr = _threads.snap_load(ptr, loader); + _current_knot_id = ~0U; + ptr = snap_read(ptr, current_knot_name); + if (current_knot_name) { + bool found + = _story->get_container_id(_story->find_offset_for(current_knot_name), _current_knot_id); + inkAssert(found, "Unable to find current knot in migrated story."); + } + _current_knot_id_backup = ~0U; + ptr = snap_read(ptr, current_knot_name); + if (current_knot_name) { + bool found = _story->get_container_id( + _story->find_offset_for(current_knot_name), _current_knot_id_backup + ); + inkAssert(found, "Unable to find current knot backup in migration %u", current_knot_name); + } + ptr = _container.snap_load(ptr, loader); + ptr = _threads.snap_load(ptr, loader); bool has_fallback_choice; ptr = snap_read(ptr, has_fallback_choice); _fallback_choice = nullopt; @@ -738,11 +802,53 @@ bool runner_impl::move_to(hash_t path) // Clear state and move to destination reset(); _ptr = _story->instructions(); - jump(destination, false, true); + jump(destination, false, false); return true; } +bool runner_impl::migrate_to(hash_t path) +{ + ip_t destination = _story->find_offset_for(path); + if (destination == nullptr) { + return false; + } + clear_tags(tags_clear_level::KEEP_NONE); + fetch_tags(_story->instructions()); + assign_tags({tags_level::GLOBAL}); + if (_current_knot_id != ~0U) { + ip_t start_of_knot = _story->find_offset_for(_story->container_hash(_current_knot_id)); + fetch_tags(start_of_knot); + assign_tags({tags_level::KNOT}); + if (start_of_knot != destination) { + for (ip_t iter = start_of_knot; iter != destination; iter += 6) { + if (read(iter) == Command::DEFINE_TEMP) { + hash_t temp_name = read(iter + 2); + if (get_var(temp_name) == nullptr) { + ip_t eval_start = iter - 6; + inkAssert( + read(eval_start) == Command::END_EVAL, + "expected an evaluation segment before defininng a temporary variable" + ); + while (read(eval_start) != Command::START_EVAL) { + eval_start -= 6; + } + jump(eval_start, false, false); + while (_ptr != iter + 6) { + step(); + } + } + } + } + } + } + // rebuild container stack to display new offsets + _container.clear(); + _ptr = nullptr; + jump(destination, false, true); + return true; +} + void runner_impl::internal_bind(hash_t name, internal::function_base* function) { _functions.add(name, function); @@ -893,6 +999,7 @@ void runner_impl::step() set_done_ptr(nullptr); } if (cmd >= Command::OP_BEGIN && cmd < Command::OP_END) { + read(); _operations(cmd, _eval); } else { switch (cmd) { @@ -996,6 +1103,7 @@ void runner_impl::step() _eval.push(value{}.set(target)); } break; case Command::NEWLINE: { + read(); if (_evaluation_mode) { _eval.push(values::newline); } else { @@ -1005,6 +1113,7 @@ void runner_impl::step() } } break; case Command::GLUE: { + read(); if (_evaluation_mode) { _eval.push(values::glue); } else { @@ -1012,6 +1121,7 @@ void runner_impl::step() } } break; case Command::VOID: { + read(); if (_evaluation_mode) { _eval.push(values::null); // TODO: void type? } @@ -1095,9 +1205,15 @@ void runner_impl::step() } break; // == Terminal commands - case Command::DONE: on_done(true); break; + case Command::DONE: + read(); + on_done(true); + break; - case Command::END: _ptr = nullptr; break; + case Command::END: + read(); + _ptr = nullptr; + break; // == Tunneling case Command::TUNNEL: { @@ -1154,10 +1270,12 @@ void runner_impl::step() } break; case Command::TUNNEL_RETURN: case Command::FUNCTION_RETURN: { + read(); execute_return(); } break; case Command::THREAD: { + read(); // Push a thread frame so we can return easily // TODO We push ahead of a single divert. Is that correct in all cases....????? auto returnTo = _ptr + CommandSize; @@ -1259,18 +1377,29 @@ void runner_impl::step() } break; // == Evaluation stack - case Command::START_EVAL: _evaluation_mode = true; break; + case Command::START_EVAL: + read(); + _evaluation_mode = true; + break; case Command::END_EVAL: + read(); _evaluation_mode = false; // Assert stack is empty? Is that necessary? break; case Command::OUTPUT: { + read(); value v = _eval.pop(); _output << v; } break; - case Command::POP: _eval.pop(); break; - case Command::DUPLICATE: _eval.push(_eval.top_value()); break; + case Command::POP: + read(); + _eval.pop(); + break; + case Command::DUPLICATE: + read(); + _eval.push(_eval.top_value()); + break; case Command::PUSH_VARIABLE_VALUE: { // Try to find in local stack hash_t variableName = read(); @@ -1289,12 +1418,14 @@ void runner_impl::step() break; } case Command::START_STR: { + read(); inkAssert(_evaluation_mode, "Can not enter string mode while not in evaluation mode!"); _string_mode = true; _evaluation_mode = false; _output << values::marker; } break; case Command::END_STR: { + read(); // TODO: Assert we really had a marker on there? inkAssert(! _evaluation_mode, "Must be in evaluation mode"); _string_mode = false; @@ -1309,11 +1440,13 @@ void runner_impl::step() // == Tag commands case Command::START_TAG: { + read(); _output << values::marker; } break; case Command::END_TAG: { + read(); auto tag = _output.get_alloc(_globals->strings(), _globals->lists()); add_tag(tag, tags_level::UNKNOWN); } break; @@ -1445,6 +1578,7 @@ void runner_impl::step() } } break; case Command::VISIT: { + read(); // Push the visit count for the current container to the top // is 0-indexed for some reason. idk why but this is what ink expects _eval.push(value{}.set( @@ -1452,9 +1586,11 @@ void runner_impl::step() )); } break; case Command::TURN: { + read(); _eval.push(value{}.set(static_cast(_globals->turns()))); } break; case Command::SEQUENCE: { + read(); // TODO: The C# ink runtime does a bunch of fancy logic // to make sure each element is picked at least once in every // iteration loop. I don't feel like replicating that right now. @@ -1468,6 +1604,7 @@ void runner_impl::step() ); } break; case Command::SEED: { + read(); int32_t seed = _eval.pop().get(); _rng.srand(seed); @@ -1489,6 +1626,7 @@ void runner_impl::step() ))); } break; case Command::TAG: { + read(); add_tag(read(), tags_level::UNKNOWN); } break; default: inkAssert(false, "Unrecognized command!"); break; diff --git a/inkcpp/runner_impl.h b/inkcpp/runner_impl.h index 446d837d..c35e1d57 100644 --- a/inkcpp/runner_impl.h +++ b/inkcpp/runner_impl.h @@ -123,6 +123,7 @@ class runner_impl size_t snap(unsigned char* data, snapper&) const; const unsigned char* snap_load(const unsigned char* data, loader&); + bool can_be_migrated() const; // c-style getline virtual const char* getline_alloc() override; @@ -130,6 +131,9 @@ class runner_impl // move to path virtual bool move_to(hash_t path) override; + // move to path but keep as much state as possible + bool migrate_to(hash_t path); + #if defined(INK_ENABLE_STL) || defined(INK_ENABLE_UNREAL) // Gets a single line of output virtual line_type getline() override; @@ -193,7 +197,7 @@ class runner_impl private: template - inline T read(); + inline T read(optional pos = nullopt); choice& add_choice(); void clear_choices(); @@ -208,6 +212,8 @@ class runner_impl KEEP_KNOT, ///< keep knot and global tags }; void clear_tags(tags_clear_level which); + // Fetch string only tags at Tag/Global level + void fetch_tags(ip_t begin); // Special code for jumping from the current IP to another void jump(ip_t, bool record_visits, bool track_knot_visit); @@ -396,7 +402,7 @@ const unsigned char* } template<> -inline const char* runner_impl::read(); +inline const char* runner_impl::read(optional); template bool runner_impl::has_tags() const diff --git a/inkcpp/simple_restorable_stack.h b/inkcpp/simple_restorable_stack.h index 12ce3d40..db7f5138 100644 --- a/inkcpp/simple_restorable_stack.h +++ b/inkcpp/simple_restorable_stack.h @@ -45,10 +45,13 @@ class simple_restorable_stack : public snapshot_interface bool rev_iter(const T*& iterator) const; // == Save/Restore == + bool is_saved() const { return _save != InvalidIndex; } + void save(); void restore(); void forget(); + virtual bool can_be_migrated() const; virtual size_t snap(unsigned char* data, const snapper&) const; virtual const unsigned char* snap_load(const unsigned char* data, const loader&); @@ -290,6 +293,12 @@ inline void simple_restorable_stack::forget() _save = _jump = InvalidIndex; } +template +bool simple_restorable_stack::can_be_migrated() const +{ + return ! is_saved(); +} + template size_t simple_restorable_stack::snap(unsigned char* data, const snapper&) const { diff --git a/inkcpp/snapshot_impl.cpp b/inkcpp/snapshot_impl.cpp index a3bd6604..7c6f2a3a 100644 --- a/inkcpp/snapshot_impl.cpp +++ b/inkcpp/snapshot_impl.cpp @@ -27,7 +27,7 @@ snapshot* snapshot::from_file(const char* filename) { std::ifstream ifs(filename, std::ios::binary | std::ios::ate); if (! ifs.is_open()) { - ink_assert(false, "Failed to open snapshot file: %s", filename); + inkAssert(false, "Failed to open snapshot file: %s", filename); } size_t length = static_cast(ifs.tellg()); @@ -43,7 +43,7 @@ void snapshot::write_to_file(const char* filename) const { std::ofstream ofs(filename, std::ios::binary); if (! ofs.is_open()) { - ink_assert(false, "Failed to open file to write snapshot: %s", filename); + inkAssert(false, "Failed to open file to write snapshot: %s", filename); } ofs.write(reinterpret_cast(get_data()), get_data_len()); } @@ -52,9 +52,16 @@ void snapshot::write_to_file(const char* filename) const namespace ink::runtime::internal { -size_t snapshot_impl::file_size(size_t serialization_length, size_t runner_cnt) +size_t + snapshot_impl::file_size(size_t serialization_length, size_t runner_cnt, bool list_definition) { - return serialization_length + sizeof(header) + (runner_cnt + 1) * sizeof(size_t); + return serialization_length + sizeof(header) + + (runner_cnt + 1 + (list_definition ? 1 : 0)) * sizeof(size_t); +} + +bool snapshot_impl::can_be_migrated(const story& story) const +{ + return can_be_migrated() || (story.hash() == _header.hash); } const unsigned char* snapshot_impl::get_data() const { return _file; } @@ -65,16 +72,24 @@ snapshot_impl::snapshot_impl(const globals_impl& globals) : _managed{true} { snapshot_interface::snapper snapper(globals.strings(), globals._owner->string(0)); - _length = globals.snap(nullptr, snapper); - size_t runner_cnt = 0; + bool migratable = globals.can_be_migrated(); + size_t runner_cnt = 0; + + _length = globals.snap(nullptr, snapper); for (auto node = globals._runners_start; node; node = node->next) { _length += node->object->snap(nullptr, snapper); + migratable = migratable && node->object->can_be_migrated(); ++runner_cnt; } + if (migratable) { + _length += globals._owner->list_meta_size(); + } - _length = file_size(_length, runner_cnt); + _length = file_size(_length, runner_cnt, migratable); _header.length = _length; _header.num_runners = runner_cnt; + _header.hash = globals._owner->hash(); + _header.migratable = migratable; unsigned char* data = new unsigned char[_length]; _file = data; unsigned char* ptr = data; @@ -83,7 +98,9 @@ snapshot_impl::snapshot_impl(const globals_impl& globals) // write lookup table ptr += sizeof(header); { - size_t offset = static_cast((ptr - data) + (_header.num_runners + 1) * sizeof(size_t)); + size_t offset = static_cast( + (ptr - data) + (_header.num_runners + 1 + migratable) * sizeof(size_t) + ); memcpy(ptr, &offset, sizeof(offset)); ptr += sizeof(offset); offset += globals.snap(nullptr, snapper); @@ -92,12 +109,21 @@ snapshot_impl::snapshot_impl(const globals_impl& globals) ptr += sizeof(offset); offset += node->object->snap(nullptr, snapper); } + if (migratable) { + memcpy(ptr, &offset, sizeof(offset)); + ptr += sizeof(offset); + offset += globals._owner->list_meta_size(); + } } ptr += globals.snap(ptr, snapper); for (auto node = globals._runners_start; node; node = node->next) { ptr += node->object->snap(ptr, snapper); } + if (migratable) { + memcpy(ptr, globals._owner->list_meta(), globals._owner->list_meta_size()); + ptr += globals._owner->list_meta_size(); + } } snapshot_impl::snapshot_impl(const unsigned char* data, size_t length, bool managed) @@ -108,6 +134,7 @@ snapshot_impl::snapshot_impl(const unsigned char* data, size_t length, bool mana const unsigned char* ptr = data; memcpy(&_header, ptr, sizeof(_header)); inkAssert(_header.length == _length, "Corrupted file length"); + inkAssert(_header.version == decltype(_header){}.version, "Snapshot version missmatch"); } size_t snap_choice::snap(unsigned char* data, const snapper& snapper) const diff --git a/inkcpp/snapshot_impl.h b/inkcpp/snapshot_impl.h index 049fe3eb..2c684df2 100644 --- a/inkcpp/snapshot_impl.h +++ b/inkcpp/snapshot_impl.h @@ -11,6 +11,11 @@ #include "array.h" #include "choice.h" +namespace ink::runtime +{ +class story; +} // namespace ink::runtime + namespace ink::runtime::internal { class snap_choice @@ -67,7 +72,7 @@ class snap_tag : public snapshot_interface static_assert(sizeof(snap_tag) == sizeof(const char*)); -class snapshot_impl : public snapshot +class snapshot_impl final : public snapshot { public: ~snapshot_impl() override @@ -93,8 +98,17 @@ class snapshot_impl : public snapshot const unsigned char* get_runner_snap(size_t idx) const { return _file + get_offset(idx + 1); } + const unsigned char* get_list_metadata() const { return _file + get_offset(num_runners() + 1); } + size_t num_runners() const override { return _header.num_runners; } + bool can_be_migrated() const override { return _header.migratable; } + + bool can_be_migrated(const story&) const; + + hash_t hash() const { return _header.hash; } + + private: // file information // only populated when loading snapshots @@ -102,17 +116,22 @@ class snapshot_impl : public snapshot const unsigned char* _file; size_t _length; bool _managed; - static size_t file_size(size_t, size_t); + static size_t file_size(size_t, size_t, bool); struct header { size_t num_runners; size_t length; - + hash_t hash; + bool migratable; + size_t version = 1; } _header; size_t get_offset(size_t idx) const { - inkAssert(idx <= _header.num_runners, "Out of Bound access for runner in snapshot."); + inkAssert( + idx <= _header.num_runners + (can_be_migrated() ? 1 : 0), + "Out of Bound access for runner in snapshot." + ); return reinterpret_cast(_file + sizeof(header))[idx]; } }; diff --git a/inkcpp/snapshot_interface.h b/inkcpp/snapshot_interface.h index 660fc5a7..ced1f112 100644 --- a/inkcpp/snapshot_interface.h +++ b/inkcpp/snapshot_interface.h @@ -68,11 +68,16 @@ class snapshot_interface struct loader { managed_array& string_table; /// FIXME: make configurable const char* story_string_table; + const bool migratable = false; const snap_tag* runner_tags = nullptr; - loader(managed_array& string_table, const char* story_string_table) + loader( + managed_array& string_table, const char* story_string_table, + bool migratable + ) : string_table{string_table} , story_string_table{story_string_table} + , migratable(migratable) { } @@ -102,6 +107,16 @@ class snapshot_interface return nullptr; }; + /** Check if the snappable component is in a state which could be migrated to a/new different + * story. + * @attention a migration can still fail, even if @c can_be_migrated() was true. + */ + bool can_be_migrated() const + { + inkFail("Snap function not implementd"); + return false; + }; + #ifdef __GNUC__ # pragma GCC diagnostic pop #else diff --git a/inkcpp/stack.cpp b/inkcpp/stack.cpp index dd9c562f..97192b87 100644 --- a/inkcpp/stack.cpp +++ b/inkcpp/stack.cpp @@ -282,12 +282,11 @@ offset_t basic_stack::pop_frame(frame_type* type, bool& eval) // We now have a frame marker. Check if it's a thread // Thread handling if ( - // FIXME: is_tghead_marker, is_jump_marker - frame->data.type() == value_type::thread_start - || frame->data.type() == value_type::thread_end - || frame->data.type() == value_type::jump_marker - ) - { + // FIXME: is_tghead_marker, is_jump_marker + frame->data.type() == value_type::thread_start + || frame->data.type() == value_type::thread_end + || frame->data.type() == value_type::jump_marker + ) { // End of thread marker, we need to create a jump marker if (frame->data.type() == value_type::thread_end) { // Push a new jump marker after the thread end @@ -579,6 +578,33 @@ void basic_eval_stack::forget() base::forget([&none](value& elem) { elem = none; }); } +bool basic_stack::can_be_migrated() const +{ + bool values_migratable = true; + for_each_all([&values_migratable](const entry& e) { + if (! e.data.can_be_migrated()) { + values_migratable = false; + } + }); + return base::can_be_migrated() && _next_thread == 0 && values_migratable; +} + +bool basic_stack::migrate(basic_stack& new_stack) +{ + inkAssert(can_be_migrated() && new_stack.can_be_migrated()); + // move existing values to new_stack, iff there the variable is also in the new stack + for_each_all([&new_stack](const entry& e) { + const value* oth = new_stack.get(e.name); + if (oth) { + new_stack.set(e.name, e.data); + } + }); + // set stack to correct new values + clear(); + new_stack.for_each_all([this](const entry& e) { set(e.name, e.data); }); + return true; +} + void basic_stack::fetch_values(basic_stack& stack) { auto itr = base::begin(); diff --git a/inkcpp/stack.h b/inkcpp/stack.h index eb06cac2..1308b71e 100644 --- a/inkcpp/stack.h +++ b/inkcpp/stack.h @@ -86,6 +86,9 @@ namespace runtime void restore(); void forget(); + // copy new elements from _new, and delete elements now longer existing + bool migrate(basic_stack& _new); + // replace all pointer in current frame with values from _stack void fetch_values(basic_stack& _stack); // push all values to other _stack @@ -94,6 +97,7 @@ namespace runtime // snapshot interface size_t snap(unsigned char* data, const snapper&) const; const unsigned char* snap_load(const unsigned char* data, const loader&); + bool can_be_migrated() const; private: entry& add(hash_t name, const value& val); @@ -208,6 +212,8 @@ namespace runtime { return base::snap_load(data, loader); } + + bool can_be_migrated() const { return base::can_be_migrated(); } }; template diff --git a/inkcpp/story_impl.cpp b/inkcpp/story_impl.cpp index becc06f5..8d284deb 100644 --- a/inkcpp/story_impl.cpp +++ b/inkcpp/story_impl.cpp @@ -189,14 +189,20 @@ hash_t story_impl::container_hash(container_t id) const } } inkAssert(hit, "Unable to find container for id!"); + hash_t hash = container_hash(offset); + inkAssert(hash, "Did not find hash entry for container! (1)"); + return hash; +} + +hash_t story_impl::container_hash(ip_t offset) const +{ hash_t* h_iter = _container_hash_start; - while (iter != _container_hash_end) { + while (h_iter != _container_hash_end) { if (instructions() + *( offset_t* ) (h_iter + 1) == offset) { return *h_iter; } h_iter += 2; } - inkAssert(false, "Did not find hash entry for container!"); return 0; } @@ -224,14 +230,25 @@ globals story_impl::new_globals() globals story_impl::new_globals_from_snapshot(const snapshot& data) { const snapshot_impl& snapshot = reinterpret_cast(data); - globals_impl* globs = new globals_impl(this); + if (! snapshot.can_be_migrated(*this)) { + return globals(); + } + auto* globs = new globals_impl(this); snapshot.strings().clear(); - snapshot_interface::loader loader{ - snapshot.strings(), - _string_table, - }; - const unsigned char* end = globs->snap_load(snapshot.get_globals_snap(), loader); + snapshot_interface::loader loader(snapshot.strings(), _string_table, snapshot.can_be_migrated()); + auto end = globs->snap_load(snapshot.get_globals_snap(), loader); inkAssert(end == snapshot.get_runner_snap(0), "not all data were used for global reconstruction"); + if (hash() != snapshot.hash()) { + globals new_globs = new_globals(); + runner thread = new_runner(new_globs); + if (! globs->migrate_new_globals( + *new_globs.cast().get(), + reinterpret_cast(snapshot.get_list_metadata()) + )) { + delete globs; + return globals(); + } + } return globals(globs, _block); } @@ -247,20 +264,27 @@ runner story_impl::new_runner_from_snapshot(const snapshot& data, globals store, const snapshot_impl& snapshot = reinterpret_cast(data); if (store == nullptr) store = new_globals_from_snapshot(snapshot); - auto* run = new runner_impl(this, store); + auto* run = new runner_impl(this, store); + // snapshot id is inverso of creation time, but creation time is the more intouitve numbering to + // use + idx = (data.num_runners() - idx - 1); snapshot_interface::loader loader{ snapshot.strings(), _string_table, + snapshot.can_be_migrated(), }; - // snapshot id is inverso of creation time, but creation time is the more intouitve numbering to - // use - idx = (data.num_runners() - idx - 1); auto end = run->snap_load(snapshot.get_runner_snap(idx), loader); inkAssert( (idx + 1 < snapshot.num_runners() && end == snapshot.get_runner_snap(idx + 1)) - || end == snapshot.get_data() + snapshot.get_data_len(), + || end == snapshot.get_data() + snapshot.get_data_len() + || end == snapshot.get_list_metadata(), "not all data were used for runner reconstruction" ); + if (hash() != snapshot.hash()) { + if (! run->migrate_to(*reinterpret_cast(snapshot.get_runner_snap(idx)))) { + return runner(); + } + } return runner(run, _block); } @@ -322,9 +346,11 @@ void story_impl::setup_pointers() while (_header.read_list_flag(ptr) != null_flag) ; } + _list_meta_size = static_cast(ptr - _list_meta); } else { - _list_meta = nullptr; - _lists = nullptr; + _list_meta = nullptr; + _list_meta_size = 0; + _lists = nullptr; } inkAssert( _header.ink_bin_version_number == ink::InkBinVersion, diff --git a/inkcpp/story_impl.h b/inkcpp/story_impl.h index 83dd37cb..ae2a07e5 100644 --- a/inkcpp/story_impl.h +++ b/inkcpp/story_impl.h @@ -40,6 +40,8 @@ class story_impl : public story const char* list_meta() const { return _list_meta; } + size_t list_meta_size() const { return _list_meta_size; } + bool iterate_containers( const uint32_t*& iterator, container_t& index, ip_t& offset, bool reverse = false ) const; @@ -48,6 +50,7 @@ class story_impl : public story CommandFlag container_flag(ip_t offset) const; CommandFlag container_flag(container_t id) const; hash_t container_hash(container_t id) const; + hash_t container_hash(ip_t offset) const; ip_t find_offset_for(hash_t path) const; @@ -60,6 +63,8 @@ class story_impl : public story const ink::internal::header& get_header() const { return _header; } + hash_t hash() const override { return hash_data(_file, _length); } + private: void setup_pointers(); @@ -74,6 +79,7 @@ class story_impl : public story const char* _string_table; const char* _list_meta; + size_t _list_meta_size; const list_flag* _lists; // container info diff --git a/inkcpp/string_table.h b/inkcpp/string_table.h index ff6752e6..c7768a4d 100644 --- a/inkcpp/string_table.h +++ b/inkcpp/string_table.h @@ -34,6 +34,8 @@ class string_table final : public snapshot_interface size_t snap(unsigned char* data, const snapper&) const; const unsigned char* snap_load(const unsigned char* data, const loader&); + bool can_be_migrated() const { return true; } + // get position of string when iterate through data // used to enable storing a string table references size_t get_id(const char* string) const; diff --git a/inkcpp/system.cpp b/inkcpp/system.cpp index 6b5990c1..7410cab0 100644 --- a/inkcpp/system.cpp +++ b/inkcpp/system.cpp @@ -10,30 +10,39 @@ namespace ink { -#define A 54059 /* a prime */ -#define B 76963 /* another prime */ -#define C 86969 /* yet another prime */ -#define FIRSTH 37 /* also prime */ +# define A 54059 /* a prime */ +# define B 76963 /* another prime */ +# define C 86969 /* yet another prime */ +# define FIRSTH 37 /* also prime */ - hash_t hash_string(const char* string) - { - hash_t h = FIRSTH; - while (*string) { - h = (h * A) ^ (string[0] * B); - string++; - } - return h; // or return h % C; +hash_t hash_string(const char* string) +{ + hash_t h = FIRSTH; + while (*string) { + h = (h * A) ^ (string[0] * B); + string++; + } + return h; // or return h % C; +} + +hash_t hash_data(const unsigned char* data, size_t len) +{ + hash_t h = FIRSTH; + for (size_t i = 0; i < len; ++i) { + h = (h * A) ^ (data[i] * B); } + return h; // or return h % C; +} - namespace internal - { - void zero_memory(void* buffer, size_t length) - { - char* buf = static_cast(buffer); - for (size_t i = 0; i < length; i++) - *(buf++) = 0; - } - } // namespace internal - } // namespace ink +namespace internal +{ + void zero_memory(void* buffer, size_t length) + { + char* buf = static_cast(buffer); + for (size_t i = 0; i < length; i++) + *(buf++) = 0; + } +} // namespace internal +} // namespace ink #endif diff --git a/inkcpp/value.cpp b/inkcpp/value.cpp index 3393d60f..0f8ca75c 100644 --- a/inkcpp/value.cpp +++ b/inkcpp/value.cpp @@ -237,6 +237,14 @@ ink::runtime::value value::to_interface_value(list_table& table) const return val(); } +bool value::can_be_migrated() const +{ + if (_type == value_type::string && ! string_value.allocated) { + return false; + } + return true; +} + size_t value::snap(unsigned char* data, const snapper& snapper) const { unsigned char* ptr = data; diff --git a/inkcpp/value.h b/inkcpp/value.h index 38995dc2..f3948ea0 100644 --- a/inkcpp/value.h +++ b/inkcpp/value.h @@ -107,6 +107,7 @@ class value : public snapshot_interface { public: // snapshot interface + bool can_be_migrated() const; size_t snap(unsigned char* data, const snapper&) const; const unsigned char* snap_load(const unsigned char* data, const loader&); diff --git a/inkcpp_c/include/inkcpp.h b/inkcpp_c/include/inkcpp.h index 98d40b70..39513821 100644 --- a/inkcpp_c/include/inkcpp.h +++ b/inkcpp_c/include/inkcpp.h @@ -3,6 +3,7 @@ #include #include +#include #include #ifdef __cplusplus @@ -97,6 +98,12 @@ typedef struct HInkSTory HInkStory; void ink_snapshot_get_binary( const HInkSnapshot* self, const unsigned char** data, size_t* data_length ); + /** @memberof HInkSnapshot + * @copydoc ink::runtime::snapshot::can_be_migrated() + * @param self + * @retval True if the snapshot was taken at a simple state which can be migrated. + */ + bool ink_snapshot_can_be_migrated(const HInkSnapshot* self); /** @class HInkChoice * @ingroup clib diff --git a/inkcpp_c/inkcpp.cpp b/inkcpp_c/inkcpp.cpp index a51b8f56..8c425301 100644 --- a/inkcpp_c/inkcpp.cpp +++ b/inkcpp_c/inkcpp.cpp @@ -80,9 +80,9 @@ extern "C" { fseek(file, 0, SEEK_SET); unsigned char* data = static_cast(malloc(file_length)); inkAssert(data, "Malloc of size %u failed", file_length); - size_t length = fread(data, sizeof(unsigned char), file_length, file); + unsigned length = fread(data, sizeof(unsigned char), static_cast(file_length), file); inkAssert( - static_cast(file_length) == length, + file_length == static_cast(length), "Expected to read file of size %u, but only read %u", file_length, length ); fclose(file); @@ -152,6 +152,11 @@ extern "C" { return reinterpret_cast(self)->num_runners(); } + bool ink_snapshot_can_be_migrated(const HInkSnapshot* self) + { + return reinterpret_cast(self)->can_be_migrated(); + } + const char* ink_choice_text(const HInkChoice* self) { return reinterpret_cast(self)->text(); diff --git a/inkcpp_c/tests/Snapshot.c b/inkcpp_c/tests/Snapshot.c index cbf10de5..fcfaa59a 100644 --- a/inkcpp_c/tests/Snapshot.c +++ b/inkcpp_c/tests/Snapshot.c @@ -18,48 +18,102 @@ void check_end(HInkRunner* runner) assert(ink_runner_num_choices(runner) == 2); } +#define CHECK_CHOICE(RUNNER, IDX, STR) \ + assert(strcmp(ink_choice_text(ink_runner_get_choice(RUNNER, IDX)), STR) == 0) +#define CHECK_NEXT_LINE(RUNNER, STR) assert(strcmp(ink_runner_get_line(RUNNER), STR) == 0) + int main() { - HInkStory* story = ink_story_from_file(INK_TEST_RESOURCE_DIR "SimpleStoryFlow.bin"); - HInkRunner* runner = ink_story_new_runner(story, NULL); + { + HInkStory* story = ink_story_from_file(INK_TEST_RESOURCE_DIR "SimpleStoryFlow.bin"); + HInkRunner* runner = ink_story_new_runner(story, NULL); - ink_runner_get_line(runner); - assert(ink_runner_num_choices(runner) == 3); - ink_runner_choose(runner, 2); + ink_runner_get_line(runner); + assert(ink_runner_num_choices(runner) == 3); + ink_runner_choose(runner, 2); - // snapshot after choose -> snapshot will print text after loading - HInkSnapshot* snap1 = ink_runner_create_snapshot(runner); + // snapshot after choose -> snapshot will print text after loading + HInkSnapshot* snap1 = ink_runner_create_snapshot(runner); - int cnt = 0; - while (ink_runner_can_continue(runner)) { - ink_runner_get_line(runner); - ++cnt; - } + int cnt = 0; + while (ink_runner_can_continue(runner)) { + ink_runner_get_line(runner); + ++cnt; + } - // snapshot befroe choose, context (last output lines) can not bet optained at loading - HInkSnapshot* snap2 = ink_runner_create_snapshot(runner); + // snapshot befroe choose, context (last output lines) can not bet optained at loading + HInkSnapshot* snap2 = ink_runner_create_snapshot(runner); - check_end(runner); + check_end(runner); - ink_runner_delete(runner); - runner = ink_story_new_runner_from_snapshot(story, snap1, NULL, 0); + ink_runner_delete(runner); + runner = ink_story_new_runner_from_snapshot(story, snap1, NULL, 0); - // same amount at output then before - while (ink_runner_can_continue(runner)) { - ink_runner_get_line(runner); - --cnt; - } - assert(cnt == 0); + // same amount at output then before + while (ink_runner_can_continue(runner)) { + ink_runner_get_line(runner); + --cnt; + } + assert(cnt == 0); - check_end(runner); + check_end(runner); - ink_runner_delete(runner); - runner = ink_story_new_runner_from_snapshot(story, snap2, NULL, 0); + ink_runner_delete(runner); + runner = ink_story_new_runner_from_snapshot(story, snap2, NULL, 0); + + assert(ink_runner_can_continue(runner) == 0); + check_end(runner); + } + { + HInkStory* before_story = ink_story_from_file(INK_TEST_RESOURCE_DIR "MigrationBefore.bin"); + HInkStory* after_story = ink_story_from_file(INK_TEST_RESOURCE_DIR "MigrationAfter.bin"); + HInkRunner* before_runner = ink_story_new_runner(before_story, NULL); + CHECK_NEXT_LINE(before_runner, "We're going to the seaside!\n"); + assert(ink_runner_num_choices(before_runner) == 3); + CHECK_CHOICE(before_runner, 0, "Make a sand castle"); + CHECK_CHOICE(before_runner, 1, "Go swimming"); + CHECK_CHOICE(before_runner, 2, "Time to go home"); + ink_runner_choose(before_runner, 0); + + CHECK_NEXT_LINE(before_runner, "We made a great sand castle, it even has a moat!\n"); + CHECK_NEXT_LINE(before_runner, "We're going to the seaside!\n"); + CHECK_NEXT_LINE(before_runner, "So far we've done the following: SandCastle\n"); + assert(ink_runner_num_choices(before_runner) == 3); + CHECK_CHOICE(before_runner, 0, "Make a sand castle"); + CHECK_CHOICE(before_runner, 1, "Go swimming"); + CHECK_CHOICE(before_runner, 2, "Time to go home"); + ink_runner_choose(before_runner, 1); + + HInkSnapshot* snap = ink_runner_create_snapshot(before_runner); + assert(ink_snapshot_can_be_migrated(snap)); + + CHECK_NEXT_LINE(before_runner, "We swim and swam, it was delightful!\n"); + CHECK_NEXT_LINE(before_runner, "We're going to the seaside!\n"); + CHECK_NEXT_LINE(before_runner, "So far we've done the following: Swimming, SandCastle\n"); + assert(ink_runner_num_choices(before_runner) == 2); + CHECK_CHOICE(before_runner, 0, "Make a sand castle"); + CHECK_CHOICE(before_runner, 1, "Time to go home"); + + HInkRunner* after_runner = ink_story_new_runner_from_snapshot(after_story, snap, NULL, 0); + + CHECK_NEXT_LINE(after_runner, "We swim and swam, it was delightful!\n"); + CHECK_NEXT_LINE(after_runner, "We're going to the seaside!\n"); + CHECK_NEXT_LINE(after_runner, "So far we've done the following: Swimming, SandCastle\n"); + assert(ink_runner_num_choices(after_runner) == 3); + CHECK_CHOICE(after_runner, 0, "Make a sand castle"); + CHECK_CHOICE(after_runner, 1, "Get Ice Cream"); + CHECK_CHOICE(after_runner, 2, "Time to go home"); + ink_runner_choose(after_runner, 1); + + CHECK_NEXT_LINE(after_runner, "We got ice cream, mine was raspberry!\n"); + CHECK_NEXT_LINE(after_runner, "We're going to the seaside!\n"); + CHECK_NEXT_LINE( + after_runner, "So far we've done the following: Swimming, SandCastle, IceCream\n" + ); + } - assert(ink_runner_can_continue(runner) == 0); - check_end(runner); return 0; } diff --git a/inkcpp_cl/inkcpp_cl.cpp b/inkcpp_cl/inkcpp_cl.cpp index 1d9bbdde..541f385a 100644 --- a/inkcpp_cl/inkcpp_cl.cpp +++ b/inkcpp_cl/inkcpp_cl.cpp @@ -155,33 +155,35 @@ int main(int argc, const char** argv) } // Open file and compile - try { - ink::compiler::compilation_results results; - std::ofstream fout(outputFilename, std::ios::binary | std::ios::out); - ink::compiler::run(inputFilename.c_str(), fout, &results); - fout.close(); - if (json_file_is_tmp_file) { - remove(inputFilename.c_str()); - } + if (inputFilename != outputFilename) { + try { + ink::compiler::compilation_results results; + std::ofstream fout(outputFilename, std::ios::binary | std::ios::out); + ink::compiler::run(inputFilename.c_str(), fout, &results); + fout.close(); + if (json_file_is_tmp_file) { + remove(inputFilename.c_str()); + } - // Report errors - for (auto& warn : results.warnings) { - std::cerr << "WARNING: " << warn << '\n'; - } - for (auto& err : results.errors) { - std::cerr << "ERROR: " << err << '\n'; - } + // Report errors + for (auto& warn : results.warnings) { + std::cerr << "WARNING: " << warn << '\n'; + } + for (auto& err : results.errors) { + std::cerr << "ERROR: " << err << '\n'; + } - if (results.errors.size() > 0 && playMode) { - std::cerr << "Cancelling play mode. Errors detected in compilation" << std::endl; - return -1; - } - } catch (std::exception& e) { - if (json_file_is_tmp_file) { - remove(inputFilename.c_str()); + if (results.errors.size() > 0 && playMode) { + std::cerr << "Cancelling play mode. Errors detected in compilation" << std::endl; + return -1; + } + } catch (std::exception& e) { + if (json_file_is_tmp_file) { + remove(inputFilename.c_str()); + } + std::cerr << "Unhandled InkBin compiler exception: " << e.what() << std::endl; + return 1; } - std::cerr << "Unhandled InkBin compiler exception: " << e.what() << std::endl; - return 1; } if (! playMode) { @@ -246,10 +248,19 @@ int main(int argc, const char** argv) std::cout << "?> "; std::cin >> c; if (c == -1) { + std::cout << "To create a migratable snapshot please enter a choice in addition, or `-1` " + "to snap right now:\nsnap after\n?>"; + std::cin >> c; + if (c != -1) { + thread->choose(c - 1); + } snapshot* snap = thread->create_snapshot(); snap->write_to_file( std::regex_replace(inputFilename, std::regex("\\.[^\\.]+$"), ".snap").c_str() ); + if (snap->can_be_migrated()) { + std::cout << "Migratable snapshot." << std::endl; + } delete snap; break; } diff --git a/inkcpp_compiler/binary_emitter.cpp b/inkcpp_compiler/binary_emitter.cpp index aec80eea..3425201c 100644 --- a/inkcpp_compiler/binary_emitter.cpp +++ b/inkcpp_compiler/binary_emitter.cpp @@ -156,8 +156,12 @@ void binary_emitter::write_raw( { _containers.write(command); _containers.write(flag); + constexpr size_t MAX_PAYLOAD_SIZE = 4; + ink_assert(payload_size <= MAX_PAYLOAD_SIZE, "enforce constant instruction size"); if (payload_size > 0) _containers.write(( const byte_t* ) payload, payload_size); + constexpr const byte_t empty[MAX_PAYLOAD_SIZE] = {}; + _containers.write(empty, MAX_PAYLOAD_SIZE - payload_size); } void binary_emitter::write_path( diff --git a/inkcpp_compiler/reporter.h b/inkcpp_compiler/reporter.h index 359be80d..47c2c2fa 100644 --- a/inkcpp_compiler/reporter.h +++ b/inkcpp_compiler/reporter.h @@ -11,45 +11,48 @@ namespace ink::compiler::internal { - class error_strbuf : public std::stringbuf - { - public: - // start a new error message to be outputted to a given list - void start(error_list* list); - - // If set, the next sync will throw an exception - void throw_on_sync(bool); - protected: - virtual int sync() override; - - private: - error_list* _list = nullptr; - bool _throw = false; - }; - - class reporter - { - protected: - reporter(); - virtual ~reporter() { } - - // sets the results pointer for this reporter - void set_results(compilation_results*); - - // clears the results pointer - void clear_results(); - - // report warning - std::ostream& warn(); - - // report error - std::ostream& err(); - - // report critical error - std::ostream& crit(); - private: - compilation_results* _results; - error_strbuf _buffer; - std::ostream _stream; - }; - } // namespace ink::compiler::internal +class error_strbuf : public std::stringbuf +{ +public: + // start a new error message to be outputted to a given list + void start(error_list* list); + + // If set, the next sync will throw an exception + void throw_on_sync(bool); + +protected: + virtual int sync() override; + +private: + error_list* _list = nullptr; + bool _throw = false; +}; + +class reporter +{ +protected: + reporter(); + + virtual ~reporter() {} + + // sets the results pointer for this reporter + void set_results(compilation_results*); + + // clears the results pointer + void clear_results(); + + // report warning + std::ostream& warn(); + + // report error + std::ostream& err(); + + // report critical error + std::ostream& crit(); + +private: + compilation_results* _results; + error_strbuf _buffer; + std::ostream _stream; +}; +} // namespace ink::compiler::internal diff --git a/inkcpp_python/pybind11 b/inkcpp_python/pybind11 index 0c69e1eb..1b499083 160000 --- a/inkcpp_python/pybind11 +++ b/inkcpp_python/pybind11 @@ -1 +1 @@ -Subproject commit 0c69e1eb2177fa8f8580632c7b1f97fdb606ce8f +Subproject commit 1b4990838904501de7110d27e96c0a4152029156 diff --git a/inkcpp_python/src/module.cpp b/inkcpp_python/src/module.cpp index 8800f7e8..5706d00b 100644 --- a/inkcpp_python/src/module.cpp +++ b/inkcpp_python/src/module.cpp @@ -100,7 +100,7 @@ PYBIND11_MODULE(inkcpp_py, m) return py::make_iterator(self.begin(list_name), self.end()); }, R"(Rerutrns all flags contained in this list from a list of name list_name. - + Use iter(List) to iterate over all flags.)", py::keep_alive<0, 1>(), py::arg("list_name").none(false) ) @@ -183,7 +183,7 @@ Use iter(List) to iterate over all flags.)", return self.get(); }, R"(If value contains a inkcpp_py.Value.Type.String, return it. Else throws an AttributeError. - + If you want convert it to a string use: `str(value)`.)" ); py_value.def( @@ -219,6 +219,10 @@ If you want convert it to a string use: `str(value)`.)" "write_to_file", &snapshot::write_to_file, "Store snapshot in file.", py::arg("filename").none(false) ) + .def( + "can_be_migrated", &snapshot::can_be_migrated, + "If the snapshot can be migrated to a changed story file." + ) .def_static( "from_file", &snapshot::from_file, "Load snapshot from file", py::arg("filename").none(false) @@ -252,7 +256,7 @@ If you want convert it to a string use: `str(value)`.)" "tags", [](const choice& self) { std::vector tags(self.num_tags()); - for (size_t i = 0; i < self.num_tags(); ++i) { + for (ink::size_t i = 0; i < self.num_tags(); ++i) { tags[i] = self.get_tag(i); } return tags; @@ -334,7 +338,7 @@ To reload: "tags", [](const runner& self) { std::vector tags(self.num_tags()); - for (size_t i = 0; i < self.num_tags(); ++i) { + for (ink::size_t i = 0; i < self.num_tags(); ++i) { tags[i] = self.get_tag(i); } return tags; @@ -357,7 +361,7 @@ To reload: "knot_tags", [](const runner& self) { std::vector tags(self.num_knot_tags()); - for (size_t i = 0; i < self.num_knot_tags(); ++i) { + for (ink::size_t i = 0; i < self.num_knot_tags(); ++i) { tags[i] = self.get_knot_tag(i); } return tags; @@ -381,7 +385,7 @@ To reload: "global_tags", [](const runner& self) { std::vector tags(self.num_global_tags()); - for (size_t i = 0; i < self.num_global_tags(); ++i) { + for (ink::size_t i = 0; i < self.num_global_tags(); ++i) { tags[i] = self.get_global_tag(i); } return tags; @@ -392,15 +396,15 @@ To reload: "all_tags", [](const runner& self) { std::vector line_tags(self.num_tags()); - for (size_t i = 0; i < self.num_tags(); ++i) { + for (ink::size_t i = 0; i < self.num_tags(); ++i) { line_tags[i] = self.get_tag(i); } std::vector knot_tags(self.num_knot_tags()); - for (size_t i = 0; i < self.num_knot_tags(); ++i) { + for (ink::size_t i = 0; i < self.num_knot_tags(); ++i) { knot_tags[i] = self.get_knot_tag(i); } std::vector global_tags(self.num_global_tags()); - for (size_t i = 0; i < self.num_global_tags(); ++i) { + for (ink::size_t i = 0; i < self.num_global_tags(); ++i) { global_tags[i] = self.get_global_tag(i); } return py::dict("line"_a = line_tags, "knot"_a = knot_tags, "global"_a = global_tags); @@ -506,7 +510,7 @@ Reconstructs a runner from a snapshot. Args: snapshot: snapshot to load runner from. globals: store used by this runner, else load the store from the file and use it. - (created with inkcpp_py.Story.new_runner_from_snapshot) + (created with inkcpp_py.Story.new_runner_from_snapshot) runner_id: if multiple runners are stored in the snapshot, id of runner to reconstruct. (ids start at 0 and are dense) Returns: diff --git a/inkcpp_python/tests/conftest.py b/inkcpp_python/tests/conftest.py index a9c5fa82..0d089546 100644 --- a/inkcpp_python/tests/conftest.py +++ b/inkcpp_python/tests/conftest.py @@ -37,12 +37,12 @@ def res(ink_source): def story_path(tmpdir_factory): tmpdir = tmpdir_factory.getbasetemp() # tmpdir = os.fsencode('/tmp/pytest') - return {name: files - for (name, files) in map(extract_paths(tmpdir), + return {name: files + for (name, files) in map(extract_paths(tmpdir), filter( - lambda file: os.path.splitext(file)[1] == ".ink", + lambda file: os.path.splitext(file)[1] == ".ink", os.listdir("./inkcpp_test/ink/")))} - + @pytest.fixture(scope='session', autouse=True) def assets(story_path, inklecate_cmd): res = {} @@ -62,7 +62,7 @@ def assets(story_path, inklecate_cmd): @pytest.fixture(scope='session', autouse=True) def generate(): - def g(asset): + def g(asset: dict[str, ink.Story]): store = asset.new_globals() return [ asset, diff --git a/inkcpp_python/tests/test_Snapshot.py b/inkcpp_python/tests/test_Snapshot.py index 4ad0c6cd..704f1f92 100644 --- a/inkcpp_python/tests/test_Snapshot.py +++ b/inkcpp_python/tests/test_Snapshot.py @@ -1,6 +1,7 @@ import inkcpp_py as ink import pytest + def check_end(runner): assert runner.num_choices() == 3 runner.choose(2) @@ -8,9 +9,10 @@ def check_end(runner): runner.getline() assert runner.num_choices() == 2 + class TestSnapshot: def test_snapshot(self, assets, generate): - [story, glob, runner] = generate(assets['SimpleStoryFlow']) + [story, glob, runner] = generate(assets["SimpleStoryFlow"]) runner2 = story.new_runner(glob) runner.getline() assert runner.num_choices() == 3 @@ -55,3 +57,47 @@ def test_snapshot(self, assets, generate): runner = story.new_runner_from_snapshot(snap2, glob, 0) assert not runner.can_continue() check_end(runner) + + def test_migration(self, assets, generate): + [before_story, befero_glob, before_runner] = generate(assets["MigrationBefore"]) + after_story = assets["MigrationAfter"] + assert before_runner.getall() == "We're going to the seaside!\n" + assert before_runner.num_choices() == 3 + assert before_runner.get_choice(0).text() == "Make a sand castle" + assert before_runner.get_choice(1).text() == "Go swimming" + assert before_runner.get_choice(2).text() == "Time to go home" + before_runner.choose(0) + assert ( + before_runner.getall() + == "We made a great sand castle, it even has a moat!\n" + "We're going to the seaside!\nSo far we've done the following: SandCastle\n" + ) + assert before_runner.num_choices() == 3 + assert before_runner.get_choice(0).text() == "Make a sand castle" + assert before_runner.get_choice(1).text() == "Go swimming" + assert before_runner.get_choice(2).text() == "Time to go home" + before_runner.choose(1) + snap = before_runner.create_snapshot() + assert snap.can_be_migrated() + assert ( + before_runner.getall() == "We swim and swam, it was delightful!\n" + "We're going to the seaside!\nSo far we've done the following: Swimming, SandCastle\n" + ) + assert before_runner.num_choices() == 2 + assert before_runner.get_choice(0).text() == "Make a sand castle" + assert before_runner.get_choice(1).text() == "Time to go home" + + after_runner = after_story.new_runner_from_snapshot(snap) + assert ( + after_runner.getall() == "We swim and swam, it was delightful!\n" + "We're going to the seaside!\nSo far we've done the following: Swimming, SandCastle\n" + ) + assert after_runner.num_choices() == 3 + assert after_runner.get_choice(0).text() == "Make a sand castle" + assert after_runner.get_choice(1).text() == "Get Ice Cream" + assert after_runner.get_choice(2).text() == "Time to go home" + after_runner.choose(1) + assert ( + after_runner.getall() == "We got ice cream, mine was raspberry!\n" + "We're going to the seaside!\nSo far we've done the following: Swimming, SandCastle, IceCream\n" + ) diff --git a/inkcpp_test/CMakeLists.txt b/inkcpp_test/CMakeLists.txt index 172cb84a..d8c01499 100644 --- a/inkcpp_test/CMakeLists.txt +++ b/inkcpp_test/CMakeLists.txt @@ -1,6 +1,7 @@ if(INKCPP_NO_STL) message(FATAL_ERROR "Can not build tests without STL support, please disable INKCPP_TEST") endif() + add_executable( inkcpp_test catch.hpp @@ -23,11 +24,13 @@ add_executable( ThirdTierChoiceAfterBrackets.cpp NoEarlyTags.cpp ExternalFunctionsExecuteProperly.cpp - ExternalFunctionTypes.cpp + ExternalFunctionTypes.cpp LookaheadSafe.cpp EmptyStringForDivert.cpp MoveTo.cpp - Fixes.cpp) + ListMatching.cpp + Fixes.cpp + Migration.cpp) target_link_libraries(inkcpp_test PUBLIC inkcpp inkcpp_compiler inkcpp_shared) target_include_directories(inkcpp_test PRIVATE ../shared/private/) @@ -61,8 +64,8 @@ if((inkcpp_inklecate_upper STREQUAL "ALL") OR (inkcpp_inklecate_upper STREQUAL " message( FATAL_ERROR "inklecate download is not provided, please check if the download failed. - You may set INKCPP_INKLECATE=NONE and provide inkcleate via your PATH or \ - the INKLECATE enviroment variable. + You may set INKCPP_INKLECATE=NONE and provide inkcleate via your PATH or \ + the INKLECATE enviroment variable. You can also disable tests altogether by setting INKCPP_TEST=OFF") endif(inklecate_linux_POPULATED) elseif(APPLE) @@ -72,8 +75,8 @@ if((inkcpp_inklecate_upper STREQUAL "ALL") OR (inkcpp_inklecate_upper STREQUAL " message( FATAL_ERROR "inklecate download is not provided, please check if the download failed. - You may set INKCPP_INKLECATE=NONE and provide inkcleate via your PATH or \ - the INKLECATE enviroment variable. + You may set INKCPP_INKLECATE=NONE and provide inkcleate via your PATH or \ + the INKLECATE enviroment variable. You can also disable tests altogether by setting INKCPP_TEST=OFF") endif(inklecate_mac_POPULATED) elseif( @@ -94,8 +97,8 @@ if((inkcpp_inklecate_upper STREQUAL "ALL") OR (inkcpp_inklecate_upper STREQUAL " else() message( FATAL_ERROR - "Current os could not be identified, \ - therfore inklecate must be provided explicit for the tests to work + "Current os could not be identified, therfore inklecate must be provided \ + explicit for the tests to work please set INKCPP_INKLECATE=NONE and provide inklecate via your PATH or \ the INKLECATE enviroment variables. Alternatily disable tests by setting INKCPP_TEST=OFF.") diff --git a/inkcpp_test/FallbackFunction.cpp b/inkcpp_test/FallbackFunction.cpp index b090e1b3..e4e1d2a6 100644 --- a/inkcpp_test/FallbackFunction.cpp +++ b/inkcpp_test/FallbackFunction.cpp @@ -20,9 +20,9 @@ SCENARIO("run a story with external function and fallback function", "[external WHEN("bind both external functions") { int cnt_sqrt = 0; - auto fn_sqrt = [&cnt_sqrt](int x) -> int { + auto fn_sqrt = [&cnt_sqrt](double x) -> double { ++cnt_sqrt; - return static_cast(sqrt(x)); + return static_cast(sqrt(x)); }; int cnt_greeting = 0; auto fn_greeting = [&cnt_greeting]() -> const char* { @@ -49,9 +49,9 @@ SCENARIO("run a story with external function and fallback function", "[external WHEN("only bind function without fallback") { int cnt_sqrt = 0; - auto fn_sqrt = [&cnt_sqrt](int x) -> int { + auto fn_sqrt = [&cnt_sqrt](double x) -> double { ++cnt_sqrt; - return static_cast(sqrt(x)); + return static_cast(sqrt(x)); }; thread->bind("sqrt", fn_sqrt); diff --git a/inkcpp_test/Fixes.cpp b/inkcpp_test/Fixes.cpp index 3c9cfd57..9790a1d3 100644 --- a/inkcpp_test/Fixes.cpp +++ b/inkcpp_test/Fixes.cpp @@ -267,7 +267,7 @@ SCENARIO("Provoke thread array expension _ #142", "[fixes]") REQUIRE(thread->num_choices() == 15); const char options[] = "abcdefghijklmno"; for (const char* c = options; *c; ++c) { - CHECK(thread->get_choice(static_cast(c - options))->text()[0] == *c); + CHECK(thread->get_choice(static_cast(c - options))->text()[0] == *c); } } } @@ -289,7 +289,7 @@ SCENARIO("Provoke thread array expension _ #142", "[fixes]") REQUIRE(thread->num_choices() == 10); const char* options = "bdfhjklmno"; for (const char* c = options; *c; ++c) { - CHECK(thread->get_choice(static_cast(c - options))->text()[0] == *c); + CHECK(thread->get_choice(static_cast(c - options))->text()[0] == *c); } } } diff --git a/inkcpp_test/ListMatching.cpp b/inkcpp_test/ListMatching.cpp new file mode 100644 index 00000000..830b43de --- /dev/null +++ b/inkcpp_test/ListMatching.cpp @@ -0,0 +1,206 @@ +#include "catch.hpp" + +#include "../inkcpp/hungarian_solver.h" +#include +#include +#include +#include + +using namespace ink::runtime; + +namespace ink::runtime::internal +{ +struct MatchListValues { + const char* const* names; + const int* values; + size_t length; +}; + +float** cost_matrix(const MatchListValues& rh, const MatchListValues& lh, float drop_panelty); +float d_contains(const size_t lh[2], const size_t rh[2], const int* matches); +float d_value(int lh, int rh, int lh_range[2], int rh_range[2]); +float d_label(const char* lh, const char* rh); +} // namespace ink::runtime::internal + +SCENARIO("santy check distance functions", "[list_match]") +{ + SECTION("Labels") + { + SECTION("jaro_simularity") + { + GIVEN("Two Stings") + { + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "FARMVILLE"); + CHECK_THAT(j, Catch::Matchers::WithinAbs(0.88, 0.01)); + } + GIVEN("Two Strings in different Casing, no impact ignore casing") + { + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "farmville"); + CHECK_THAT(j, Catch::Matchers::WithinAbs(0.88, 0.01)); + } + GIVEN("Two strings with fill characters, small impact") + { + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "FARMV_IL-LE"); + CHECK_THAT(j, Catch::Matchers::WithinAbs(0.83, 0.01)); + } + } + SECTION("jaro_winkler_simularity") + { + GIVEN("Two Strings wih without prefix") + { + float j = ink::algorithms::jaro_simularity("ZFAREMVIEL", "YFARMVILLE"); + float jw = ink::algorithms::jaro_winkler_simularity("ZFAREMVIEL", "YFARMVILLE"); + CHECK_THAT(jw, Catch::Matchers::WithinAbs(j, 0.01)); + } + GIVEN("Two Strings with prefix") + { + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "FARMVILLE"); + float jw = ink::algorithms::jaro_winkler_simularity("FAREMVIEL", "FARMVILLE"); + CHECK(j < jw); + } + } + } + SECTION("Values") + { + int range1[] = {0, 20}; + int range2[] = {5, 35}; + GIVEN("Same Value") + { + float d = ink::runtime::internal::d_value(5, 5, range1, range1); + CHECK(d == 0); + d = ink::runtime::internal::d_value(5, 5, range1, range2); + CHECK(d == 0); + } + GIVEN("Different Value") + { + float d1 = ink::runtime::internal::d_value(10, 20, range1, range1); + float d2 = ink::runtime::internal::d_value(10, 20, range2, range2); + float d3 = ink::runtime::internal::d_value(10, 20, range1, range2); + CHECK(d3 == 0); // there are both in the center of their respected range + CHECK(d1 > d2); // same absolute distance in bigger range is a smaller distance + } + } + SECTION("Sets") + { + GIVEN("Equal Sets") + { + ink::size_t lh[] = {5, 10}; + ink::size_t rh[] = {5, 10}; + int matches[] = {0, 0, 0, 0, 0, 5, 6, 7, 8, 9}; + float d = ink::runtime::internal::d_contains(lh, rh, matches); + CHECK_THAT(d, Catch::Matchers::WithinAbs(1, 0.001)); + } + GIVEN("Dropped Values") + { + ink::size_t lh[] = {5, 10}; + ink::size_t rh[] = {5, 8}; + int matches[] = {0, 0, 0, 0, 0, 5, -1, -1, 6, 7}; + float d = ink::runtime::internal::d_contains(lh, rh, matches); + CHECK_THAT(d, Catch::Matchers::WithinAbs(0.6, 0.001)); + } + GIVEN("New Values") + { + ink::size_t lh[] = {5, 8}; + ink::size_t rh[] = {5, 10}; + int matches[] = {0, 0, 0, 0, 0, 5, 6, 7}; + float d = ink::runtime::internal::d_contains(lh, rh, matches); + CHECK_THAT(d, Catch::Matchers::WithinAbs(0.6, 0.001)); + } + GIVEN("Swapped Values") + { + ink::size_t lh[] = {5, 10}; + ink::size_t rh[] = {5, 10}; + int matches[] = {0, 0, 0, 0, 0, 5, 9, 6, 8, 7}; + float d = ink::runtime::internal::d_contains(lh, rh, matches); + CHECK_THAT(d, Catch::Matchers::WithinAbs(1, 0.001)); + } + GIVEN("Changed Values") + { + ink::size_t lh[] = {5, 10}; + ink::size_t rh[] = {5, 10}; + int matches[] = {0, 0, 0, 0, 0, 5, 9, -1, -1, -1}; + float d = ink::runtime::internal::d_contains(lh, rh, matches); + CHECK_THAT(d, Catch::Matchers::WithinAbs(0.25, 0.001)); + } + } +} + +SCENARIO("find best assigments", "[list_match][hungarian]") +{ + GIVEN("Example 1") + { + // clang-format off + float cost[] = { + 8/**/, 5 , 9 , + 4 , 2 , 4/**/, + 7 , 3/**/, 8 , + }; + // clang-format on + int matches[3]; + float total_cost = ink::algorithms::hungarian_solver(cost, matches, 3); + CHECK(total_cost == 15.f); + CHECK(matches[0] == 0); + CHECK(matches[1] == 2); + CHECK(matches[2] == 1); + } + GIVEN("Example 2") + { + // clang-format off + float cost[] = { + 108 , 150 , 122/**/, + 125 , 135/**/, 148 , + 150/**/, 175 , 250 , + }; + // clang-format off + int matches[3]; + float total_cost = ink::algorithms::hungarian_solver(cost, matches, 3); + CHECK(total_cost == 407); + CHECK(matches[0] == 2); + CHECK(matches[1] == 1); + CHECK(matches[2] == 0); + } + GIVEN("With Example 1 Threshold") { + // clang-format off + float cost[] = { + 8/**/, 5 , 9 , + 4 , 2 , 4/**/, + 7 , 3/**/, 8 , + }; + // clang-format on + int matches[3]; + float total_cost = ink::algorithms::hungarian_solver(cost, matches, 3, 5); + CHECK(total_cost == 15.f); + CHECK(matches[0] == -1); + CHECK(matches[1] == 2); + CHECK(matches[2] == 1); + } +} + +SCENARIO("Simple List Migration stories", "[list_match]") +{ + GIVEN("Splitted List") + { + std::unique_ptr ink_a{story::from_file(INK_TEST_RESOURCE_DIR "ListMatchStoryA.bin")}; + std::unique_ptr ink_b{story::from_file(INK_TEST_RESOURCE_DIR "ListMatchStoryB.bin")}; + globals globals_a = ink_a->new_globals(); + runner thread_a = ink_a->new_runner(globals_a); + WHEN("Load new list extensions, split and typo fix") + { + CHECK(thread_a->getline() == "You are currently at Flor, Balcony\n"); + REQUIRE(thread_a->has_choices()); + thread_a->choose(0); + std::unique_ptr snap{thread_a->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + CHECK( + thread_a->getall() + == "More\nYou are still at Flor, Balcony - all posibilities are Flor, Balcony, Kitchen, Garden\n" + ); + auto globals_b = ink_b->new_globals_from_snapshot(*snap); + auto thread_b = ink_b->new_runner_from_snapshot(*snap, globals_b); + CHECK( + thread_b->getall() + == "More\nYou are still at Floor, Balcony - all posibilities are Kitchen, Street, Floor, Balcony, Livingroom, Garden\n" + ); + } + } +} diff --git a/inkcpp_test/Migration.cpp b/inkcpp_test/Migration.cpp new file mode 100644 index 00000000..ab0f42ac --- /dev/null +++ b/inkcpp_test/Migration.cpp @@ -0,0 +1,226 @@ +#include "catch.hpp" +#include "../snapshot_impl.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace ink::runtime; + +SCENARIO("Simple isolated migration tests.") +{ + std::unique_ptr base_story{story::from_file(INK_TEST_RESOURCE_DIR "MigrationBase.bin")}; + globals base_globals = base_story->new_globals(); + runner base_thread = base_story->new_runner(base_globals); + WHEN("Just Run the base story") + { + std::string content = base_thread->getall(); + REQUIRE(base_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + THEN("All values as defined") + { + CHECK(base_globals->get("do_not_migrate").value_or(0) == 10); + CHECK(base_globals->get("do_migrate").value_or(0) == 15); + } + REQUIRE(base_thread->num_global_tags() == 2); + REQUIRE(base_thread->get_global_tag(0) == std::string("test:migration")); + REQUIRE(base_thread->get_global_tag(1) == std::string("flavor:base")); + REQUIRE(base_thread->num_knot_tags() == 1); + REQUIRE(base_thread->get_knot_tag(0) == std::string("knot:Main")); + } + GIVEN("Simple story with changes in globals.") + { + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR + "MigrationChangeGlobals.bin")}; + WHEN("Just Run the new story") + { + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + REQUIRE(new_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + THEN("All values as defined") + { + CHECK(new_globals->get("do_migrate").value_or(0) == 10); + CHECK(new_globals->get("new_var").value_or(0) == 20); + } + } + WHEN("Run base story and load in new_story") + { + std::string content = base_thread->getall(); + REQUIRE(base_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + base_thread->choose(0); + std::unique_ptr snap{base_thread->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + globals new_globals = new_story->new_globals_from_snapshot(*snap); + runner new_thread = new_story->new_runner_from_snapshot(*snap, new_globals); + THEN("expect merged globals") + { + CHECK_FALSE(new_globals->get("do_not_migrate").has_value()); + CHECK(new_globals->get("do_migrate").value_or(0) == 15); + CHECK(new_globals->get("new_var").value_or(0) == 20); + } + THEN("expect story to continue normally") + { + content = new_thread->getall(); + REQUIRE(content == "A\ncatch\n1 -1 0\nOh.\n"); + } + } + } + GIVEN("Simple story with changed knots.") + { + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR + "MigrationChangeNodes.bin")}; + WHEN("Just Run the new story") + { + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + REQUIRE(new_thread->has_choices()); + REQUIRE(content == "B\n-1 0 0\nThis is a simple story.\n"); + new_thread->choose(0); + content = new_thread->getall(); + REQUIRE(content == "A\ncatch\n-1 1 0\nOh.\n"); + } + WHEN("Run base story and load new story.") + { + std::string content = base_thread->getall(); + REQUIRE(base_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + base_thread->choose(0); + std::unique_ptr snap{base_thread->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + globals new_globals = new_story->new_globals_from_snapshot(*snap); + runner new_thread = new_story->new_runner_from_snapshot(*snap, new_globals); + THEN("Migrated visit counts. Unreachable node has visit, new node has no") + { + content = new_thread->getall(); + REQUIRE(content == "A\ncatch\n1 1 0\nOh.\n"); + } + } + } + GIVEN("Simple story with changed temporary variables.") + { + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR "MigrationTemp.bin")}; + WHEN("Just Run the new story.") + { + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + REQUIRE(new_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + REQUIRE(new_thread->get_current_knot() == 0x25e83b84); + new_thread->choose(0); + REQUIRE(new_thread->get_current_knot() == 0x25e83b84); + content = new_thread->getall(); + REQUIRE(new_thread->get_current_knot() == 0x25e83b84); + REQUIRE(content == "A\ncatch\n2 - 3\n1 -1 0\nOh.\n"); + } + WHEN("Run base story and load new story.") + { + std::string content = base_thread->getall(); + REQUIRE(base_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + REQUIRE(base_thread->get_current_knot() == 0x25e83b84); + base_thread->choose(0); + REQUIRE(base_thread->get_current_knot() == 0x25e83b84); + std::unique_ptr snap{base_thread->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + globals new_globals = new_story->new_globals_from_snapshot(*snap); + runner new_thread = new_story->new_runner_from_snapshot(*snap, new_globals); + THEN("Transfared old temporary variable, and kept default from new one.") + { + REQUIRE(new_thread->get_current_knot() == 0x25e83b84); + content = new_thread->getall(); + REQUIRE(content == "A\ncatch\n5 - 6\n1 -1 0\nOh.\n"); + } + } + } + GIVEN("Simple story with other knot/global tags.") + { + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR "MigrationKnotTags.bin") + }; + WHEN("Just Run the new story.") + { + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + REQUIRE(new_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + REQUIRE(new_thread->num_global_tags() == 2); + REQUIRE(new_thread->get_global_tag(0) == std::string("test:migration")); + REQUIRE(new_thread->get_global_tag(1) == std::string("flavor:changed")); + REQUIRE(new_thread->num_knot_tags() == 1); + REQUIRE(new_thread->get_knot_tag(0) == std::string("knot:different")); + } + WHEN("Run base story and load new story.") + { + std::string content = base_thread->getall(); + REQUIRE(base_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + REQUIRE(base_thread->num_global_tags() == 2); + REQUIRE(base_thread->get_global_tag(0) == std::string("test:migration")); + REQUIRE(base_thread->get_global_tag(1) == std::string("flavor:base")); + REQUIRE(base_thread->num_knot_tags() == 1); + REQUIRE(base_thread->get_knot_tag(0) == std::string("knot:Main")); + base_thread->choose(0); + std::unique_ptr snap{base_thread->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + globals new_globals = new_story->new_globals_from_snapshot(*snap); + runner new_thread = new_story->new_runner_from_snapshot(*snap, new_globals); + THEN("Got new global/knot tags") + { + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + REQUIRE(new_thread->num_global_tags() == 2); + REQUIRE(new_thread->get_global_tag(0) == std::string("test:migration")); + REQUIRE(new_thread->get_global_tag(1) == std::string("flavor:changed")); + REQUIRE(new_thread->num_knot_tags() == 1); + REQUIRE(new_thread->get_knot_tag(0) == std::string("knot:different")); + } + } + } +} + +SCENARIO("Migration Test for small story") +{ + std::unique_ptr before{story::from_file(INK_TEST_RESOURCE_DIR "MigrationBefore.bin")}; + std::unique_ptr after{story::from_file(INK_TEST_RESOURCE_DIR "MigrationAfter.bin")}; + GIVEN("Test sequcen with multiple loads") + { + runner thread_before = before->new_runner(); + REQUIRE(thread_before->getall() == "We're going to the seaside!\n"); + CHECK(thread_before->num_choices() == 3); + CHECK(thread_before->get_choice(0)->text() == std::string("Make a sand castle")); + CHECK(thread_before->get_choice(1)->text() == std::string("Go swimming")); + CHECK(thread_before->get_choice(2)->text() == std::string("Time to go home")); + thread_before->choose(0); + REQUIRE(thread_before->getall() == "We made a great sand castle, it even has a moat!\nWe're going to the seaside!\nSo far we've done the following: SandCastle\n"); + CHECK(thread_before->num_choices() == 3); + CHECK(thread_before->get_choice(0)->text() == std::string("Make a sand castle")); + CHECK(thread_before->get_choice(1)->text() == std::string("Go swimming")); + CHECK(thread_before->get_choice(2)->text() == std::string("Time to go home")); + + thread_before->choose(1); + std::unique_ptr snap1{thread_before->create_snapshot()}; + REQUIRE(snap1->can_be_migrated()); + REQUIRE(thread_before->getall() == "We swim and swam, it was delightful!\nWe're going to the seaside!\nSo far we've done the following: Swimming, SandCastle\n"); + + CHECK(thread_before->num_choices() == 2); + CHECK(thread_before->get_choice(0)->text() == std::string("Make a sand castle")); + CHECK(thread_before->get_choice(1)->text() == std::string("Time to go home")); + + runner thread_after = after->new_runner_from_snapshot(*snap1); + REQUIRE(thread_after->getall() == "We swim and swam, it was delightful!\nWe're going to the seaside!\nSo far we've done the following: Swimming, SandCastle\n"); + CHECK(thread_after->num_choices() == 3); + CHECK(thread_after->get_choice(0)->text() == std::string("Make a sand castle")); + CHECK(thread_after->get_choice(1)->text() == std::string("Get Ice Cream")); + CHECK(thread_after->get_choice(2)->text() == std::string("Time to go home")); + thread_after->choose(1); + REQUIRE(thread_after->getall() == "We got ice cream, mine was raspberry!\nWe're going to the seaside!\nSo far we've done the following: Swimming, SandCastle, IceCream\n"); + } +} diff --git a/inkcpp_test/ink/ListMatchStoryA.ink b/inkcpp_test/ink/ListMatchStoryA.ink new file mode 100644 index 00000000..77439af4 --- /dev/null +++ b/inkcpp_test/ink/ListMatchStoryA.ink @@ -0,0 +1,7 @@ +LIST Stages = Flor, Balcony, Kitchen, Garden +VAR current_stage = () +~ current_stage = (Flor, Balcony) + +You are currently at {current_stage} +* More +- You are still at {current_stage} - all posibilities are {LIST_ALL(current_stage)} diff --git a/inkcpp_test/ink/ListMatchStoryB.ink b/inkcpp_test/ink/ListMatchStoryB.ink new file mode 100644 index 00000000..f76ac70b --- /dev/null +++ b/inkcpp_test/ink/ListMatchStoryB.ink @@ -0,0 +1,9 @@ +LIST Indoor = Kitchen, Floor, Livingroom +LIST Outdoor = Street, Balcony, Garden +VAR current_stage = () +~ current_stage = Floor + +You are currently at {current_stage} +* More +- You are still at {current_stage} - all posibilities are {LIST_ALL(current_stage)} +-> END diff --git a/inkcpp_test/ink/MigrationAfter.ink b/inkcpp_test/ink/MigrationAfter.ink new file mode 100644 index 00000000..6f690f90 --- /dev/null +++ b/inkcpp_test/ink/MigrationAfter.ink @@ -0,0 +1,31 @@ +# source https://github.com/harryr0se from issue: #112 +LIST activities = Swimming, SandCastle, IceCream +VAR completed = () + +-> holiday +=== holiday +We're going to the seaside! +{completed: So far we've done the following: {completed}} ++ [Make a sand castle] -> sand_castle -> holiday +* [Go swimming] -> swimming -> holiday ++ [Get Ice Cream] -> ice_cream -> holiday +* Time to go home -> home + += sand_castle +We made a great sand castle, it even has a moat! +~ completed += SandCastle +->-> + += swimming +We swim and swam, it was delightful! +~ completed += Swimming +->-> + += ice_cream +We got ice cream, mine was raspberry! +~ completed += IceCream +->-> + += home +What a nice holiday that was +-> END diff --git a/inkcpp_test/ink/MigrationBase.ink b/inkcpp_test/ink/MigrationBase.ink new file mode 100644 index 00000000..21f6ab51 --- /dev/null +++ b/inkcpp_test/ink/MigrationBase.ink @@ -0,0 +1,26 @@ +# test:migration +# flavor:base + +VAR do_not_migrate = 10 +VAR do_migrate = 15 +->Node1 +=== OldNode +O +-> Node1 +=== Node1 +A +-> Main +=== Main +# knot:Main +~ temp tKeep = 2 +~ temp tOld = 3 +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +This is a simple story. +~ tKeep = 5 +* A +* B +- catch +{tKeep} {tOld} +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +Oh. +->DONE diff --git a/inkcpp_test/ink/MigrationBefore.ink b/inkcpp_test/ink/MigrationBefore.ink new file mode 100644 index 00000000..1b72658c --- /dev/null +++ b/inkcpp_test/ink/MigrationBefore.ink @@ -0,0 +1,25 @@ +# source https://github.com/harryr0se from issue: #112 +LIST activities = Swimming, SandCastle +VAR completed = () + +-> holiday +=== holiday +We're going to the seaside! +{completed: So far we've done the following: {completed}} ++ [Make a sand castle] -> sand_castle -> holiday +* [Go swimming] -> swimming -> holiday +* Time to go home -> home + += sand_castle +We made a great sand castle, it even has a moat! +~ completed += SandCastle +->-> + += swimming +We swim and swam, it was delightful! +~ completed += Swimming +->-> + += home +What a nice holiday that was +-> END diff --git a/inkcpp_test/ink/MigrationChangeGlobals.ink b/inkcpp_test/ink/MigrationChangeGlobals.ink new file mode 100644 index 00000000..b0174ea7 --- /dev/null +++ b/inkcpp_test/ink/MigrationChangeGlobals.ink @@ -0,0 +1,18 @@ +VAR do_migrate = 10 +VAR new_var = 20 +->Node1 +=== OldNode +O +-> Node1 +=== Node1 +A +-> Main +=== Main +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +This is a simple story. +* A +* B +- catch +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +Oh. +->DONE diff --git a/inkcpp_test/ink/MigrationChangeNodes.ink b/inkcpp_test/ink/MigrationChangeNodes.ink new file mode 100644 index 00000000..62c7ca4e --- /dev/null +++ b/inkcpp_test/ink/MigrationChangeNodes.ink @@ -0,0 +1,18 @@ +VAR do_not_migrate = 10 +VAR do_migrate = 15 +->NewNode +=== Node1 +A +-> Main +=== NewNode +B +-> Main +=== Main +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> NewNode)} {TURNS_SINCE(-> Main)} +This is a simple story. +* A +* B +- catch +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> NewNode)} {TURNS_SINCE(-> Main)} +Oh. +->DONE diff --git a/inkcpp_test/ink/MigrationKnotTags.ink b/inkcpp_test/ink/MigrationKnotTags.ink new file mode 100644 index 00000000..9c4037b0 --- /dev/null +++ b/inkcpp_test/ink/MigrationKnotTags.ink @@ -0,0 +1,26 @@ +# test:migration +# flavor:changed + +VAR do_not_migrate = 10 +VAR do_migrate = 15 +->Node1 +=== OldNode +O +-> Node1 +=== Node1 +A +-> Main +=== Main +# knot:different +~ temp tKeep = 2 +~ temp tOld = 3 +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +This is a simple story. +~ tKeep = 5 +* A +* B +- catch +{tKeep} {tOld} +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +Oh. +->DONE diff --git a/inkcpp_test/ink/MigrationTemp.ink b/inkcpp_test/ink/MigrationTemp.ink new file mode 100644 index 00000000..422829f9 --- /dev/null +++ b/inkcpp_test/ink/MigrationTemp.ink @@ -0,0 +1,26 @@ +# test:migration +# flavor:base + +VAR do_not_migrate = 10 +VAR do_migrate = 15 +->Node1 +=== OldNode +O +-> Node1 +=== Node1 +A +-> Main +=== Main +# knot:Main +~ temp tKeep = 2 +~ temp tNew = 6 +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +This is a simple story. +~ tNew = 3 +* A +* B +- catch +{tKeep} - {tNew} +{TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +Oh. +->DONE diff --git a/shared/public/system.h b/shared/public/system.h index f37dfce2..ecc25092 100644 --- a/shared/public/system.h +++ b/shared/public/system.h @@ -74,15 +74,6 @@ typedef uint32_t hash_t; /** Invalid hash value */ const hash_t InvalidHash = 0; -#ifdef INK_ENABLE_UNREAL -/** Simple hash for serialization of strings */ -inline hash_t hash_string(const char* string) -{ - return CityHash32(string, FCStringAnsi::Strlen(string)); -} -#else -hash_t hash_string(const char* string); -#endif /** Byte type */ typedef unsigned char byte_t; @@ -129,6 +120,17 @@ constexpr list_flag null_flag{-1, -1}; /** value representing an empty list */ constexpr list_flag empty_flag{-1, 0}; +#ifdef INK_ENABLE_UNREAL +/** Simple hash for serialization of strings */ +inline hash_t hash_string(const char* string) +{ + return CityHash32(string, FCStringAnsi::Strlen(string)); +} +#else +hash_t hash_string(const char* string); +hash_t hash_data(const unsigned char* data, size_t len); +#endif + namespace internal { #ifdef __GNUC__ @@ -236,24 +238,22 @@ void ink_assert(bool condition, const char* msg = nullptr, Args... args) msg = EMPTY; } if (! condition) { -#if defined(INKCPP_ENABLE_STL) || defined(INKCPP_ENABLE_CSTD) +#if defined(INK_ENABLE_STL) || defined(INK_ENABLE_CSTD) if constexpr (sizeof...(args) > 0) { size_t size = snprintf(nullptr, 0, msg, args...) + 1; char* message = static_cast(malloc(size)); snprintf(message, size, msg, args...); msg = message; - } else + } #endif - { #ifdef INK_ENABLE_EXCEPTIONS - throw ink_exception(msg); + throw ink_exception(msg); #elif defined(INK_ENABLE_CSTD) - fprintf(stderr, "Ink Assert: %s\n", msg); - abort(); + fprintf(stderr, "Ink Assert: %s\n", msg); + abort(); #else # warning no assertion handling this could lead to invalid code paths #endif - } } } #ifdef __GNUC__