diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ff9d06c..7d8eb526 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -200,6 +200,8 @@ jobs: name: Build Doxygen documentation needs: [compilation, build-python] runs-on: ubuntu-latest + env: + DOXYGEN_VERSION: "1.17.0" steps: - uses: actions/checkout@v4 with: @@ -215,13 +217,19 @@ jobs: pybind11-stubgen pdoc --user + - uses: actions/cache@v5 + id: cache-doxygen + with: + path: doxygen-${{ env.DOXYGEN_VERSION }}.linux.bin.tar.gz + key: doxygen-${{ env.DOXYGEN_VERSION }} + - name: download Doxygen + if: steps.cache-doxygen.outputs.cache-hit != 'true' + run: wget https://www.doxygen.nl/files/doxygen-$DOXYGEN_VERSION.linux.bin.tar.gz - name: Install Doxygen run: | sudo apt-get install graphviz -y - wget https://www.doxygen.nl/files/doxygen-1.10.0.linux.bin.tar.gz - gunzip doxygen-*.tar.gz - tar xf doxygen-*.tar - cd doxygen-1.10.0/ + tar xzf doxygen-$DOXYGEN_VERSION.linux.bin.tar.gz + cd doxygen-$DOXYGEN_VERSION/ sudo make install - name: Setup cmake uses: jwlawson/actions-setup-cmake@v2 diff --git a/.gitignore b/.gitignore index b433ae47..4a347881 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,11 @@ build/* bin/ Bin/ +# python build artifacts +dist/ +__pycache__/ +inkcpp_py.egg-info/ + # Visual Studio /out/build/ .vs diff --git a/Documentation/unreal/InkCPP_DEMO.zip b/Documentation/unreal/InkCPP_DEMO.zip index 6ae55b55..6174d1c7 100644 Binary files a/Documentation/unreal/InkCPP_DEMO.zip and b/Documentation/unreal/InkCPP_DEMO.zip differ diff --git a/Documentation/unreal/imgs/SaveGame.png b/Documentation/unreal/imgs/SaveGame.png new file mode 100644 index 00000000..493cb8c3 Binary files /dev/null and b/Documentation/unreal/imgs/SaveGame.png differ diff --git a/Doxyfile b/Doxyfile index ded1987f..966bc55f 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1,7 +1,7 @@ -# Doxyfile 1.9.8 +# Doxyfile 1.17.0 # This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project. +# Doxygen (www.doxygen.org) for a project. # # All text after a double hash (##) is considered a comment and is placed in # front of the TAG it is preceding. @@ -15,10 +15,10 @@ # # Note: # -# Use doxygen to compare the used configuration file with the template +# Use Doxygen to compare the used configuration file with the template # configuration file: # doxygen -x [configFile] -# Use doxygen to compare the used configuration file with the template +# Use Doxygen to compare the used configuration file with the template # configuration file without replacing the environment variables or CMake type # replacement variables: # doxygen -x_noenv [configFile] @@ -51,7 +51,7 @@ PROJECT_NAME = inkcpp PROJECT_NUMBER = # Using the PROJECT_BRIEF tag one can provide an optional one line description -# for a project that appears at the top of each page and should give viewer a +# for a project that appears at the top of each page and should give viewers a # quick idea about the purpose of the project. Keep the description short. PROJECT_BRIEF = @@ -63,19 +63,25 @@ PROJECT_BRIEF = PROJECT_LOGO = +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is -# entered, it will be relative to the location where doxygen was started. If +# entered, it will be relative to the location where Doxygen was started. If # left blank the current directory will be used. OUTPUT_DIRECTORY = Documentation -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 +# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096 # sub-directories (in 2 levels) under the output directory of each output format # and will distribute the generated files over these directories. Enabling this -# option can be useful when feeding doxygen a huge amount of source files, where -# putting all generated files in the same directory would otherwise causes -# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# option can be useful when feeding Doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise cause +# performance problems for the file system. Adjust CREATE_SUBDIRS_LEVEL to # control the number of sub-directories. # The default value is: NO. @@ -92,7 +98,7 @@ CREATE_SUBDIRS = NO CREATE_SUBDIRS_LEVEL = 8 -# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# If the ALLOW_UNICODE_NAMES tag is set to YES, Doxygen will allow non-ASCII # characters to appear in the names of generated files. If set to NO, non-ASCII # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode # U+3044. @@ -101,7 +107,7 @@ CREATE_SUBDIRS_LEVEL = 8 ALLOW_UNICODE_NAMES = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this +# documentation generated by Doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, # Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English @@ -115,14 +121,14 @@ ALLOW_UNICODE_NAMES = NO OUTPUT_LANGUAGE = English -# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# If the BRIEF_MEMBER_DESC tag is set to YES, Doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. # The default value is: YES. BRIEF_MEMBER_DESC = YES -# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# If the REPEAT_BRIEF tag is set to YES, Doxygen will prepend the brief # description of a member or function before the detailed description # # Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the @@ -153,13 +159,13 @@ ABBREVIATE_BRIEF = "The $name class" \ the # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# doxygen will generate a detailed section even if there is only a brief +# Doxygen will generate a detailed section even if there is only a brief # description. # The default value is: NO. ALWAYS_DETAILED_SEC = NO -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# If the INLINE_INHERITED_MEMB tag is set to YES, Doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. @@ -167,7 +173,7 @@ ALWAYS_DETAILED_SEC = NO INLINE_INHERITED_MEMB = NO -# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# If the FULL_PATH_NAMES tag is set to YES, Doxygen will prepend the full path # before files name in the file list and in the header files. If set to NO the # shortest path that makes the file name unique will be used # The default value is: YES. @@ -177,11 +183,11 @@ FULL_PATH_NAMES = YES # The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. # Stripping is only done if one of the specified strings matches the left-hand # part of the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the path to +# If left blank the directory from which Doxygen is run is used as the path to # strip. # # Note that you can specify absolute paths here, but also relative paths, which -# will be relative from the directory where doxygen is started. +# will be relative from the directory where Doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. STRIP_FROM_PATH = @@ -195,41 +201,42 @@ STRIP_FROM_PATH = STRIP_FROM_INC_PATH = -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but -# less readable) file names. This can be useful is your file systems doesn't +# If the SHORT_NAMES tag is set to YES, Doxygen will generate much shorter (but +# less readable) file names. This can be useful if your file system doesn't # support long names like on DOS, Mac, or CD-ROM. # The default value is: NO. SHORT_NAMES = NO -# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the -# first line (until the first dot) of a Javadoc-style comment as the brief -# description. If set to NO, the Javadoc-style will behave just like regular Qt- -# style comments (thus requiring an explicit @brief command for a brief -# description.) +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen will interpret the +# first line (until the first dot, question mark or exclamation mark) of a +# Javadoc-style comment as the brief description. If set to NO, the Javadoc- +# style will behave just like regular Qt-style comments (thus requiring an +# explicit @brief command for a brief description.) # The default value is: NO. JAVADOC_AUTOBRIEF = YES -# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# If the JAVADOC_BANNER tag is set to YES then Doxygen will interpret a line # such as # /*************** # as being the beginning of a Javadoc-style comment "banner". If set to NO, the # Javadoc-style will behave just like regular comments and it will not be -# interpreted by doxygen. +# interpreted by Doxygen. # The default value is: NO. JAVADOC_BANNER = NO -# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first -# line (until the first dot) of a Qt-style comment as the brief description. If -# set to NO, the Qt-style will behave just like regular Qt-style comments (thus -# requiring an explicit \brief command for a brief description.) +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will interpret the first +# line (until the first dot, question mark or exclamation mark) of a Qt-style +# comment as the brief description. If set to NO, the Qt-style will behave just +# like regular Qt-style comments (thus requiring an explicit \brief command for +# a brief description.) # The default value is: NO. QT_AUTOBRIEF = NO -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen treat a # multi-line C++ special comment block (i.e. a block of //! or /// comments) as # a brief description. This used to be the default behavior. The new default is # to treat a multi-line C++ comment block as a detailed description. Set this @@ -241,10 +248,10 @@ QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO -# By default Python docstrings are displayed as preformatted text and doxygen's +# By default Python docstrings are displayed as preformatted text and Doxygen's # special commands cannot be used. By setting PYTHON_DOCSTRING to NO the -# doxygen's special commands can be used and the contents of the docstring -# documentation blocks is shown as doxygen documentation. +# Doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as Doxygen documentation. # The default value is: YES. PYTHON_DOCSTRING = YES @@ -255,7 +262,7 @@ PYTHON_DOCSTRING = YES INHERIT_DOCS = YES -# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# If the SEPARATE_MEMBER_PAGES tag is set to YES then Doxygen will produce a new # page for each member. If set to NO, the documentation of a member will be part # of the file/class/namespace that contains it. # The default value is: NO. @@ -325,40 +332,54 @@ OPTIMIZE_OUTPUT_SLICE = NO # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# language is one of the parsers supported by Doxygen: IDL, Java, JavaScript, # Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, # VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: # FortranFree, unknown formatted Fortran: Fortran. In the later case the parser # tries to guess whether the code is fixed or free formatted code, this is the -# default for Fortran type files). For instance to make doxygen treat .inc files +# default for Fortran type files). For instance to make Doxygen treat .inc files # as Fortran files (default is PHP), and .f files as C (default is Fortran), # use: inc=Fortran f=C. # # Note: For files without extension you can use no_extension as a placeholder. # # Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. When specifying no_extension you should add +# the files are not read by Doxygen. When specifying no_extension you should add # * to the FILE_PATTERNS. # # Note see also the list of default file extension mappings. EXTENSION_MAPPING = -# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments # according to the Markdown format, which allows for more readable # documentation. See https://daringfireball.net/projects/markdown/ for details. -# The output of markdown processing is further processed by doxygen, so you can -# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# The output of markdown processing is further processed by Doxygen, so you can +# mix Doxygen, HTML, and XML commands with Markdown formatting. Disable only in # case of backward compatibilities issues. # The default value is: YES. MARKDOWN_SUPPORT = YES +# If the MARKDOWN_STRICT tag is enabled then Doxygen treats text in comments as +# Markdown formatted also in cases where Doxygen's native markup format +# conflicts with that of Markdown. This is only relevant in cases where +# backticks are used. Doxygen's native markup style allows a single quote to end +# a text fragment started with a backtick and then treat it as a piece of quoted +# text, whereas in Markdown such text fragment is treated as verbatim and only +# ends when a second matching backtick is found. Also, Doxygen's native markup +# format requires double quotes to be escaped when they appear in a backtick +# section, whereas this is not needed for Markdown. +# The default value is: YES. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_STRICT = YES + # When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up # to that level are automatically included in the table of contents, even if # they do not have an id attribute. # Note: This feature currently applies only to Markdown headings. -# Minimum value: 0, maximum value: 99, default value: 5. +# Minimum value: 0, maximum value: 99, default value: 6. # This tag requires that the tag MARKDOWN_SUPPORT is set to YES. TOC_INCLUDE_HEADINGS = 5 @@ -374,20 +395,29 @@ TOC_INCLUDE_HEADINGS = 5 MARKDOWN_ID_STYLE = DOXYGEN -# When enabled doxygen tries to link words that correspond to documented +# When enabled Doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or -# globally by setting AUTOLINK_SUPPORT to NO. +# globally by setting AUTOLINK_SUPPORT to NO. Words listed in the +# AUTOLINK_IGNORE_WORDS tag are excluded from automatic linking. # The default value is: YES. AUTOLINK_SUPPORT = YES +# This tag specifies a list of words that, when matching the start of a word in +# the documentation, will suppress auto links generation, if it is enabled via +# AUTOLINK_SUPPORT. This list does not affect links explicitly created using # +# or the \link or \ref commands. +# This tag requires that the tag AUTOLINK_SUPPORT is set to YES. + +AUTOLINK_IGNORE_WORDS = + # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should set this -# tag to YES in order to let doxygen match functions declarations and +# tag to YES in order to let Doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); -# versus func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. +# versus func(std::string) {}). This also makes the inheritance and +# collaboration diagrams that involve STL classes more complete and accurate. # The default value is: NO. BUILTIN_STL_SUPPORT = NO @@ -399,16 +429,16 @@ BUILTIN_STL_SUPPORT = NO CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen -# will parse them like normal C++ but will assume all classes use public instead -# of private inheritance when no explicit protection keyword is present. +# https://www.riverbankcomputing.com/software) sources only. Doxygen will parse +# them like normal C++ but will assume all classes use public instead of private +# inheritance when no explicit protection keyword is present. # The default value is: NO. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate # getter and setter methods for a property. Setting this option to YES will make -# doxygen to replace the get and set methods by a property in the documentation. +# Doxygen to replace the get and set methods by a property in the documentation. # This will only work if the methods are indeed getting or setting a simple # type. If this is not the case, or you want to show the methods anyway, you # should set this option to NO. @@ -417,7 +447,7 @@ SIP_SUPPORT = NO IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES then doxygen will reuse the documentation of the first +# tag is set to YES then Doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. # The default value is: NO. @@ -475,18 +505,18 @@ TYPEDEF_HIDES_STRUCT = NO # The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This # cache is used to resolve symbols given their name and scope. Since this can be # an expensive process and often the same symbol appears multiple times in the -# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small -# doxygen will become slower. If the cache is too large, memory is wasted. The +# code, Doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# Doxygen will become slower. If the cache is too large, memory is wasted. The # cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range # is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 -# symbols. At the end of a run doxygen will report the cache usage and suggest +# symbols. At the end of a run Doxygen will report the cache usage and suggest # the optimal cache size from a speed point of view. # Minimum value: 0, maximum value: 9, default value: 0. LOOKUP_CACHE_SIZE = 0 -# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use -# during processing. When set to 0 doxygen will based this on the number of +# The NUM_PROC_THREADS specifies the number of threads Doxygen is allowed to use +# during processing. When set to 0 Doxygen will based this on the number of # cores available in the system. You can set it explicitly to a value larger # than 0 to get more control over the balance between CPU load and processing # speed. At this moment only the input processing can be done using multiple @@ -494,7 +524,7 @@ LOOKUP_CACHE_SIZE = 0 # which effectively disables parallel processing. Please report any issues you # encounter. Generating dot graphs in parallel is controlled by the # DOT_NUM_THREADS setting. -# Minimum value: 0, maximum value: 32, default value: 1. +# Minimum value: 0, maximum value: 512, default value: 1. NUM_PROC_THREADS = 1 @@ -510,7 +540,7 @@ TIMESTAMP = NO # Build related configuration options #--------------------------------------------------------------------------- -# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# If the EXTRACT_ALL tag is set to YES, Doxygen will assume all entities in # documentation are documented, even if no documentation was available. Private # class members and static file members will be hidden unless the # EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. @@ -576,7 +606,7 @@ EXTRACT_ANON_NSPACES = NO RESOLVE_UNNAMED_PARAMS = YES -# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members inside documented classes or files. If set to NO these # members will be included in the various overviews, but no documentation # section is generated. This option has no effect if EXTRACT_ALL is enabled. @@ -584,7 +614,7 @@ RESOLVE_UNNAMED_PARAMS = YES HIDE_UNDOC_MEMBERS = NO -# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. If set # to NO, these classes will be included in the various overviews. This option # will also hide undocumented C++ concepts if enabled. This option has no effect @@ -593,14 +623,22 @@ HIDE_UNDOC_MEMBERS = NO HIDE_UNDOC_CLASSES = NO -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# If the HIDE_UNDOC_NAMESPACES tag is set to YES, Doxygen will hide all +# undocumented namespaces that are normally visible in the namespace hierarchy. +# If set to NO, these namespaces will be included in the various overviews. This +# option has no effect if EXTRACT_ALL is enabled. +# The default value is: YES. + +HIDE_UNDOC_NAMESPACES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all friend # declarations. If set to NO, these declarations will be included in the # documentation. # The default value is: NO. HIDE_FRIEND_COMPOUNDS = NO -# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. If set to NO, these # blocks will be appended to the function's detailed documentation block. # The default value is: NO. @@ -614,7 +652,7 @@ HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO -# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# With the correct setting of option CASE_SENSE_NAMES Doxygen will better be # able to match the capabilities of the underlying filesystem. In case the # filesystem is case sensitive (i.e. it supports files in the same directory # whose names only differ in casing), the option must be set to YES to properly @@ -623,7 +661,7 @@ INTERNAL_DOCS = NO # output files written for symbols that only differ in casing, such as for two # classes, one named CLASS and the other named Class, and to also support # references to files without having to specify the exact matching casing. On -# Windows (including Cygwin) and MacOS, users should typically set this option +# Windows (including Cygwin) and macOS, users should typically set this option # to NO, whereas on Linux or other Unix flavors it should typically be set to # YES. # Possible values are: SYSTEM, NO and YES. @@ -631,14 +669,14 @@ INTERNAL_DOCS = NO CASE_SENSE_NAMES = NO -# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# If the HIDE_SCOPE_NAMES tag is set to NO then Doxygen will show members with # their full class and namespace scopes in the documentation. If set to YES, the # scope will be hidden. # The default value is: NO. HIDE_SCOPE_NAMES = NO -# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then Doxygen will # append additional text to a page's title, such as Class Reference. If set to # YES the compound reference will be hidden. # The default value is: NO. @@ -651,7 +689,7 @@ HIDE_COMPOUND_REFERENCE= NO SHOW_HEADERFILE = YES -# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# If the SHOW_INCLUDE_FILES tag is set to YES then Doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. @@ -664,7 +702,7 @@ SHOW_INCLUDE_FILES = YES SHOW_GROUPED_MEMB_INC = NO -# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen will list include # files with double quotes in the documentation rather than with sharp brackets. # The default value is: NO. @@ -676,14 +714,14 @@ FORCE_LOCAL_INCLUDES = NO INLINE_INFO = YES -# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# If the SORT_MEMBER_DOCS tag is set to YES then Doxygen will sort the # (detailed) documentation of file and class members alphabetically by member # name. If set to NO, the members will appear in declaration order. # The default value is: YES. SORT_MEMBER_DOCS = YES -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# If the SORT_BRIEF_DOCS tag is set to YES then Doxygen will sort the brief # descriptions of file, namespace and class members alphabetically by member # name. If set to NO, the members will appear in declaration order. Note that # this will also influence the order of the classes in the class list. @@ -691,7 +729,7 @@ SORT_MEMBER_DOCS = YES SORT_BRIEF_DOCS = NO -# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then Doxygen will sort the # (brief and detailed) documentation of class members so that constructors and # destructors are listed first. If set to NO the constructors will appear in the # respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. @@ -703,7 +741,7 @@ SORT_BRIEF_DOCS = NO SORT_MEMBERS_CTORS_1ST = NO -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# If the SORT_GROUP_NAMES tag is set to YES then Doxygen will sort the hierarchy # of group names into alphabetical order. If set to NO the group names will # appear in their defined order. # The default value is: NO. @@ -720,11 +758,11 @@ SORT_GROUP_NAMES = NO SORT_BY_SCOPE_NAME = NO -# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# If the STRICT_PROTO_MATCHING option is enabled and Doxygen fails to do proper # type resolution of all parameters of a function it will reject a match between # the prototype and the implementation of a member function even if there is # only one candidate or it is obvious which candidate to choose by doing a -# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# simple string match. By disabling STRICT_PROTO_MATCHING Doxygen will still # accept a match between prototype and implementation in such cases. # The default value is: NO. @@ -755,6 +793,27 @@ GENERATE_BUGLIST = YES GENERATE_DEPRECATEDLIST= YES +# The GENERATE_REQUIREMENTS tag can be used to enable (YES) or disable (NO) the +# requirements page. When enabled, this page is automatically created when at +# least one comment block with a \requirement command appears in the input. +# The default value is: YES. + +GENERATE_REQUIREMENTS = YES + +# The REQ_TRACEABILITY_INFO tag controls if traceability information is shown on +# the requirements page (only relevant when using \requirement comment blocks). +# The setting NO will disable the traceability information altogether. The +# setting UNSATISFIED_ONLY will show a list of requirements that are missing a +# satisfies relation (through the command: \satisfies). Similarly the setting +# UNVERIFIED_ONLY will show a list of requirements that are missing a verifies +# relation (through the command: \verifies). Setting the tag to YES (the +# default) will show both lists if applicable. +# Possible values are: YES, NO, UNSATISFIED_ONLY and UNVERIFIED_ONLY. +# The default value is: YES. +# This tag requires that the tag GENERATE_REQUIREMENTS is set to YES. + +REQ_TRACEABILITY_INFO = YES + # The ENABLED_SECTIONS tag can be used to enable conditional documentation # sections, marked by \if ... \endif and \cond # ... \endcond blocks. @@ -794,25 +853,25 @@ SHOW_FILES = YES SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from +# Doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command command input-file, where command is the value of the # FILE_VERSION_FILTER tag, and input-file is the name of an input file provided -# by doxygen. Whatever the program writes to standard output is used as the file +# by Doxygen. Whatever the program writes to standard output is used as the file # version. For an example see the documentation. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed -# by doxygen. The layout file controls the global structure of the generated +# by Doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. To create the layout file -# that represents doxygen's defaults, run doxygen with the -l option. You can +# that represents Doxygen's defaults, run Doxygen with the -l option. You can # optionally specify a file name after the option, if omitted DoxygenLayout.xml # will be used as the name of the layout file. See also section "Changing the # layout of pages" for information. # -# Note that if you run doxygen from a directory containing a file called -# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# Note that if you run Doxygen from a directory containing a file called +# DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE # tag is left empty. LAYOUT_FILE = @@ -827,19 +886,35 @@ LAYOUT_FILE = CITE_BIB_FILES = +# The EXTERNAL_TOOL_PATH tag can be used to extend the search path (PATH +# environment variable) so that external tools such as latex and gs can be +# found. +# Note: Directories specified with EXTERNAL_TOOL_PATH are added in front of the +# path already specified by the PATH variable, and are added in the order +# specified. +# Note: This option is particularly useful for macOS version 14 (Sonoma) and +# higher, when running Doxygen from Doxywizard, because in this case any user- +# defined changes to the PATH are ignored. A typical example on macOS is to set +# EXTERNAL_TOOL_PATH = /Library/TeX/texbin /usr/local/bin +# together with the standard path, the full search path used by doxygen when +# launching external tools will then become +# PATH=/Library/TeX/texbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + +EXTERNAL_TOOL_PATH = + #--------------------------------------------------------------------------- # Configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated to -# standard output by doxygen. If QUIET is set to YES this implies that the +# standard output by Doxygen. If QUIET is set to YES this implies that the # messages are off. # The default value is: NO. QUIET = YES # The WARNINGS tag can be used to turn on/off the warning messages that are -# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# generated to standard error (stderr) by Doxygen. If WARNINGS is set to YES # this implies that the warnings are on. # # Tip: Turn warnings on while writing the documentation. @@ -847,14 +922,14 @@ QUIET = YES WARNINGS = YES -# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# If the WARN_IF_UNDOCUMENTED tag is set to YES then Doxygen will generate # warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag # will automatically be disabled. # The default value is: YES. WARN_IF_UNDOCUMENTED = YES -# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# If the WARN_IF_DOC_ERROR tag is set to YES, Doxygen will generate warnings for # potential errors in the documentation, such as documenting some parameters in # a documented function twice, or documenting parameters that don't exist or # using markup commands wrongly. @@ -862,8 +937,8 @@ WARN_IF_UNDOCUMENTED = YES WARN_IF_DOC_ERROR = YES -# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete -# function parameter documentation. If set to NO, doxygen will accept that some +# If WARN_IF_INCOMPLETE_DOC is set to YES, Doxygen will warn about incomplete +# function parameter documentation. If set to NO, Doxygen will accept that some # parameters have no documentation without warning. # The default value is: YES. @@ -871,7 +946,7 @@ WARN_IF_INCOMPLETE_DOC = YES # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong parameter +# value. If set to NO, Doxygen will only warn about wrong parameter # documentation, but not about the absence of documentation. If EXTRACT_ALL is # set to YES then this flag will automatically be disabled. See also # WARN_IF_INCOMPLETE_DOC @@ -879,20 +954,28 @@ WARN_IF_INCOMPLETE_DOC = YES WARN_NO_PARAMDOC = NO -# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about -# undocumented enumeration values. If set to NO, doxygen will accept +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, Doxygen will warn about +# undocumented enumeration values. If set to NO, Doxygen will accept # undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag # will automatically be disabled. # The default value is: NO. WARN_IF_UNDOC_ENUM_VAL = NO -# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# If WARN_LAYOUT_FILE option is set to YES, Doxygen will warn about issues found +# while parsing the user defined layout file, such as missing or wrong elements. +# See also LAYOUT_FILE for details. If set to NO, problems with the layout file +# will be suppressed. +# The default value is: YES. + +WARN_LAYOUT_FILE = YES + +# If the WARN_AS_ERROR tag is set to YES then Doxygen will immediately stop when # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS -# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but -# at the end of the doxygen process doxygen will return with a non-zero status. -# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves -# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# then Doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the Doxygen process Doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then Doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined Doxygen will not # write the warning messages in between other messages but write them at the end # of a run, in case a WARN_LOGFILE is defined the warning messages will be # besides being in the defined file also be shown at the end of a run, unless @@ -903,7 +986,7 @@ WARN_IF_UNDOC_ENUM_VAL = NO WARN_AS_ERROR = NO -# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# The WARN_FORMAT tag determines the format of the warning messages that Doxygen # can produce. The string should contain the $file, $line, and $text tags, which # will be replaced by the file and line number from which the warning originated # and the warning text. Optionally the format may contain $version, which will @@ -916,7 +999,7 @@ WARN_FORMAT = "$file:$line: $text" # In the $text part of the WARN_FORMAT command it is possible that a reference # to a more specific place is given. To make it easier to jump to this place -# (outside of doxygen) the user can define a custom "cut" / "paste" string. +# (outside of Doxygen) the user can define a custom "cut" / "paste" string. # Example: # WARN_LINE_FORMAT = "'vi $file +$line'" # See also: WARN_FORMAT @@ -950,7 +1033,7 @@ INPUT = inkcpp/include \ inkcpp_c/include # This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses # libiconv (or the iconv built into libc) for the transcoding. See the libiconv # documentation (see: # https://www.gnu.org/software/libiconv/) for the list of possible encodings. @@ -960,12 +1043,12 @@ INPUT = inkcpp/include \ INPUT_ENCODING = UTF-8 # This tag can be used to specify the character encoding of the source files -# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify +# that Doxygen parses. The INPUT_FILE_ENCODING tag can be used to specify # character encoding on a per file pattern basis. Doxygen will compare the file # name with each pattern and apply the encoding instead of the default -# INPUT_ENCODING) if there is a match. The character encodings are a list of the -# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding -# "INPUT_ENCODING" for further information on supported encodings. +# INPUT_ENCODING if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). +# See also: INPUT_ENCODING for further information on supported encodings. INPUT_FILE_ENCODING = @@ -975,16 +1058,16 @@ INPUT_FILE_ENCODING = # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not -# read by doxygen. +# read by Doxygen. # # Note the list of default checked file patterns might differ from the list of # default file extension mappings. # # If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, -# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, -# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, # *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be -# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, # *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ @@ -1043,7 +1126,7 @@ RECURSIVE = NO # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. # -# Note that relative paths are relative to the directory from which doxygen is +# Note that relative paths are relative to the directory from which Doxygen is # run. EXCLUDE = @@ -1070,7 +1153,9 @@ EXCLUDE_PATTERNS = # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -EXCLUDE_SYMBOLS = ink::list_flag ink::runtime::internal* ink::internal* +EXCLUDE_SYMBOLS = ink::list_flag \ + ink::runtime::internal* \ + ink::internal* # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include @@ -1098,7 +1183,7 @@ EXAMPLE_RECURSIVE = NO IMAGE_PATH = -# The INPUT_FILTER tag can be used to specify a program that doxygen should +# The INPUT_FILTER tag can be used to specify a program that Doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command: # @@ -1113,14 +1198,14 @@ IMAGE_PATH = # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. # -# Note that doxygen will use the data processed and written to standard output +# Note that Doxygen will use the data processed and written to standard output # for further processing, therefore nothing else, like debug statements or used # commands (so in case of a Windows batch file always use @echo OFF), should be # written to standard output. # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. +# properly processed by Doxygen. INPUT_FILTER = @@ -1133,7 +1218,7 @@ INPUT_FILTER = # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. +# properly processed by Doxygen. FILTER_PATTERNS = @@ -1155,10 +1240,19 @@ FILTER_SOURCE_PATTERNS = # If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that # is part of the input, its contents will be placed on the main page # (index.html). This can be useful if you have a project on for instance GitHub -# and want to reuse the introduction page also for the doxygen output. +# and want to reuse the introduction page also for the Doxygen output. USE_MDFILE_AS_MAINPAGE = +# If the IMPLICIT_DIR_DOCS tag is set to YES, any README.md file found in sub- +# directories of the project's root, is used as the documentation for that sub- +# directory, except when the README.md starts with a \dir, \page or \mainpage +# command. If set to NO, the README.md file needs to start with an explicit \dir +# command in order to be used as directory documentation. +# The default value is: YES. + +IMPLICIT_DIR_DOCS = YES + # The Fortran standard specifies that for fixed formatted Fortran code all # characters from position 72 are to be considered as comment. A common # extension is to allow longer lines before the automatic comment starts. The @@ -1182,12 +1276,13 @@ FORTRAN_COMMENT_AFTER = 72 SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. +# multi-line macros, enums or list initialized variables directly into the +# documentation. # The default value is: NO. INLINE_SOURCES = NO -# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct Doxygen to hide any # special comment blocks from generated source code fragments. Normal C, C++ and # Fortran comments will always remain visible. # The default value is: YES. @@ -1225,7 +1320,7 @@ REFERENCES_LINK_SOURCE = YES SOURCE_TOOLTIPS = YES # If the USE_HTAGS tag is set to YES then the references to source code will -# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# point to the HTML generated by the htags(1) tool instead of Doxygen built-in # source browser. The htags tool is part of GNU's global source tagging system # (see https://www.gnu.org/software/global/global.html). You will need version # 4.8.6 or higher. @@ -1239,14 +1334,14 @@ SOURCE_TOOLTIPS = YES # Doxygen will invoke htags (and that will in turn invoke gtags), so these # tools must be available from the command line (i.e. in the search path). # -# The result: instead of the source browser generated by doxygen, the links to +# The result: instead of the source browser generated by Doxygen, the links to # source code will now point to the output of htags. # The default value is: NO. # This tag requires that the tag SOURCE_BROWSER is set to YES. USE_HTAGS = NO -# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# If the VERBATIM_HEADERS tag is set the YES then Doxygen will generate a # verbatim copy of the header file for each class for which an include is # specified. Set to NO to disable this. # See also: Section \class. @@ -1254,6 +1349,46 @@ USE_HTAGS = NO VERBATIM_HEADERS = YES +# If the CLANG_ASSISTED_PARSING tag is set to YES then Doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which Doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then Doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by Doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- @@ -1278,7 +1413,7 @@ IGNORE_PREFIX = # Configuration options related to the HTML output #--------------------------------------------------------------------------- -# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# If the GENERATE_HTML tag is set to YES, Doxygen will generate HTML output # The default value is: YES. GENERATE_HTML = YES @@ -1299,40 +1434,40 @@ HTML_OUTPUT = html HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a user-defined HTML header file for -# each generated HTML page. If the tag is left blank doxygen will generate a +# each generated HTML page. If the tag is left blank Doxygen will generate a # standard header. # # To get valid HTML the header file that includes any scripts and style sheets -# that doxygen needs, which is dependent on the configuration options used (e.g. +# that Doxygen needs, which is dependent on the configuration options used (e.g. # the setting GENERATE_TREEVIEW). It is highly recommended to start with a # default header using # doxygen -w html new_header.html new_footer.html new_stylesheet.css # YourConfigFile # and then modify the file new_header.html. See also section "Doxygen usage" -# for information on how to generate the default header that doxygen normally +# for information on how to generate the default header that Doxygen normally # uses. # Note: The header is subject to change so you typically have to regenerate the -# default header when upgrading to a newer version of doxygen. For a description +# default header when upgrading to a newer version of Doxygen. For a description # of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each -# generated HTML page. If the tag is left blank doxygen will generate a standard +# generated HTML page. If the tag is left blank Doxygen will generate a standard # footer. See HTML_HEADER for more information on how to generate a default # footer and what special commands can be used inside the footer. See also # section "Doxygen usage" for information on how to generate the default footer -# that doxygen normally uses. +# that Doxygen normally uses. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading style # sheet that is used by each HTML page. It can be used to fine-tune the look of -# the HTML output. If left blank doxygen will generate a default style sheet. +# the HTML output. If left blank Doxygen will generate a default style sheet. # See also section "Doxygen usage" for information on how to generate the style -# sheet that doxygen normally uses. +# sheet that Doxygen normally uses. # Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as # it is more robust and this tag (HTML_STYLESHEET) will in the future become # obsolete. @@ -1342,7 +1477,7 @@ HTML_STYLESHEET = # The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined # cascading style sheets that are included after the standard style sheets -# created by doxygen. Using this option one can overrule certain style aspects. +# created by Doxygen. Using this option one can overrule certain style aspects. # This is preferred over using HTML_STYLESHEET since it does not replace the # standard style sheet and is therefore more robust against future updates. # Doxygen will copy the style sheet files to the output directory. @@ -1370,11 +1505,11 @@ HTML_EXTRA_FILES = Documentation/inkcpp_py.html # The HTML_COLORSTYLE tag can be used to specify if the generated HTML output # should be rendered with a dark or light theme. -# Possible values are: LIGHT always generate light mode output, DARK always -# generate dark mode output, AUTO_LIGHT automatically set the mode according to -# the user preference, use light mode if no preference is set (the default), -# AUTO_DARK automatically set the mode according to the user preference, use -# dark mode if no preference is set and TOGGLE allow to user to switch between +# Possible values are: LIGHT always generates light mode output, DARK always +# generates dark mode output, AUTO_LIGHT automatically sets the mode according +# to the user preference, uses light mode if no preference is set (the default), +# AUTO_DARK automatically sets the mode according to the user preference, uses +# dark mode if no preference is set and TOGGLE allows a user to switch between # light and dark mode via a button. # The default value is: AUTO_LIGHT. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1437,6 +1572,26 @@ HTML_DYNAMIC_SECTIONS = NO HTML_CODE_FOLDING = YES +# If the HTML_COPY_CLIPBOARD tag is set to YES then Doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# Doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1454,7 +1609,7 @@ HTML_INDEX_NUM_ENTRIES = 32 # generated that can be used as input for Apple's Xcode 3 integrated development # environment (see: # https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To -# create a documentation set, doxygen will generate a Makefile in the HTML +# create a documentation set, Doxygen will generate a Makefile in the HTML # output directory. Running make will produce the docset in that directory and # running make install will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at @@ -1502,18 +1657,18 @@ DOCSET_PUBLISHER_ID = org.doxygen.Publisher DOCSET_PUBLISHER_NAME = Publisher -# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# If the GENERATE_HTMLHELP tag is set to YES then Doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop # on Windows. In the beginning of 2021 Microsoft took the original page, with -# a.o. the download links, offline the HTML help workshop was already many years -# in maintenance mode). You can download the HTML help workshop from the web -# archives at Installation executable (see: -# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo -# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). +# a.o. the download links, offline (the HTML help workshop was already many +# years in maintenance mode). You can download the HTML help workshop from the +# web archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/https://download.microsoft.com/downl +# oad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). # # The HTML Help Workshop contains a compiler that can convert all HTML output -# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# generated by Doxygen into a single compiled HTML file (.chm). Compiled HTML # files are now used as the Windows 98 help format, and will replace the old # Windows help format (.hlp) on all Windows platforms in the future. Compressed # HTML files also contain an index, a table of contents, and you can search for @@ -1533,7 +1688,7 @@ CHM_FILE = # The HHC_LOCATION tag can be used to specify the location (absolute path # including file name) of the HTML help compiler (hhc.exe). If non-empty, -# doxygen will try to run the HTML help compiler on the generated index.hhp. +# Doxygen will try to run the HTML help compiler on the generated index.hhp. # The file has to be specified with full path. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. @@ -1635,7 +1790,7 @@ QHP_CUST_FILTER_ATTRS = QHP_SECT_FILTER_ATTRS = # The QHG_LOCATION tag can be used to specify the location (absolute path -# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# including file name) of Qt's qhelpgenerator. If non-empty Doxygen will try to # run qhelpgenerator on the generated .qhp file. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1680,29 +1835,38 @@ DISABLE_INDEX = NO # (i.e. any modern browser). Windows users are probably better off using the # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can # further fine tune the look of the index (see "Fine-tuning the output"). As an -# example, the default style sheet generated by doxygen has an example that +# example, the default style sheet generated by Doxygen has an example that # shows how to put an image at the root of the tree instead of the PROJECT_NAME. -# Since the tree basically has the same information as the tab index, you could -# consider setting DISABLE_INDEX to YES when enabling this option. -# The default value is: NO. +# Since the tree basically has more details information than the tab index, you +# could consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_TREEVIEW = YES -# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the -# FULL_SIDEBAR option determines if the side bar is limited to only the treeview -# area (value NO) or if it should extend to the full height of the window (value -# YES). Setting this to YES gives a layout similar to -# https://docs.readthedocs.io with more room for contents, but less room for the -# project logo, title, and description. If either GENERATE_TREEVIEW or -# DISABLE_INDEX is set to NO, this option has no effect. +# When GENERATE_TREEVIEW is set to YES, the PAGE_OUTLINE_PANEL option determines +# if an additional navigation panel is shown at the right hand side of the +# screen, displaying an outline of the contents of the main page, similar to +# e.g. https://developer.android.com/reference If GENERATE_TREEVIEW is set to +# NO, this option has no effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +PAGE_OUTLINE_PANEL = YES + +# When GENERATE_TREEVIEW is set to YES, the FULL_SIDEBAR option determines if +# the side bar is limited to only the treeview area (value NO) or if it should +# extend to the full height of the window (value YES). Setting this to YES gives +# a layout similar to e.g. https://docs.readthedocs.io with more room for +# contents, but less room for the project logo, title, and description. If +# GENERATE_TREEVIEW is set to NO, this option has no effect. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. FULL_SIDEBAR = NO # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that -# doxygen will group on one line in the generated HTML documentation. +# Doxygen will group on one line in the generated HTML documentation. # # Note that a value of 0 will completely suppress the enum values from appearing # in the overview section. @@ -1711,6 +1875,12 @@ FULL_SIDEBAR = NO ENUM_VALUES_PER_LINE = 4 +# When the SHOW_ENUM_VALUES tag is set doxygen will show the specified +# enumeration values besides the enumeration mnemonics. +# The default value is: NO. + +SHOW_ENUM_VALUES = NO + # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used # to set the initial width (in pixels) of the frame in which the tree is shown. # Minimum value: 0, maximum value: 1500, default value: 250. @@ -1718,21 +1888,21 @@ ENUM_VALUES_PER_LINE = 4 TREEVIEW_WIDTH = 250 -# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# If the EXT_LINKS_IN_WINDOW option is set to YES, Doxygen will open links to # external symbols imported via tag files in a separate window. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. EXT_LINKS_IN_WINDOW = NO -# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email +# If the OBFUSCATE_EMAILS tag is set to YES, Doxygen will obfuscate email # addresses. # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. OBFUSCATE_EMAILS = YES -# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# If the HTML_FORMULA_FORMAT option is set to svg, Doxygen will use the pdf2svg # tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see # https://inkscape.org) to generate formulas as SVG images instead of PNGs for # the HTML output. These images will generally look nicer at scaled resolutions. @@ -1745,7 +1915,7 @@ HTML_FORMULA_FORMAT = png # Use this tag to change the font size of LaTeX formulas included as images in # the HTML documentation. When you change the font size after a successful -# doxygen run you need to manually remove any form_*.png images from the HTML +# Doxygen run you need to manually remove any form_*.png images from the HTML # output directory to force them to be regenerated. # Minimum value: 8, maximum value: 50, default value: 10. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1774,7 +1944,7 @@ USE_MATHJAX = NO # regards to the different settings, so it is possible that also other MathJax # settings have to be changed when switching between the different MathJax # versions. -# Possible values are: MathJax_2 and MathJax_3. +# Possible values are: MathJax_2, MathJax_3 and MathJax_4. # The default value is: MathJax_2. # This tag requires that the tag USE_MATHJAX is set to YES. @@ -1783,13 +1953,14 @@ MATHJAX_VERSION = MathJax_2 # When MathJax is enabled you can set the default output format to be used for # the MathJax output. For more details about the output format see MathJax # version 2 (see: -# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# https://docs.mathjax.org/en/v2.7/output.html), MathJax version 3 (see: +# https://docs.mathjax.org/en/v3.2/output/index.html) and MathJax version 4 # (see: -# http://docs.mathjax.org/en/latest/web/components/output.html). +# https://docs.mathjax.org/en/v4.0/output/index.htm). # Possible values are: HTML-CSS (which is slower, but has the best # compatibility. This is the name for Mathjax version 2, for MathJax version 3 # this will be translated into chtml), NativeMML (i.e. MathML. Only supported -# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# for MathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This # is the name for Mathjax version 3, for MathJax version 2 this will be # translated into HTML-CSS) and SVG. # The default value is: HTML-CSS. @@ -1798,46 +1969,60 @@ MATHJAX_VERSION = MathJax_2 MATHJAX_FORMAT = HTML-CSS # When MathJax is enabled you need to specify the location relative to the HTML -# output directory using the MATHJAX_RELPATH option. The destination directory -# should contain the MathJax.js script. For instance, if the mathjax directory -# is located at the same level as the HTML output directory, then -# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax -# Content Delivery Network so you can quickly see the result without installing -# MathJax. However, it is strongly recommended to install a local copy of -# MathJax from https://www.mathjax.org before deployment. The default value is: +# output directory using the MATHJAX_RELPATH option. For Mathjax version 2 the +# destination directory should contain the MathJax.js script. For instance, if +# the mathjax directory is located at the same level as the HTML output +# directory, then MATHJAX_RELPATH should be ../mathjax. For Mathjax versions 3 +# and 4 the destination directory should contain the tex-.js script +# (where is either chtml or svg). The default value points to the +# MathJax Content Delivery Network so you can quickly see the result without +# installing MathJax. However, it is strongly recommended to install a local +# copy of MathJax from https://www.mathjax.org before deployment. The default +# value is: # - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 # - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 +# - in case of MathJax version 4: https://cdn.jsdelivr.net/npm/mathjax@4 # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/ # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example -# for MathJax version 2 (see -# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): +# for MathJax version 2 (see https://docs.mathjax.org/en/v2.7/tex.html): # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols # For example for MathJax version 3 (see -# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# https://docs.mathjax.org/en/v3.2/input/tex/extensions/): # MATHJAX_EXTENSIONS = ams +# For example for MathJax version 4 (see +# https://docs.mathjax.org/en/v4.0/input/tex/extensions/): +# MATHJAX_EXTENSIONS = units +# Note that for Mathjax version 4 quite a few extensions are already +# automatically loaded. To disable a package in Mathjax version 4 one can use +# the package name prepended with a minus sign (- like MATHJAX_EXTENSIONS += +# -textmacros) # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = -# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces -# of code that will be used on startup of the MathJax code. See the MathJax site -# (see: -# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an -# example see the documentation. +# The MATHJAX_CODEFILE tag can be used to specify a file with JavaScript pieces +# of code that will be used on startup of the MathJax code. See the Mathjax site +# for more details: +# - MathJax version 2 (see: +# https://docs.mathjax.org/en/v2.7/) +# - MathJax version 3 (see: +# https://docs.mathjax.org/en/v3.2/) +# - MathJax version 4 (see: +# https://docs.mathjax.org/en/v4.0/) For an example see the documentation. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_CODEFILE = -# When the SEARCHENGINE tag is enabled doxygen will generate a search box for -# the HTML output. The underlying search engine uses javascript and DHTML and +# When the SEARCHENGINE tag is enabled Doxygen will generate a search box for +# the HTML output. The underlying search engine uses JavaScript and DHTML and # should work on any modern browser. Note that when using HTML help # (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) # there is already a search function so this one should typically be disabled. -# For large projects the javascript based search engine can be slow, then +# For large projects the JavaScript based search engine can be slow, then # enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to # search using the keyboard; to jump to the search box use + S # (what the is depends on the OS and browser, but it is typically @@ -1856,7 +2041,7 @@ SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be # implemented using a web server instead of a web client using JavaScript. There # are two flavors of web server based searching depending on the EXTERNAL_SEARCH -# setting. When disabled, doxygen will generate a PHP script for searching and +# setting. When disabled, Doxygen will generate a PHP script for searching and # an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing # and searching needs to be provided by external tools. See the section # "External Indexing and Searching" for details. @@ -1865,7 +2050,7 @@ SEARCHENGINE = YES SERVER_BASED_SEARCH = NO -# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP +# When EXTERNAL_SEARCH tag is enabled Doxygen will no longer generate the PHP # script for searching. Instead the search results are written to an XML file # which needs to be processed by an external indexer. Doxygen will invoke an # external search engine pointed to by the SEARCHENGINE_URL option to obtain the @@ -1910,7 +2095,7 @@ SEARCHDATA_FILE = searchdata.xml EXTERNAL_SEARCH_ID = -# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen +# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through Doxygen # projects other than the one defined by this configuration file, but that are # all added to the same external search index. Each project needs to have a # unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of @@ -1924,7 +2109,7 @@ EXTRA_SEARCH_MAPPINGS = # Configuration options related to the LaTeX output #--------------------------------------------------------------------------- -# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. +# If the GENERATE_LATEX tag is set to YES, Doxygen will generate LaTeX output. # The default value is: YES. GENERATE_LATEX = NO @@ -1969,7 +2154,7 @@ MAKEINDEX_CMD_NAME = makeindex LATEX_MAKEINDEX_CMD = makeindex -# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX +# If the COMPACT_LATEX tag is set to YES, Doxygen generates more compact LaTeX # documents. This may be useful for small projects and may help to save some # trees in general. # The default value is: NO. @@ -2000,15 +2185,15 @@ EXTRA_PACKAGES = # The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for # the generated LaTeX document. The header should contain everything until the -# first chapter. If it is left blank doxygen will generate a standard header. It +# first chapter. If it is left blank Doxygen will generate a standard header. It # is highly recommended to start with a default header using # doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty # and then modify the file new_header.tex. See also section "Doxygen usage" for -# information on how to generate the default header that doxygen normally uses. +# information on how to generate the default header that Doxygen normally uses. # # Note: Only use a user-defined header if you know what you are doing! # Note: The header is subject to change so you typically have to regenerate the -# default header when upgrading to a newer version of doxygen. The following +# default header when upgrading to a newer version of Doxygen. The following # commands have a special meaning inside the header (and footer): For a # description of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2017,10 +2202,10 @@ LATEX_HEADER = # The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for # the generated LaTeX document. The footer should contain everything after the -# last chapter. If it is left blank doxygen will generate a standard footer. See +# last chapter. If it is left blank Doxygen will generate a standard footer. See # LATEX_HEADER for more information on how to generate a default footer and what # special commands can be used inside the footer. See also section "Doxygen -# usage" for information on how to generate the default footer that doxygen +# usage" for information on how to generate the default footer that Doxygen # normally uses. Note: Only use a user-defined footer if you know what you are # doing! # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2029,7 +2214,7 @@ LATEX_FOOTER = # The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined # LaTeX style sheets that are included after the standard style sheets created -# by doxygen. Using this option one can overrule certain style aspects. Doxygen +# by Doxygen. Using this option one can overrule certain style aspects. Doxygen # will copy the style sheet files to the output directory. # Note: The order of the extra style sheet files is of importance (e.g. the last # style sheet in the list overrules the setting of the previous ones in the @@ -2055,7 +2240,7 @@ LATEX_EXTRA_FILES = PDF_HYPERLINKS = YES -# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as +# If the USE_PDFLATEX tag is set to YES, Doxygen will use the engine as # specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX # files. Set this option to YES, to get a higher quality PDF documentation. # @@ -2080,7 +2265,7 @@ USE_PDFLATEX = YES LATEX_BATCHMODE = NO -# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the +# If the LATEX_HIDE_INDICES tag is set to YES then Doxygen will not include the # index chapters (such as File Index, Compound Index, etc.) in the output. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2090,7 +2275,7 @@ LATEX_HIDE_INDICES = NO # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. See # https://en.wikipedia.org/wiki/BibTeX and \cite for more info. -# The default value is: plain. +# The default value is: plainnat. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_BIB_STYLE = plain @@ -2107,7 +2292,7 @@ LATEX_EMOJI_DIRECTORY = # Configuration options related to the RTF output #--------------------------------------------------------------------------- -# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The +# If the GENERATE_RTF tag is set to YES, Doxygen will generate RTF output. The # RTF output is optimized for Word 97 and may not look too pretty with other RTF # readers/editors. # The default value is: NO. @@ -2122,7 +2307,7 @@ GENERATE_RTF = NO RTF_OUTPUT = rtf -# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF +# If the COMPACT_RTF tag is set to YES, Doxygen generates more compact RTF # documents. This may be useful for small projects and may help to save some # trees in general. # The default value is: NO. @@ -2142,28 +2327,36 @@ COMPACT_RTF = NO RTF_HYPERLINKS = NO -# Load stylesheet definitions from file. Syntax is similar to doxygen's +# Load stylesheet definitions from file. Syntax is similar to Doxygen's # configuration file, i.e. a series of assignments. You only have to provide # replacements, missing definitions are set to their default value. # # See also section "Doxygen usage" for information on how to generate the -# default style sheet that doxygen normally uses. +# default style sheet that Doxygen normally uses. # This tag requires that the tag GENERATE_RTF is set to YES. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an RTF document. Syntax is -# similar to doxygen's configuration file. A template extensions file can be +# similar to Doxygen's configuration file. A template extensions file can be # generated using doxygen -e rtf extensionFile. # This tag requires that the tag GENERATE_RTF is set to YES. RTF_EXTENSIONS_FILE = +# The RTF_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the RTF_OUTPUT output directory. +# Note that the files will be copied as-is; there are no commands or markers +# available. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_EXTRA_FILES = + #--------------------------------------------------------------------------- # Configuration options related to the man page output #--------------------------------------------------------------------------- -# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for +# If the GENERATE_MAN tag is set to YES, Doxygen will generate man pages for # classes and files. # The default value is: NO. @@ -2194,7 +2387,7 @@ MAN_EXTENSION = .3 MAN_SUBDIR = -# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, then it # will generate one additional man file for each entity documented in the real # man page(s). These additional files only source the real man page, but without # them the man command would be unable to find the correct page. @@ -2207,7 +2400,7 @@ MAN_LINKS = NO # Configuration options related to the XML output #--------------------------------------------------------------------------- -# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that +# If the GENERATE_XML tag is set to YES, Doxygen will generate an XML file that # captures the structure of the code including all documentation. # The default value is: NO. @@ -2221,7 +2414,7 @@ GENERATE_XML = NO XML_OUTPUT = xml -# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program +# If the XML_PROGRAMLISTING tag is set to YES, Doxygen will dump the program # listings (including syntax highlighting and cross-referencing information) to # the XML output. Note that enabling this will significantly increase the size # of the XML output. @@ -2230,7 +2423,7 @@ XML_OUTPUT = xml XML_PROGRAMLISTING = YES -# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include +# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, Doxygen will include # namespace members in file scope as well, matching the HTML output. # The default value is: NO. # This tag requires that the tag GENERATE_XML is set to YES. @@ -2241,7 +2434,7 @@ XML_NS_MEMB_FILE_SCOPE = NO # Configuration options related to the DOCBOOK output #--------------------------------------------------------------------------- -# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files +# If the GENERATE_DOCBOOK tag is set to YES, Doxygen will generate Docbook files # that can be used to generate PDF. # The default value is: NO. @@ -2259,7 +2452,7 @@ DOCBOOK_OUTPUT = docbook # Configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- -# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an +# If the GENERATE_AUTOGEN_DEF tag is set to YES, Doxygen will generate an # AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. @@ -2271,8 +2464,8 @@ GENERATE_AUTOGEN_DEF = NO # Configuration options related to Sqlite3 output #--------------------------------------------------------------------------- -# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 -# database with symbols found by doxygen stored in tables. +# If the GENERATE_SQLITE3 tag is set to YES Doxygen will generate a Sqlite3 +# database with symbols found by Doxygen stored in tables. # The default value is: NO. GENERATE_SQLITE3 = NO @@ -2285,9 +2478,9 @@ GENERATE_SQLITE3 = NO SQLITE3_OUTPUT = sqlite3 -# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db -# database file will be recreated with each doxygen run. If set to NO, doxygen -# will warn if an a database file is already found and not modify it. +# The SQLITE3_RECREATE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each Doxygen run. If set to NO, Doxygen +# will warn if a database file is already found and not modify it. # The default value is: YES. # This tag requires that the tag GENERATE_SQLITE3 is set to YES. @@ -2297,7 +2490,7 @@ SQLITE3_RECREATE_DB = YES # Configuration options related to the Perl module output #--------------------------------------------------------------------------- -# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module +# If the GENERATE_PERLMOD tag is set to YES, Doxygen will generate a Perl module # file that captures the structure of the code including all documentation. # # Note that this feature is still experimental and incomplete at the moment. @@ -2305,7 +2498,7 @@ SQLITE3_RECREATE_DB = YES GENERATE_PERLMOD = NO -# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary +# If the PERLMOD_LATEX tag is set to YES, Doxygen will generate the necessary # Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI # output from the Perl module output. # The default value is: NO. @@ -2335,13 +2528,13 @@ PERLMOD_MAKEVAR_PREFIX = # Configuration options related to the preprocessor #--------------------------------------------------------------------------- -# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all +# If the ENABLE_PREPROCESSING tag is set to YES, Doxygen will evaluate all # C-preprocessor directives found in the sources and include files. # The default value is: YES. ENABLE_PREPROCESSING = YES -# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names +# If the MACRO_EXPANSION tag is set to YES, Doxygen will expand all macro names # in the source code. If set to NO, only conditional compilation will be # performed. Macro expansion can be done in a controlled way by setting # EXPAND_ONLY_PREDEF to YES. @@ -2390,10 +2583,10 @@ INCLUDE_FILE_PATTERNS = # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. PREDEFINED = DOXYGEN \ - INK_ENABLE_STL \ - INK_ENABLE_CSTD \ - INK_ENABLE_UNREAL \ - UFUNCTION(...):= + INK_ENABLE_STL \ + INK_ENABLE_CSTD \ + INK_ENABLE_UNREAL \ + UFUNCTION(...):= # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The @@ -2404,7 +2597,7 @@ PREDEFINED = DOXYGEN \ EXPAND_AS_DEFINED = UFUNCTION -# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will +# If the SKIP_FUNCTION_MACROS tag is set to YES then Doxygen's preprocessor will # remove all references to function-like macros that are alone on a line, have # an all uppercase name, and do not end with a semicolon. Such function macros # are typically used for boiler-plate code, and will confuse the parser if not @@ -2428,12 +2621,12 @@ SKIP_FUNCTION_MACROS = YES # section "Linking to external documentation" for more information about the use # of tag files. # Note: Each tag file must have a unique name (where the name does NOT include -# the path). If a tag file is not located in the directory in which doxygen is +# the path). If a tag file is not located in the directory in which Doxygen is # run, you must also specify the path to the tagfile here. TAGFILES = -# When a file name is specified after GENERATE_TAGFILE, doxygen will create a +# When a file name is specified after GENERATE_TAGFILE, Doxygen will create a # tag file that is based on the input files it reads. See section "Linking to # external documentation" for more information about the usage of tag files. @@ -2470,7 +2663,7 @@ EXTERNAL_PAGES = YES HIDE_UNDOC_RELATIONS = YES -# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# If you set the HAVE_DOT tag to YES then Doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: # https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is @@ -2479,19 +2672,32 @@ HIDE_UNDOC_RELATIONS = YES HAVE_DOT = NO -# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed -# to run in parallel. When set to 0 doxygen will base this on the number of +# The DOT_NUM_THREADS specifies the number of dot invocations Doxygen is allowed +# to run in parallel. When set to 0 Doxygen will base this on the number of # processors available in the system. You can set it explicitly to a value # larger than 0 to get control over the balance between CPU load and processing # speed. -# Minimum value: 0, maximum value: 32, default value: 0. +# Minimum value: 0, maximum value: 512, default value: 0. # This tag requires that the tag HAVE_DOT is set to YES. DOT_NUM_THREADS = 0 +# The DOT_BATCH_SIZE specifies the number of dot graphs Doxygen is allowed to +# compile in a single invocation of dot. When set to 1 Doxygen will invoke dot +# for each graph separately, which can cause significant process creation +# overhead especially on systems with many CPU cores. Together with +# DOT_NUM_THREADS this setting can be used to optimise the dot processing speed +# for a particular system. Doxygen will try to give each thread a balanced batch +# of work. If the total number of graphs to process exceeds DOT_NUM_THREADS * +# DOT_BATCH_SIZE then additional batches will be created for dot to process. +# Minimum value: 1, maximum value: 1000, default value: 50. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_BATCH_SIZE = 50 + # DOT_COMMON_ATTR is common attributes for nodes, edges and labels of # subgraphs. When you want a differently looking font in the dot files that -# doxygen generates you can specify fontname, fontcolor and fontsize attributes. +# Doxygen generates you can specify fontname, fontcolor and fontsize attributes. # For details please see Node, # Edge and Graph Attributes specification You need to make sure dot is able # to find the font, which can be done by putting it in a standard location or by @@ -2525,20 +2731,24 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then Doxygen will # generate a graph for each documented class showing the direct and indirect # inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and # HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case # the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the # CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. # If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance -# relations will be shown as texts / links. +# relations will be shown as texts / links. Explicit enabling an inheritance +# graph or choosing a different representation for an inheritance graph of a +# specific class, can be accomplished by means of the command \inheritancegraph. +# Disabling an inheritance graph can be accomplished by means of the command +# \hideinheritancegraph. # Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. CLASS_GRAPH = YES -# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a +# If the COLLABORATION_GRAPH tag is set to YES then Doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the # class with other documented classes. Explicit enabling a collaboration graph, @@ -2550,7 +2760,7 @@ CLASS_GRAPH = YES COLLABORATION_GRAPH = YES -# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for +# If the GROUP_GRAPHS tag is set to YES then Doxygen will generate a graph for # groups, showing the direct groups dependencies. Explicit enabling a group # dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means # of the command \groupgraph. Disabling a directory graph can be accomplished by @@ -2561,7 +2771,7 @@ COLLABORATION_GRAPH = YES GROUP_GRAPHS = YES -# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and +# If the UML_LOOK tag is set to YES, Doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. # The default value is: NO. @@ -2582,10 +2792,19 @@ UML_LOOK = NO UML_LIMIT_NUM_FIELDS = 10 -# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and +# If the UML_LOOK tag is enabled, field labels are shown along the edge between +# two class nodes. If there are many fields and many nodes the graph may become +# too cluttered. The UML_MAX_EDGE_LABELS threshold limits the number of items to +# make the size more manageable. Set this to 0 for no limit. +# Minimum value: 0, maximum value: 100, default value: 10. +# This tag requires that the tag UML_LOOK is set to YES. + +UML_MAX_EDGE_LABELS = 10 + +# If the DOT_UML_DETAILS tag is set to NO, Doxygen will show attributes and # methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS -# tag is set to YES, doxygen will add type and arguments for attributes and -# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen +# tag is set to YES, Doxygen will add type and arguments for attributes and +# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, Doxygen # will not generate fields with class member information in the UML graphs. The # class diagrams will look similar to the default class diagrams but using UML # notation for the relationships. @@ -2597,8 +2816,8 @@ DOT_UML_DETAILS = NO # The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters # to display on a single line. If the actual line length exceeds this threshold -# significantly it will wrapped across multiple lines. Some heuristics are apply -# to avoid ugly line breaks. +# significantly it will be wrapped across multiple lines. Some heuristics are +# applied to avoid ugly line breaks. # Minimum value: 0, maximum value: 1000, default value: 17. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2613,7 +2832,7 @@ DOT_WRAP_THRESHOLD = 17 TEMPLATE_RELATIONS = NO # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to -# YES then doxygen will generate a graph for each documented file showing the +# YES then Doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented # files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, # can be accomplished by means of the command \includegraph. Disabling an @@ -2624,7 +2843,7 @@ TEMPLATE_RELATIONS = NO INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are -# set to YES then doxygen will generate a graph for each documented file showing +# set to YES then Doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented # files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set # to NO, can be accomplished by means of the command \includedbygraph. Disabling @@ -2635,7 +2854,7 @@ INCLUDE_GRAPH = YES INCLUDED_BY_GRAPH = YES -# If the CALL_GRAPH tag is set to YES then doxygen will generate a call +# If the CALL_GRAPH tag is set to YES then Doxygen will generate a call # dependency graph for every global function or class method. # # Note that enabling this option will significantly increase the time of a run. @@ -2647,7 +2866,7 @@ INCLUDED_BY_GRAPH = YES CALL_GRAPH = NO -# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller +# If the CALLER_GRAPH tag is set to YES then Doxygen will generate a caller # dependency graph for every global function or class method. # # Note that enabling this option will significantly increase the time of a run. @@ -2659,14 +2878,14 @@ CALL_GRAPH = NO CALLER_GRAPH = NO -# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical +# If the GRAPHICAL_HIERARCHY tag is set to YES then Doxygen will graphical # hierarchy of all classes instead of a textual one. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. GRAPHICAL_HIERARCHY = YES -# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the +# If the DIRECTORY_GRAPH tag is set to YES then Doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the # files in the directories. Explicit enabling a directory graph, when @@ -2689,24 +2908,29 @@ DIR_GRAPH_MAX_DEPTH = 1 # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: # https://www.graphviz.org/)). -# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order -# to make the SVG files visible in IE 9+ (other browsers do not have this -# requirement). +# +# Note the formats svg:cairo and svg:cairo:cairo cannot be used in combination +# with INTERACTIVE_SVG (the INTERACTIVE_SVG will be set to NO). # Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, -# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and -# png:gdiplus:gdiplus. +# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus, +# png:gdiplus:gdiplus, svg:cairo, svg:cairo:cairo, svg:svg, svg:svg:core, +# gif:cairo, gif:cairo:gd, gif:cairo:gdiplus, gif:gdiplus, gif:gdiplus:gdiplus, +# gif:gd, gif:gd:gd, jpg:cairo, jpg:cairo:gd, jpg:cairo:gdiplus, jpg:gd, +# jpg:gd:gd, jpg:gdiplus and jpg:gdiplus:gdiplus. # The default value is: png. # This tag requires that the tag HAVE_DOT is set to YES. DOT_IMAGE_FORMAT = png -# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to -# enable generation of interactive SVG images that allow zooming and panning. +# If DOT_IMAGE_FORMAT is set to svg or svg:svg or svg:svg:core, then this option +# can be set to YES to enable generation of interactive SVG images that allow +# zooming and panning. # # Note that this requires a modern browser other than Internet Explorer. Tested # and working are Firefox, Chrome, Safari, and Opera. -# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make -# the SVG files visible. Older versions of IE do not have SVG support. +# +# Note This option will be automatically disabled when DOT_IMAGE_FORMAT is set +# to svg:cairo or svg:cairo:cairo. # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2725,7 +2949,7 @@ DOT_PATH = DOTFILE_DIRS = -# You can include diagrams made with dia in doxygen documentation. Doxygen will +# You can include diagrams made with dia in Doxygen documentation. Doxygen will # then run dia to produce the diagram and insert it in the documentation. The # DIA_PATH tag allows you to specify the directory where the dia binary resides. # If left empty dia is assumed to be found in the default search path. @@ -2738,7 +2962,7 @@ DIA_PATH = DIAFILE_DIRS = -# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the +# When using PlantUML, the PLANTUML_JAR_PATH tag should be used to specify the # path where java can find the plantuml.jar file or to the filename of jar file # to be used. If left blank, it is assumed PlantUML is not used or called during # a preprocessing step. Doxygen will generate a warning when it encounters a @@ -2746,20 +2970,78 @@ DIAFILE_DIRS = PLANTUML_JAR_PATH = -# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a -# configuration file for plantuml. +# When using PlantUML, the PLANTUML_CFG_FILE tag can be used to specify a +# configuration file for PlantUML. PLANTUML_CFG_FILE = -# When using plantuml, the specified paths are searched for files specified by -# the !include statement in a plantuml block. +# When using PlantUML, the specified paths are searched for files specified by +# the !include statement in a PlantUML block. PLANTUML_INCLUDE_PATH = +# The PLANTUMLFILE_DIRS tag can be used to specify one or more directories that +# contain PlantUml files that are included in the documentation (see the +# \plantumlfile command). + +PLANTUMLFILE_DIRS = + +# When using Mermaid diagrams with CLI rendering, the MERMAID_PATH tag should be +# used to specify the directory where the mmdc (Mermaid CLI) executable can be +# found. If left blank, CLI-based rendering is disabled. For HTML output, +# client-side rendering via JavaScript is used by default and does not require +# mmdc. For LaTeX/PDF output, mmdc is required to pre-generate images. Doxygen +# will generate a warning when CLI rendering is needed but mmdc is not +# available. + +MERMAID_PATH = + +# When using Mermaid diagrams, the MERMAID_CONFIG_FILE tag can be used to +# specify a JSON configuration file for the Mermaid CLI tool (mmdc). This file +# can contain theme settings and other Mermaid configuration options. + +MERMAID_CONFIG_FILE = + +# The MERMAID_RENDER_MODE tag selects how Mermaid diagrams are rendered. +# Possible values are: AUTO (use client-side rendering for HTML and mmdc for +# LaTeX/PDF and other formats. If MERMAID_PATH is not set, non-HTML diagrams +# will produce a warning), CLI (use the mmdc tool to pre-generate images +# (requires Node.js and mermaid-js/mermaid-cli). Works for all output formats) +# and CLIENT_SIDE (embed mermaid.js in HTML output for client-side rendering. +# Does not require mmdc but only works for HTML output). +# The default value is: AUTO. + +MERMAID_RENDER_MODE = AUTO + +# The MERMAID_JS_URL tag specifies the URL to load mermaid.js from when using +# client-side rendering (MERMAID_RENDER_MODE is CLIENT_SIDE or AUTO). The +# default points to the latest Mermaid v11 release on the jsDelivr CDN. +# +# The default CDN URL requires internet access when viewing the generated +# documentation. For offline use, download mermaid.esm.min.mjs and set this to a +# relative path, or use MERMAID_RENDER_MODE=CLI to pre-generate images instead. +# Examples: +# - Latest v11 (default): +# 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs' +# - Pinned version: +# 'https://cdn.jsdelivr.net/npm/mermaid@11.3.0/dist/mermaid.esm.min.mjs' +# - Local copy: './mermaid.esm.min.mjs' (user must place file in HTML output +# directory) +# The default value is: +# https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs. + +MERMAID_JS_URL = https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs + +# The MERMAIDFILE_DIRS tag can be used to specify one or more directories that +# contain Mermaid files that are included in the documentation (see the +# \mermaidfile command). + +MERMAIDFILE_DIRS = + # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes # that will be shown in the graph. If the number of nodes in a graph becomes -# larger than this value, doxygen will truncate the graph, which is visualized -# by representing a node as a red box. Note that doxygen if the number of direct +# larger than this value, Doxygen will truncate the graph, which is visualized +# by representing a node as a red box. Note that if the number of direct # children of the root node in a graph is already larger than # DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that # the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. @@ -2780,26 +3062,17 @@ DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 -# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output -# files in one run (i.e. multiple -o and -T options on the command line). This -# makes dot run faster, but since only newer versions of dot (>1.8.10) support -# this, this feature is disabled by default. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_MULTI_TARGETS = NO - -# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page +# If the GENERATE_LEGEND tag is set to YES Doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated # graphs. -# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal +# Note: This tag requires that UML_LOOK isn't set, i.e. the Doxygen internal # graphical representation for inheritance and collaboration diagrams is used. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. GENERATE_LEGEND = YES -# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate +# If the DOT_CLEANUP tag is set to YES, Doxygen will remove the intermediate # files that are used to generate the various graphs. # # Note: This setting is not only used for dot files but also for msc temporary @@ -2808,11 +3081,11 @@ GENERATE_LEGEND = YES DOT_CLEANUP = YES -# You can define message sequence charts within doxygen comments using the \msc -# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# You can define message sequence charts within Doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then Doxygen will # use a built-in version of mscgen tool to produce the charts. Alternatively, # the MSCGEN_TOOL tag can also specify the name an external tool. For instance, -# specifying prog as the value, doxygen will call the tool as prog -T +# specifying prog as the value, Doxygen will call the tool as prog -T # -o . The external tool should support # output file formats "png", "eps", "svg", and "ismap". diff --git a/inkcpp/CMakeLists.txt b/inkcpp/CMakeLists.txt index 942a517e..37353c02 100644 --- a/inkcpp/CMakeLists.txt +++ b/inkcpp/CMakeLists.txt @@ -83,7 +83,7 @@ endforeach() foreach(file IN LISTS PUBLIC_HEADERS) get_filename_component(file "${file}" NAME) configure_file("include/${file}" - "${CMAKE_BINARY_DIR}/unreal/inkcpp/Source/inkcpp/Public/ink/${FILE}" COPYONLY) + "${CMAKE_BINARY_DIR}/unreal/inkcpp/Source/inkcpp/Public/ink/${file}" COPYONLY) endforeach() foreach(file IN LISTS COLLECTION_SOURCES) configure_file("${file}" "${CMAKE_BINARY_DIR}/unreal/inkcpp/Source/inkcpp/Private/ink/${file}" diff --git a/inkcpp/array.h b/inkcpp/array.h index 5eba1c88..5d02dac2 100644 --- a/inkcpp/array.h +++ b/inkcpp/array.h @@ -346,13 +346,16 @@ 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&); + bool can_be_migrated() const; + size_t snap(unsigned char* data, const snapper&) const; + virtual const unsigned char* snap_load(const unsigned char* data, const loader&) = 0; protected: inline T* buffer() { return _array; } + const unsigned char* impl_snap_load_meta(const unsigned char* data); + const unsigned char* impl_snap_load_payload(const unsigned char* data); + void set_new_buffer(T* buffer, size_t capacity) { _array = buffer; @@ -455,6 +458,8 @@ inline void basic_restorable_array::forget() // Clear _temp[i] = _null; } + + _saved = false; } template @@ -480,6 +485,8 @@ inline void basic_restorable_array::clear(const T& value) template class fixed_restorable_array : public basic_restorable_array { + using base = basic_restorable_array; + public: fixed_restorable_array(const T& initial, const T& nullValue) : basic_restorable_array(_buffer, SIZE * 2, nullValue) @@ -487,6 +494,9 @@ class fixed_restorable_array : public basic_restorable_array basic_restorable_array::clear(initial); } + const unsigned char* + snap_load(const unsigned char* data, const snapshot_interface::loader&) override; + private: T _buffer[SIZE * 2]; }; @@ -519,7 +529,7 @@ class allocated_restorable_array : public basic_restorable_array size_t new_capacity = 2 * n; T* new_buffer = new T[new_capacity]; if (_buffer) { - for (size_t i = 0; i < base::capacity(); ++i) { + for (size_t i = 0; i < base::capacity() && i < n; ++i) { new_buffer[i] = _buffer[i]; // copy temp new_buffer[i + n] = _buffer[i + base::capacity()]; @@ -543,6 +553,9 @@ class allocated_restorable_array : public basic_restorable_array } } + const unsigned char* + snap_load(const unsigned char* data, const snapshot_interface::loader&) override; + private: T _initialValue; T _nullValue; @@ -571,26 +584,55 @@ inline size_t basic_restorable_array::snap(unsigned char* data, const snapper } template -inline const unsigned char* - basic_restorable_array::snap_load(const unsigned char* data, const loader&) +inline const unsigned char* basic_restorable_array::impl_snap_load_meta(const unsigned char* data +) { auto ptr = data; ptr = snap_read(ptr, _saved); ptr = snap_read(ptr, _loaded_capacity); - if (buffer() == nullptr) { - static_cast&>(*this).resize(_loaded_capacity); - } - inkAssert( - _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 < _loaded_capacity; ++i) { + return ptr; +} + +template +inline const unsigned char* + basic_restorable_array::impl_snap_load_payload(const unsigned char* data) +{ + inkAssert( + capacity() >= loaded_capacity(), + "New config does not allow for necessary size used by this snapshot!" + ); + auto ptr = data; + for (size_t i = 0; i < loaded_capacity(); ++i) { ptr = snap_read(ptr, _array[i]); ptr = snap_read(ptr, _temp[i]); } return ptr; } + +template +inline const unsigned char* fixed_restorable_array< + T, SIZE>::snap_load(const unsigned char* data, const snapshot_interface::loader&) +{ + auto ptr = data; + ptr = base::impl_snap_load_meta(ptr); + ptr = base::impl_snap_load_payload(ptr); + return ptr; +} + +template +inline const unsigned char* allocated_restorable_array< + T>::snap_load(const unsigned char* data, const snapshot_interface::loader&) +{ + auto ptr = data; + ptr = base::impl_snap_load_meta(ptr); + if (base::buffer() == nullptr || base::capacity() < base::loaded_capacity()) { + resize(base::loaded_capacity()); + } + ptr = base::impl_snap_load_payload(ptr); + return ptr; +} + } // namespace ink::runtime::internal diff --git a/inkcpp/collections/restorable.h b/inkcpp/collections/restorable.h index 9dda466a..358578e5 100644 --- a/inkcpp/collections/restorable.h +++ b/inkcpp/collections/restorable.h @@ -228,7 +228,7 @@ class restorable : public snapshot_interface // Forward iterate template - void for_each(CallbackMethod callback, IsNullPredicate isNull) const + void for_each(CallbackMethod callback, IsNullPredicate isNull) { if (_pos == 0) { return; diff --git a/inkcpp/container_operations.cpp b/inkcpp/container_operations.cpp index 6b7aa6d5..e632e4c6 100644 --- a/inkcpp/container_operations.cpp +++ b/inkcpp/container_operations.cpp @@ -40,6 +40,7 @@ void operation::operator()( basic_eval_stack& stack, value* vals ) { + ( void ) vals; stack.push(value{}.set(static_cast(_runner.num_choices()))); } diff --git a/inkcpp/globals_impl.cpp b/inkcpp/globals_impl.cpp index 45efa0aa..40b7eedf 100644 --- a/inkcpp/globals_impl.cpp +++ b/inkcpp/globals_impl.cpp @@ -26,33 +26,17 @@ globals_impl::globals_impl(const story_impl* story) _visit_counts.resize(_num_containers); if (_lists) { // initialize static lists - init_static_list_flags(); + _lists.init_static_list_flags(_owner->lists(), _variables); } } -void globals_impl::init_static_list_flags() +void globals_impl::visit(uint32_t container_id, bool preserve_turns) { - const list_flag* flags = _owner->lists(); - 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; - } - ++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}) - ); - } -} - -void globals_impl::visit(uint32_t container_id) -{ - _visit_counts.set(container_id, {_visit_counts[container_id].visits + 1, 0}); + const int32_t existing_turns = _visit_counts[container_id].turns; + _visit_counts.set( + container_id, {_visit_counts[container_id].visits + (preserve_turns ? 0 : 1), + preserve_turns ? existing_turns : 0} + ); } uint32_t globals_impl::visits(uint32_t container_id) const @@ -294,6 +278,9 @@ const unsigned char* globals_impl::snap_load(const unsigned char* ptr, const loa _visit_counts.resize(_owner->num_containers()); } _visit_counts.save(); + for (size_t i = 0; i < old_capacity; ++i) { + _visit_counts.set(i, visit_count()); + } } inkAssert( @@ -302,8 +289,8 @@ const unsigned char* globals_impl::snap_load(const unsigned char* ptr, const loa ); for (size_t i = 0; i < old_capacity; ++i) { hash_t path; - ptr = snap_read(ptr, path); - container_t c_id; + ptr = snap_read(ptr, path); + container_t c_id = ~0U; ip_t container_ip = _owner->find_offset_for(path); bool found = container_ip != nullptr && _owner->find_container_id( @@ -320,6 +307,7 @@ const unsigned char* globals_impl::snap_load(const unsigned char* ptr, const loa } if (loader.migratable) { _visit_counts.forget(); + _visit_counts.resize(_num_containers); } inkAssert( _num_containers == _visit_counts.capacity(), @@ -331,15 +319,28 @@ 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 globals_impl::migrate_new_globals( + const loader& loader, globals_impl& new_globals, const char* list_metadata +) { - bool success - = _variables.migrate(new_globals._variables) && ((! _lists) || _lists.migrate(list_metadata)); - if (! success) { + if (! _variables.migrate(new_globals._variables)) { return false; } + if (_lists && ! loader.old_ref_table) { + if (! _lists.create_match_lut( + list_metadata, loader.list_list_matches, loader.list_value_matches, loader.old_ref_table + )) { + return false; + } + } if (_lists) { - init_static_list_flags(); + _lists.init_static_list_flags(_owner->lists(), _variables); + if (! _lists.migrate_variables( + loader.list_old_new_map, loader.list_list_matches, loader.list_value_matches, + *loader.old_ref_table, _variables + )) { + return false; + } } return true; } diff --git a/inkcpp/globals_impl.h b/inkcpp/globals_impl.h index d4dc1fe6..854a3ec8 100644 --- a/inkcpp/globals_impl.h +++ b/inkcpp/globals_impl.h @@ -30,8 +30,6 @@ 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&); @@ -45,7 +43,9 @@ class globals_impl final * @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); + bool migrate_new_globals( + const loader& loader, globals_impl& new_globals, const char* list_metadata + ); // Initializes a new global store from the given story globals_impl(const story_impl*); @@ -64,8 +64,10 @@ class globals_impl final void internal_observe(hash_t name, internal::callback_base* callback) override; public: - // Records a visit to a container - void visit(uint32_t container_id); + // Records a visit to a container. + // If preserve_turns is true the existing turns-since counter is kept intact + // (used during snapshot migration to avoid clobbering the restored value). + void visit(uint32_t container_id, bool preserve_turns = false); // Checks the number of visits to a container uint32_t visits(uint32_t container_id) const; diff --git a/inkcpp/header.cpp b/inkcpp/header.cpp index e92ee2a1..03f08226 100644 --- a/inkcpp/header.cpp +++ b/inkcpp/header.cpp @@ -10,7 +10,7 @@ namespace ink::internal { -bool header::verify() const +bool header::validate() const { if (endian() == endian_types::none) { inkFail("Header magic number was wrong!"); diff --git a/inkcpp/include/functional.h b/inkcpp/include/functional.h index 11b577a4..db354121 100644 --- a/inkcpp/include/functional.h +++ b/inkcpp/include/functional.h @@ -204,8 +204,9 @@ class function : public function_base if constexpr (traits::arity == 2) { return is_same< const ink::runtime::value*, typename traits::template argument<1>::type>::value; + } else { + return false; } - return false; } template diff --git a/inkcpp/include/list.h b/inkcpp/include/list.h index 5f7cf42c..45b93c2f 100644 --- a/inkcpp/include/list.h +++ b/inkcpp/include/list.h @@ -48,6 +48,7 @@ class list_interface { } + /** copy assigment operator. */ virtual list_interface& operator=(const list_interface&) = default; virtual ~list_interface() {} @@ -81,6 +82,9 @@ class list_interface } public: + /** copy constructor. */ + iterator(const iterator&) = default; + /** contains flag data */ struct Flag { const char* flag_name; ///< name of the flag @@ -127,9 +131,8 @@ class list_interface # pragma GCC diagnostic ignored "-Wunused-parameter" #else # pragma warning(push) -# pragma warning( \ - disable : 4100, justification : "non functional prototypes do not need the argument." \ - ) +// non functional prototypes do not need the argument. +# pragma warning(disable : 4100) #endif /** checks if a flag is contained in the list */ @@ -203,6 +206,7 @@ class list_interface /** @private */ internal::list_table* _list_table; + /** @private */ int _list; }; diff --git a/inkcpp/include/runner.h b/inkcpp/include/runner.h index c48a8797..750c3227 100644 --- a/inkcpp/include/runner.h +++ b/inkcpp/include/runner.h @@ -38,7 +38,7 @@ class runner_interface public: virtual ~runner_interface(){}; - // String type to simplify interfaces working with strings +/** String type to simplify interfaces working with strings */ #ifdef INK_ENABLE_STL using line_type = std::string; #elif defined(INK_ENABLE_UNREAL) @@ -54,16 +54,29 @@ class runner_interface virtual void set_rng_seed(uint32_t seed) = 0; /** - * Moves the runner to the specified path + * Moves the runner to the specified path. + * + * @sa move_to(const char*) for more conviance * * Clears any execution context and moves the runner - * to the content at the specified path. + * to the content at the specified path. * * @param path path to search and move execution to * @return If the path was found */ virtual bool move_to(hash_t path) = 0; + /** + * Moves the runner to the specified path. + * + * Clears any execution context and moves the runner + * to the content at the specified path. + * + * @param path path to search and move execution to + * @return If the path was found + */ + bool move_to(const char* path) { return move_to(ink::hash_string(path)); } + /** * Can the runner continue? * @@ -198,14 +211,14 @@ class runner_interface * Check if the there are global tags. * * @return ture if there are global tags. - * @info global tags are also assoziated to the first line in the knot/stitch + * @note global tags are also assoziated to the first line in the knot/stitch * @sa num_global_tags get_global_tags has_tags has_knot_tags */ virtual bool has_global_tags() const = 0; /** * Get Number of global tags. - * @info global tags are also assoziated to the first line in the knot/stitch + * @note global tags are also assoziated to the first line in the knot/stitch * @sa has_global_tags get_global_tags num_knot_tags num_tags * @return the number of tags at the top of the document. */ @@ -228,7 +241,7 @@ class runner_interface /** * Check if there are knot/stitch tags. * - * @info knot/stitch tags are also assoziated to the first line in the knot/stitch + * @note knot/stitch tags are also assoziated to the first line in the knot/stitch * @return true if there are knot/stitch tags. * @sa num_knot_tags get_knot_tag has_global_tags has_tags */ @@ -236,7 +249,7 @@ class runner_interface /** * Get Number of knot/stitch tags. - * @info knot/stitch tags are also assoziated to the first line in the knot/stitch + * @note knot/stitch tags are also assoziated to the first line in the knot/stitch * @return number of tags at the top of a knot/stitch * @sa has_knot_tags get_knot_tag num_global_tags num_tags */ diff --git a/inkcpp/include/snapshot.h b/inkcpp/include/snapshot.h index 5b8879f9..4181031c 100644 --- a/inkcpp/include/snapshot.h +++ b/inkcpp/include/snapshot.h @@ -11,7 +11,7 @@ namespace ink::runtime { /** - * Container for an InkCPP runtime snapshot. + * Container for an InkCPP runtime snapshot, which can be @ref snapshot_migration "migrated". * Each snapshot contains a @ref ink::runtime::globals_interface "globals store" * and all associated @ref ink::runtime::runner_interface "runners/threads" * For convinience there exist @ref ink::runtime::globals_interface::create_snapshot() and @@ -23,7 +23,10 @@ namespace ink::runtime * 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.** + * @section snapshot_migration Migration + * + * Migrating a snapshot will "snap bag" to the last Knot, there for it is best practive to do this + * only directly after choosing a choice. * * + Global variables which (name) still exist will be transfared. * + New ones will be initelized with its default value @@ -73,7 +76,8 @@ class snapshot virtual size_t get_data_len() const = 0; /** number of runners which are stored inside this snapshot */ virtual size_t num_runners() const = 0; - /** if this snapshot can be migrated, if the story file changes (slightly). */ + /** if this snapshot can be migrated, if the story file changes (slightly), for details see @ref + * snapshot_migration. */ virtual bool can_be_migrated() const = 0; #ifdef INK_ENABLE_STL diff --git a/inkcpp/include/story.h b/inkcpp/include/story.h index bc3fba71..387cfb18 100644 --- a/inkcpp/include/story.h +++ b/inkcpp/include/story.h @@ -108,6 +108,7 @@ class story } // namespace ink::runtime /** @namespace ink + * @ingroup cpp * Namespace contaning all modules and classes from InkCPP * * (Unreal Blueprint Classes Excluded, but there will not be there in a normal build) diff --git a/inkcpp/include/types.h b/inkcpp/include/types.h index c771cb1a..e6cca05c 100644 --- a/inkcpp/include/types.h +++ b/inkcpp/include/types.h @@ -140,45 +140,51 @@ struct value { } }; -/** access #value::Type::Bool value */ +/** access a @ref ink::runtime::value::Type::Bool value */ template<> inline const auto& value::get() const { + inkAssert(type == value::Type::Bool, "Expected type bool, if a boolean should be readed"); return v_bool; } -/** access #value::Type::Uint32 value */ +/** access a @ref ink::runtime::value::Type::Uint32 value */ template<> inline const auto& value::get() const { + inkAssert(type == value::Type::Uint32, "Expected type uint32, if a boolean should be readed"); return v_uint32; } -/** access #value::Type::Int32 value */ +/** access @ref ink::runtime::value::Type::Int32 value */ template<> inline const auto& value::get() const { + inkAssert(type == value::Type::Int32, "Expected type int32, if a boolean should be readed"); return v_int32; } -/** access #value::Type::String value */ +/** access @ref ink::runtime::value::Type::String value */ template<> inline const auto& value::get() const { + inkAssert(type == value::Type::String, "Expected type string, if a boolean should be readed"); return v_string; } -/** access #value::Type::Float value */ +/** access @ref ink::runtime::value::Type::Float value */ template<> inline const auto& value::get() const { + inkAssert(type == value::Type::Float, "Expected type float, if a boolean should be readed"); return v_float; } -/** access #value::Type::List value */ +/** access @ref ink::runtime::value::Type::List value */ template<> inline const auto& value::get() const { + inkAssert(type == value::Type::List, "Expected type list, if a boolean should be readed"); return v_list; } } // namespace ink::runtime diff --git a/inkcpp/list_impl.h b/inkcpp/list_impl.h index 1aefb2be..0a5b9042 100644 --- a/inkcpp/list_impl.h +++ b/inkcpp/list_impl.h @@ -21,6 +21,11 @@ class list_impl final : public list_interface { } + list_interface& operator=(const list_interface& oth) noexcept override + { + return *this = static_cast(oth); + } + list_impl& operator=(const list_impl&) = default; ~list_impl() override {} diff --git a/inkcpp/list_table.cpp b/inkcpp/list_table.cpp index e32dea84..d4236983 100644 --- a/inkcpp/list_table.cpp +++ b/inkcpp/list_table.cpp @@ -5,6 +5,7 @@ * https://github.com/JBenda/inkcpp for full license details. */ #include "list_table.h" +#include "array.h" #include "config.h" #include "hungarian_solver.h" #include "system.h" @@ -13,6 +14,7 @@ #include "random.h" #include "string_utils.h" #include "list_impl.h" +#include "stack.h" #include #ifdef INK_ENABLE_STL @@ -34,6 +36,13 @@ void list_table::copy_lists(const data_t* src, data_t* dst) } } +inline list_flag read_list_flag(const char*& ptr) +{ + list_flag result = *reinterpret_cast(ptr); + ptr += sizeof(list_flag); + return result; +} + list_table::list_table(const char* data) : _valid{false} { @@ -71,6 +80,10 @@ list_table::list list_table::create() for (size_t i = 0; i < _entry_state.size(); ++i) { if (_entry_state[i] == state::empty) { _entry_state[i] = state::used; + memset( + _data.begin() + static_cast(_entrySize) * static_cast(i), 0, + _entrySize + ); return list(i); } } @@ -89,16 +102,24 @@ 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; + memset( + _data.begin() + static_cast(_entrySize) * static_cast(idx), 0, + _entrySize + ); return list(idx); } return list(-1); } - while (_entry_state.size() <= idx) { + while (_entry_state.size() < idx) { _entry_state.push() = state::empty; for (int i = 0; i < _entrySize; ++i) { _data.push() = 0; } } + _entry_state.push() = state::used; + for (int i = 0; i < _entrySize; ++i) { + _data.push() = 0; + } return list(idx); } @@ -905,7 +926,7 @@ float d_contains(const size_t lh[2], const size_t rh[2], const int* matches) } } n_union -= n_intersection; - return static_cast(n_intersection) / n_union; + return 1.f - static_cast(n_intersection) / n_union; } /** Distance function for string labels. @@ -1013,14 +1034,20 @@ float* cost_matrix(const MatchListValues& lh, const MatchListValues& rh, float d return matrix; } -bool list_table::migrate(const char* old_list_metadata) +bool list_table::create_match_lut( + const char* old_list_metadata, + ink::runtime::internal::managed_array& list_list_matches, + ink::runtime::internal::managed_array& list_value_matches, + const list_table*& old_ref_table +) { - list_table old_ref_table(old_list_metadata); + list_table* ref_table = new list_table(old_list_metadata); + old_ref_table = ref_table; for (const auto& x : _data) { - old_ref_table._data.push() = x; + ref_table->_data.push() = x; } for (const auto& x : _entry_state) { - old_ref_table._entry_state.push() = x; + ref_table->_entry_state.push() = x; } _data.clear(); _entry_state.clear(); @@ -1044,88 +1071,137 @@ bool list_table::migrate(const char* old_list_metadata) 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() + ref_table->_flag_names.data(), ref_table->_flag_values.data(), 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); + const int n_flags = std::max(numFlags(), ref_table->numFlags()); + list_value_matches.resize(n_flags); + algorithms::hungarian_solver( + value_matrix, list_value_matches.data(), 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, + MatchList{ref_table->_list_end.data(), ref_table->_list_names.data(), ref_table->numLists()}, + MatchList{_list_end.data(), _list_names.data(), numLists()}, list_value_matches.data(), 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); + const int n_lists = std::max(numLists(), ref_table->numLists()); + list_list_matches.resize(n_lists); + algorithms::hungarian_solver( + list_matrix, list_list_matches.data(), 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); - } + algorithms::hungarian_solver( + value_matrix, list_value_matches.data(), n_flags, LOW_CONFIDANCE_DROP_PANELTY + ); - delete[] list_matches; - delete[] value_matches; delete[] value_matrix; delete[] list_matrix; return true; } +bool list_table::migrate_variables( + ink::runtime::internal::managed_array& list_old_new_map, + const ink::runtime::internal::managed_array& list_list_matches, + const ink::runtime::internal::managed_array& list_value_matches, + const list_table& old_ref_table, basic_stack& variables +) +{ + // TODO: optimize: map equal permanent values (old list x -> new list x) + bool migration_succeeded = true; + variables.for_each( + [&](entry& value) { + list old_list = value.data.get(); + size_t idx = old_list.lid; + while (list_old_new_map.size() <= idx) { + list_old_new_map.push() = -1; + } + if (list_old_new_map[idx] != -1) { + value.data.set(list(list_old_new_map[idx])); + return; + } + // migrate + list new_list{-1}; + switch (old_ref_table._entry_state[idx]) { + case state::permanent: new_list = create_permament(); break; + case state::used: new_list = create(); break; + default: return; + } + list_old_new_map[idx] = new_list.lid; + value.data.set(new_list); + + inkAssert(new_list.lid >= 0, "Failed to create new list entry for migration."); + const data_t* entry = old_ref_table.getPtr(idx); + data_t* new_entry = getPtr(new_list.lid); + 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]) { + migrated = false; + if (list_value_matches[j] != -1) { + hit = true; + migrated = true; + size_t k; + for (k = 0; _list_end[k] <= static_cast(list_value_matches[j]); ++k) {} + setList(new_entry, k); + setFlag(new_entry, list_value_matches[j]); + } + } + } + // keep list if list has match but all values where dropped + if (! hit && list_list_matches[i] != -1) { + setList(new_entry, list_list_matches[i]); + migrated = true; + } + } + } + // drop list + if (! is_empty_list && ! migrated) { + // FIXME: remove list ? + // _entry_state [idx] = state::empty; + migration_succeeded = false; + } + // inkAssert(migrated, "Migrating list @%d would lead to an empty list", idx); + }, + [](const entry& v) { return v.data.type() != value_type::list; } + ); + return migration_succeeded; +} + +void list_table::impl_init_static_list(const list_flag* permanent_lists) +{ + const list_flag* flags = permanent_lists; + while (*flags != null_flag) { + list_table::list l = create_permament(); + while (*flags != null_flag) { + list_flag flag = external_fvalue_to_internal(*flags); + add_inplace(l, flag); + ++flags; + } + ++flags; + } +} + +void list_table::init_static_list_flags(const list_flag* permanent_lists, basic_stack& variables) +{ + impl_init_static_list(permanent_lists); + + for (const auto& flag : named_flags()) { + variables.set( + hash_string(flag.name), + value{}.set(list_flag{flag.flag.list_id, flag.flag.flag}) + ); + } +} + } // namespace ink::runtime::internal diff --git a/inkcpp/list_table.h b/inkcpp/list_table.h index a30a82ab..62a8715e 100644 --- a/inkcpp/list_table.h +++ b/inkcpp/list_table.h @@ -9,7 +9,7 @@ #include "config.h" #include "system.h" #include "array.h" -#include "snapshot_impl.h" +#include "list.h" #ifdef INK_ENABLE_STL # include @@ -22,6 +22,7 @@ struct header; namespace ink::runtime::internal { +class basic_stack; class prng; // TODO: move to utils @@ -114,8 +115,20 @@ class list_table : public snapshot_interface list& add_inplace(list& lh, list_flag rh); list_table(const char* data); + void init_static_list_flags(const list_flag* permanent_lists, basic_stack& variables); // binary list metadata of currently loaded list - bool migrate(const char* old_list_metadata); + bool create_match_lut( + const char* old_list_metadata, + ink::runtime::internal::managed_array& list_list_matches, + ink::runtime::internal::managed_array& list_value_matches, + const list_table*& old_ref_table + ); + bool migrate_variables( + ink::runtime::internal::managed_array& list_old_new_map, + const ink::runtime::internal::managed_array& list_list_matches, + const ink::runtime::internal::managed_array& list_value_matches, + const list_table& old_ref_table, basic_stack& variables + ); explicit list_table() : _entrySize{0} @@ -148,7 +161,7 @@ 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&); - bool can_be_migrated() const { return _list_handouts.size() == 0; } + bool can_be_migrated() const { return true; } /** 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 @@ -266,6 +279,9 @@ class list_table : public snapshot_interface list_interface* handout_list(list); private: + /** initelizes static lists defined in the story. */ + void impl_init_static_list(const list_flag* permanent_lists); + /** create a list with id == idx. * @attention used for migration only * @sa create() diff --git a/inkcpp/runner_impl.cpp b/inkcpp/runner_impl.cpp index 91b14919..bb3bec2f 100644 --- a/inkcpp/runner_impl.cpp +++ b/inkcpp/runner_impl.cpp @@ -13,6 +13,7 @@ #include "snapshot_impl.h" #include "story_impl.h" #include "system.h" +#include "types.h" #include "value.h" #ifdef INK_ENABLE_STL @@ -169,8 +170,7 @@ void runner_impl::set_var( template inline T runner_impl::read(optional pos) { - using header = ink::internal::header; - ip_t ptr = pos.value_or(_ptr); + ip_t ptr = pos.value_or(_ptr); // Sanity inkAssert(ptr + sizeof(T) <= _story->end(), "Unexpected EOF in Ink execution"); @@ -312,12 +312,14 @@ void runner_impl::fetch_tags(ip_t begin) iter += 6; continue; } - add_tag(read(iter + 6 + 2), tags_level::UNKNOWN); + // store tags in dynamic data, too keep migratable stories on the table + // TODO: maybe let tags live on the static data again. + add_tag(_globals->strings().duplicate(read(iter + 6 + 2)), tags_level::UNKNOWN); iter += 18; } } -void runner_impl::jump(ip_t dest, bool record_visits, bool track_knot_visit) +void runner_impl::jump(ip_t dest, bool record_visits, bool track_knot_visit, bool preserve_turns) { // Optimization: if we are _is_falling, then we can // _should be_ able to safely assume that there is nothing to do here. A falling @@ -340,11 +342,11 @@ void runner_impl::jump(ip_t dest, bool record_visits, bool track_knot_visit) _ptr = dest; // Find the container at or before dest, which will become the top of the post-jump stack. - const uint32_t dest_offset = dest - _story->instructions(); + const uint32_t dest_offset = static_cast(dest - _story->instructions()); const container_t dest_id = _story->find_container_for(dest_offset); // If there's no destination container, stop. - if (dest_id == ~0) + if (dest_id == ~0U) return; // Are we entering the new container at its start? @@ -353,7 +355,7 @@ void runner_impl::jump(ip_t dest, bool record_visits, bool track_knot_visit) if (dest_offset == dest_container._start_offset) { // Record direct jump to non-knot if requested. (Knots handled below.) if (record_visits && ! dest_container.knot()) { - _globals->visit(dest_id); + _globals->visit(dest_id, preserve_turns); } // Consume instruction so we don't process it again during normal flow. (We need to do this here @@ -386,7 +388,7 @@ void runner_impl::jump(ip_t dest, bool record_visits, bool track_knot_visit) // Ink has a rule about incrementing visit counts when you jump to the top of a knot, which // seems to need to override inkcpp's knot_visit flag. if (track_knot_visit || container._start_offset == dest_offset) { - _globals->visit(id); + _globals->visit(id, preserve_turns); } // If tracking, update with the first knot we encounter, which is the one closest to the top @@ -484,14 +486,12 @@ runner_impl::runner_impl(const story_impl* data, globals global) , _evaluation_mode{false} , _choices() , _tags_begin(0, ~0) - , _container(~0) -#ifdef INK_ENABLE_CSTD - , _rng(static_cast(time(NULL))) -#else - , _rng() -#endif + , _container(~0U) { +#ifdef INK_ENABLE_CSTD + _rng.srand(static_cast(time(NULL))); +#endif // register with globals _globals->add_runner(this); @@ -591,10 +591,10 @@ void runner_impl::advance_line() if (_saved) { restore(); } - _globals->gc(); if (_output.saved()) { _output.restore(); } + _globals->gc(); } bool runner_impl::can_continue() const { return _ptr != nullptr && ! has_choices(); } @@ -657,12 +657,15 @@ bool runner_impl::can_be_migrated() const if (_entered_knot) { return false; } - container_t container_id - = _ptr != nullptr && _ptr >= _story->instructions() + 6 - ? _story->find_container_for(static_cast(_ptr - _story->instructions() - 6)) - : ~0U; - hash_t c_hash = (container_id != ~0U) ? _story->container_data(container_id)._hash : 0; - if (c_hash == 0) { + hash_t c_hash = 0; + if (_ptr != nullptr && _ptr >= _story->instructions() + 6) { + // Use find_migration_hash so that named-but-untracked containers (e.g. unlabeled choice + // bodies c-0, c-1 that are in _container_hash but not _container_map) are found correctly. + c_hash = _story->find_migration_hash(static_cast(_ptr - _story->instructions() - 6)); + } + // if we are not at the start or terminal but we cannot name the current position it is not + // migratable + if (c_hash == 0 && _ptr != nullptr && _ptr != _story->instructions()) { return false; } return _output.can_be_migrated() && _stack.can_be_migrated() && _ref_stack.can_be_migrated() @@ -678,12 +681,15 @@ size_t runner_impl::snap(unsigned char* data, snapper& snapper) const // This first field stores the hash of the container at the current position, // used by migration (story_impl::new_runner_from_snapshot) to navigate to the correct location. { - container_t container_id - = (_ptr != nullptr && _ptr >= _story->instructions() + 6) - ? _story->find_container_for(static_cast(_ptr - _story->instructions() - 6)) - : ~0U; - hash_t container_hash = (container_id != ~0U) ? _story->container_data(container_id)._hash : 0; - ptr = snap_write(ptr, container_hash, should_write); + hash_t container_hash = 0; + if (_ptr != nullptr && _ptr >= _story->instructions() + 6) { + // Use find_migration_hash so that named-but-untracked containers (e.g. unlabeled + // choice bodies c-0, c-1) that live in _container_hash but not _container_map are + // found via their exact start-offset, not approximated via their tracked parent. + container_hash + = _story->find_migration_hash(static_cast(_ptr - _story->instructions() - 6)); + } + ptr = snap_write(ptr, container_hash, should_write); } ptr = snap_write(ptr, offset, should_write); offset = _backup != nullptr ? _backup - _story->instructions() : 0; @@ -814,7 +820,7 @@ bool runner_impl::move_to(hash_t path) return true; } -bool runner_impl::migrate_to(hash_t path) +bool runner_impl::migrate_to(const loader& loader, hash_t path) { ip_t destination = _story->find_offset_for(path); if (destination == nullptr) { @@ -849,10 +855,20 @@ bool runner_impl::migrate_to(hash_t path) } } } - // rebuild container stack to display new offsets + // rebuild container stack using new story offsets. + // preserve_turns=true keeps the turns-since counters restored from the snapshot intact; + // without this the visit() call inside jump() would reset them to 0. _container.clear(); _ptr = nullptr; - jump(destination, false, true); + jump(destination, false, true, true); + + if (loader.old_ref_table + && ! _globals->lists().migrate_variables( + loader.list_old_new_map, loader.list_list_matches, loader.list_value_matches, + *loader.old_ref_table, _stack + )) { + return false; + } return true; } @@ -981,9 +997,8 @@ bool runner_impl::line_step() void runner_impl::step() { #ifdef INK_ENABLE_EXCEPTIONS - try + try { #endif - { inkAssert(_ptr != nullptr, "Can not step! Do not have a valid pointer"); // Load current command @@ -1638,6 +1653,7 @@ void runner_impl::step() ))); } break; case Command::TAG: { + inkFail("Command::TAG is Deprecated!"); read(); add_tag(read(), tags_level::UNKNOWN); } break; @@ -1650,9 +1666,8 @@ void runner_impl::step() *_debug_stream << std::endl; } #endif - } #ifdef INK_ENABLE_EXCEPTIONS - catch (...) { + } catch (...) { // Reset our whole state as it's probably corrupt reset(); throw; diff --git a/inkcpp/runner_impl.h b/inkcpp/runner_impl.h index 74ca4e9c..04660116 100644 --- a/inkcpp/runner_impl.h +++ b/inkcpp/runner_impl.h @@ -132,7 +132,7 @@ class runner_impl virtual bool move_to(hash_t path) override; // move to path but keep as much state as possible - bool migrate_to(hash_t path); + bool migrate_to(const loader& loader, hash_t path); #if defined(INK_ENABLE_STL) || defined(INK_ENABLE_UNREAL) // Gets a single line of output @@ -215,8 +215,10 @@ class runner_impl // 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); + // Special code for jumping from the current IP to another. + // preserve_turns: if true, existing turns-since counters on visited knots are not reset + // (used during snapshot migration to keep the restored turn values intact). + void jump(ip_t, bool record_visits, bool track_knot_visit, bool preserve_turns = false); uint32_t _current_knot_id = ~0U; // id to detect knot changes from the outside uint32_t _current_knot_id_backup = ~0U; uint32_t _entered_knot = false; // if we are in the first action after a jump to an snitch/knot diff --git a/inkcpp/snapshot_impl.cpp b/inkcpp/snapshot_impl.cpp index 7c6f2a3a..dd3b9061 100644 --- a/inkcpp/snapshot_impl.cpp +++ b/inkcpp/snapshot_impl.cpp @@ -52,6 +52,16 @@ void snapshot::write_to_file(const char* filename) const namespace ink::runtime::internal { +snapshot_impl::~snapshot_impl() +{ + if (_managed) { + delete[] _file; + } + if (old_ref_table) { + delete old_ref_table; + } +}; + size_t snapshot_impl::file_size(size_t serialization_length, size_t runner_cnt, bool list_definition) { diff --git a/inkcpp/snapshot_impl.h b/inkcpp/snapshot_impl.h index 2c684df2..8dfc7bd6 100644 --- a/inkcpp/snapshot_impl.h +++ b/inkcpp/snapshot_impl.h @@ -75,15 +75,16 @@ static_assert(sizeof(snap_tag) == sizeof(const char*)); class snapshot_impl final : public snapshot { public: - ~snapshot_impl() override - { - if (_managed) { - delete[] _file; - } - }; + ~snapshot_impl() override; managed_array& strings() const { return string_table; } + managed_array& list_old_new_map() const { return list_old_new_map_table; } + + managed_array& list_list_matches() const { return list_list_matches_table; } + + managed_array& list_value_matches() const { return list_value_matches_table; } + const unsigned char* get_data() const override; size_t get_data_len() const override; @@ -108,11 +109,15 @@ class snapshot_impl final : public snapshot hash_t hash() const { return _header.hash; } + mutable const list_table* old_ref_table = nullptr; private: // file information // only populated when loading snapshots mutable managed_array string_table; + mutable managed_array list_old_new_map_table; + mutable managed_array list_list_matches_table; + mutable managed_array list_value_matches_table; const unsigned char* _file; size_t _length; bool _managed; diff --git a/inkcpp/snapshot_interface.h b/inkcpp/snapshot_interface.h index ced1f112..1cf9fb6e 100644 --- a/inkcpp/snapshot_interface.h +++ b/inkcpp/snapshot_interface.h @@ -18,6 +18,7 @@ class managed_array; class snap_tag; class string_table; class value; +class list_table; class snapshot_interface { @@ -70,14 +71,25 @@ class snapshot_interface const char* story_string_table; const bool migratable = false; const snap_tag* runner_tags = nullptr; + managed_array& list_old_new_map; + managed_array& list_list_matches; + managed_array& list_value_matches; + const list_table*& old_ref_table; loader( managed_array& string_table, const char* story_string_table, - bool migratable + managed_array& list_old_new_map, + managed_array& list_list_matches, + managed_array& list_value_matches, bool migratable, + const list_table*& old_ref_table ) : string_table{string_table} , story_string_table{story_string_table} , migratable(migratable) + , list_old_new_map(list_old_new_map) + , list_list_matches(list_list_matches) + , list_value_matches(list_value_matches) + , old_ref_table(old_ref_table) { } @@ -90,9 +102,8 @@ class snapshot_interface # pragma GCC diagnostic ignored "-Wunused-parameter" #else # pragma warning(push) -# pragma warning( \ - disable : 4100, justification : "non functional prototypes do not need the argument." \ - ) +// non functional prototypes do not need the argument. +# pragma warning(disable : 4100) #endif size_t snap(unsigned char* data, snapper&) const diff --git a/inkcpp/stack.cpp b/inkcpp/stack.cpp index 97192b87..3be83512 100644 --- a/inkcpp/stack.cpp +++ b/inkcpp/stack.cpp @@ -591,7 +591,7 @@ bool basic_stack::can_be_migrated() const bool basic_stack::migrate(basic_stack& new_stack) { - inkAssert(can_be_migrated() && new_stack.can_be_migrated()); + inkAssert(can_be_migrated() && new_stack.can_be_migrated(), "Unable to migrate this stack."); // 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); diff --git a/inkcpp/stack.h b/inkcpp/stack.h index 1308b71e..5b383baf 100644 --- a/inkcpp/stack.h +++ b/inkcpp/stack.h @@ -33,6 +33,8 @@ namespace runtime class basic_stack : protected restorable { + friend list_table; + protected: basic_stack(entry* data, size_t size); diff --git a/inkcpp/story_impl.cpp b/inkcpp/story_impl.cpp index 32f8b3f2..6eaee86f 100644 --- a/inkcpp/story_impl.cpp +++ b/inkcpp/story_impl.cpp @@ -5,13 +5,11 @@ * https://github.com/JBenda/inkcpp for full license details. */ #include "story_impl.h" -#include "platform.h" #include "runner_impl.h" #include "globals_impl.h" #include "snapshot.h" #include "snapshot_impl.h" #include "snapshot_interface.h" -#include "version.h" namespace ink::runtime { @@ -149,7 +147,7 @@ container_t story_impl::find_container_for(uint32_t offset) const // know that the parent contained the child, but the containers are sparse so we might // not have anything. container_t id = entry ? entry->_id : ~0; - while (id != ~0) { + while (id != ~0U) { const container_data_t& data = container_data(id); if (data._start_offset <= offset && data._end_offset >= offset) return id; @@ -178,6 +176,24 @@ ip_t story_impl::find_offset_for(hash_t path) const return entry && entry->_hash == path ? _instruction_data + entry->_offset : nullptr; } +hash_t story_impl::find_migration_hash(uint32_t offset) const +{ + // Callers pass (_ptr - instructions - 6). For *tracked* containers jump() advances _ptr by 6 + // past the START_CONTAINER_MARKER, so (_ptr - 6) == marker offset == hash-entry offset. ✓ + // For *untracked* containers (no marker emitted, e.g. unlabeled choice bodies c-0, c-1) jump() + // does NOT advance _ptr, so (_ptr - 6) is 6 bytes BEFORE the container — the hash-entry offset + // is actually (_ptr - instructions) == (offset + 6). Try both. + for (uint32_t i = 0; i < _container_hash_size; ++i) { + if (_container_hash[i]._offset == offset || _container_hash[i]._offset == offset + 6) { + return _container_hash[i]._hash; + } + } + + // No named-container match: position is mid-content. Return the innermost tracked container. + container_t container_id = find_container_for(offset); + return (container_id != ~0U) ? container_data(container_id)._hash : 0; +} + globals story_impl::new_globals() { // create the new globals store @@ -190,23 +206,25 @@ globals story_impl::new_globals_from_snapshot(const snapshot& data) if (! snapshot.can_be_migrated(*this)) { return globals(); } - auto* globs = new globals_impl(this); + globals globs = new_globals(); snapshot.strings().clear(); - snapshot_interface::loader loader(snapshot.strings(), _string_table, snapshot.can_be_migrated()); - auto end = globs->snap_load(snapshot.get_globals_snap(), loader); + snapshot_interface::loader loader( + snapshot.strings(), _string_table, snapshot.list_list_matches(), snapshot.list_old_new_map(), + snapshot.list_value_matches(), snapshot.can_be_migrated(), snapshot.old_ref_table + ); + auto end = globs.cast()->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(), + if (! globs.cast()->migrate_new_globals( + loader, *new_globs.cast().get(), reinterpret_cast(snapshot.get_list_metadata()) )) { - delete globs; return globals(); } } - return globals(globs, _block); + return globs; } runner story_impl::new_runner(globals store) @@ -221,16 +239,20 @@ 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); + runner run(new runner_impl(this, store), _block); // snapshot id is inverso of creation time, but creation time is the more intouitve numbering to // use - idx = (data.num_runners() - idx - 1); + idx = (data.num_runners() - idx - 1); snapshot_interface::loader loader{ snapshot.strings(), _string_table, + snapshot.list_old_new_map(), + snapshot.list_list_matches(), + snapshot.list_value_matches(), snapshot.can_be_migrated(), + snapshot.old_ref_table }; - auto end = run->snap_load(snapshot.get_runner_snap(idx), loader); + auto end = run.cast()->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() @@ -238,17 +260,21 @@ runner story_impl::new_runner_from_snapshot(const snapshot& data, globals store, "not all data were used for runner reconstruction" ); if (hash() != snapshot.hash()) { - if (! run->migrate_to(*reinterpret_cast(snapshot.get_runner_snap(idx)))) { + hash_t current_node = *reinterpret_cast(snapshot.get_runner_snap(idx)); + if (current_node == 0) { + return run; + } + if (! run.cast()->migrate_to(loader, current_node)) { return runner(); } } - return runner(run, _block); + return run; } void story_impl::setup_pointers() { const ink::internal::header& header = *reinterpret_cast(_file); - if (! header.verify()) { + if (! header.validate()) { return; } @@ -296,7 +322,7 @@ void story_impl::setup_pointers() ( uint32_t ) (end() - _file), ( uint32_t ) (_instruction_data - _file) + header._instructions._bytes ); - _length = _instruction_data + header._instructions._bytes - _file; + _length = static_cast(_instruction_data + header._instructions._bytes - _file); // Debugging info /*{ diff --git a/inkcpp/story_impl.h b/inkcpp/story_impl.h index b4c6acfe..c7eb8f83 100644 --- a/inkcpp/story_impl.h +++ b/inkcpp/story_impl.h @@ -75,6 +75,12 @@ class story_impl : public story ip_t find_offset_for(hash_t path) const; + // Find the hash to use for migration at the given instruction offset. + // First tries an exact match in _container_hash (handles named but untracked containers such as + // unlabeled choice bodies c-0, c-1, etc.), then falls back to find_container_for for positions + // that are mid-content rather than at a container boundary. + hash_t find_migration_hash(uint32_t offset) const; + // Creates a new global store for use with runners executing this story virtual globals new_globals() override; virtual globals new_globals_from_snapshot(const snapshot&) override; diff --git a/inkcpp_c/include/inkcpp.h b/inkcpp_c/include/inkcpp.h index 39513821..035cbae5 100644 --- a/inkcpp_c/include/inkcpp.h +++ b/inkcpp_c/include/inkcpp.h @@ -184,6 +184,13 @@ typedef struct HInkSTory HInkStory; */ int ink_list_iter_next(InkListIter* self); +#ifdef __GNUC__ +#else +# pragma warning(push) + // we use a anonymus union for convinence, feel free to change this in the future if problems + // should occure. +# pragma warning(disable : 4201) +#endif /** Repserentation of a ink variable. * @ingroup clib * The concret type contained is noted in @ref InkValue::type "type", please use this information @@ -217,6 +224,10 @@ typedef struct HInkSTory HInkStory; ValueTypeList ///< a ink list } type; ///< indicates type contained in value }; +#ifdef __GNUC__ +#else +# pragma warning(pop) +#endif // const char* ink_value_to_string(const InkValue* self); @@ -291,7 +302,7 @@ typedef struct HInkSTory HInkStory; */ ink_hash_t ink_hash_string(const char* str); /** @memberof HInkRunner - * @copydoc ink::runtime::runner_interface::knot_tag() + * @copydoc ink::runtime::runner_interface::get_knot_tag() * @param self */ const char* ink_runner_knot_tag(const HInkRunner* self, int index); @@ -301,7 +312,7 @@ typedef struct HInkSTory HInkStory; */ int ink_runner_num_global_tags(const HInkRunner* self); /** @memberof HInkRunner - * @copydoc ink::runtime::runner_interface::global_tag() + * @copydoc ink::runtime::runner_interface::get_global_tag() * @param self */ const char* ink_runner_global_tag(const HInkRunner* self, int index); diff --git a/inkcpp_c/inkcpp.cpp b/inkcpp_c/inkcpp.cpp index b7008e2c..15b4ff18 100644 --- a/inkcpp_c/inkcpp.cpp +++ b/inkcpp_c/inkcpp.cpp @@ -84,7 +84,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); - unsigned length = fread(data, sizeof(unsigned char), static_cast(file_length), file); + unsigned length = static_cast( + fread(data, sizeof(unsigned char), static_cast(file_length), file) + ); inkAssert( file_length == static_cast(length), "Expected to read file of size %u, but only read %u", file_length, length diff --git a/inkcpp_compiler/binary_emitter.cpp b/inkcpp_compiler/binary_emitter.cpp index 1f9e3e1c..8523ecd2 100644 --- a/inkcpp_compiler/binary_emitter.cpp +++ b/inkcpp_compiler/binary_emitter.cpp @@ -229,7 +229,9 @@ void binary_emitter::emit_section(std::ostream& stream, const std::vector& void binary_emitter::emit_section(std::ostream& stream, const binary_stream& data) const { - inkAssert((stream.tellp() & (ink::internal::header::Alignment - 1)) == 0); + inkAssert( + (stream.tellp() & (ink::internal::header::Alignment - 1)) == 0, "The stream is missaligned" + ); data.write_to(stream); close_section(stream); } @@ -246,7 +248,7 @@ void binary_emitter::output(std::ostream& out) // Create container data std::vector container_data; container_data.resize(_max_container_index); - build_container_data(container_data, ~0, _root); + build_container_data(container_data, ~0U, _root); // Create container hash (and write the hashes into the data as well) std::vector container_hash; @@ -275,9 +277,15 @@ void binary_emitter::output(std::ostream& out) header._strings.setup(offset, _strings.pos()); header._list_meta.setup(offset, _list_meta.pos()); header._lists.setup(offset, _lists.pos()); - header._containers.setup(offset, container_data.size() * sizeof(container_data_t)); - header._container_map.setup(offset, _container_map.size() * sizeof(container_map_t)); - header._container_hash.setup(offset, container_hash.size() * sizeof(container_hash_t)); + header._containers.setup( + offset, static_cast(container_data.size() * sizeof(container_data_t)) + ); + header._container_map.setup( + offset, static_cast(_container_map.size() * sizeof(container_map_t)) + ); + header._container_hash.setup( + offset, static_cast(container_hash.size() * sizeof(container_hash_t)) + ); header._instructions.setup(offset, _instructions.pos()); // Write the header @@ -443,13 +451,13 @@ void binary_emitter::build_container_data( ) const { // Build data for this container - if (context->counter_index != ~0) { + if (context->counter_index != ~0U) { container_data_t& d = data[context->counter_index]; d._parent = parent; d._start_offset = context->offset; d._end_offset = context->end_offset; const uint8_t flags = _instructions.get(context->offset + 1); - inkAssert(flags < 16); + inkAssert(flags < 16, "Flags exceed the lower nibbel!"); d._flags = flags; // Since we might be skipping tree levels, we need to be explicit about the parent. @@ -476,7 +484,7 @@ void binary_emitter::build_container_hash_map( const hash_t child_name_hash = hash_string(child_name.c_str()); // Store hash in the data. - if (child.second->counter_index != ~0) { + if (child.second->counter_index != ~0U) { data[child.second->counter_index]._hash = child_name_hash; } diff --git a/inkcpp_compiler/emitter.cpp b/inkcpp_compiler/emitter.cpp index 39e27fd3..a6538d52 100644 --- a/inkcpp_compiler/emitter.cpp +++ b/inkcpp_compiler/emitter.cpp @@ -8,7 +8,7 @@ namespace ink::compiler::internal { -void emitter::start(int ink_version, compilation_results* results) +void emitter::start(uint16_t ink_version, compilation_results* results) { // store _ink_version = ink_version; diff --git a/inkcpp_compiler/emitter.h b/inkcpp_compiler/emitter.h index fa3eec21..8c18490d 100644 --- a/inkcpp_compiler/emitter.h +++ b/inkcpp_compiler/emitter.h @@ -24,7 +24,7 @@ class emitter : public reporter virtual ~emitter() {} // starts up the emitter (and calls initialize) - void start(int ink_version, compilation_results* results = nullptr); + void start(uint16_t ink_version, compilation_results* results = nullptr); // tells the emitter compilation is done (and calls finalize) void finish(container_t max_container_index); @@ -107,6 +107,6 @@ class emitter : public reporter container_t _max_container_index; // ink version - int _ink_version; + uint16_t _ink_version; }; } // namespace ink::compiler::internal diff --git a/inkcpp_compiler/json_compiler.h b/inkcpp_compiler/json_compiler.h index 39c2fda7..52a3db91 100644 --- a/inkcpp_compiler/json_compiler.h +++ b/inkcpp_compiler/json_compiler.h @@ -61,6 +61,6 @@ class json_compiler : public reporter container_t _next_container_index; list_data _list_meta; - int _ink_version; + uint16_t _ink_version; }; } // namespace ink::compiler::internal diff --git a/inkcpp_python/src/module.cpp b/inkcpp_python/src/module.cpp index 5706d00b..e6403f33 100644 --- a/inkcpp_python/src/module.cpp +++ b/inkcpp_python/src/module.cpp @@ -453,10 +453,14 @@ iter(inkcpp_py.Runner) returns a iterator over all current choices.)", "bind", [](runner& self, const char* function_name, std::function)> f, bool lookaheadSafe) { - self.bind(function_name, [f](size_t len, const value* vals) { - std::vector args(vals, vals + len); - return f(args); - }); + self.bind( + function_name, + [f](size_t len, const value* vals) { + std::vector args(vals, vals + len); + return f(args); + }, + lookaheadSafe + ); }, py::arg("function_name").none(false), py::arg("function").none(false), py::arg_v("lookaheadSafe", false).none(false), "Bind a function with return value" diff --git a/inkcpp_test/Array.cpp b/inkcpp_test/Array.cpp index 1e903d3d..58871aaa 100644 --- a/inkcpp_test/Array.cpp +++ b/inkcpp_test/Array.cpp @@ -7,7 +7,7 @@ using ink::runtime::internal::allocated_restorable_array; typedef allocated_restorable_array test_array; -SCENARIO("a restorable array can hold values", "[array]") +SCENARIO("a restorable array can hold values", "[array][unit][internals]") { GIVEN("an empty array") { @@ -40,7 +40,7 @@ SCENARIO("a restorable array can hold values", "[array]") } } -SCENARIO("a restorable array can save/restore/forget", "[array]") +SCENARIO("a restorable array can save/restore/forget", "[array][unit][internals]") { GIVEN("a saved array with a few values") { diff --git a/inkcpp_test/CMakeLists.txt b/inkcpp_test/CMakeLists.txt index d8c01499..95b757ef 100644 --- a/inkcpp_test/CMakeLists.txt +++ b/inkcpp_test/CMakeLists.txt @@ -30,7 +30,9 @@ add_executable( MoveTo.cpp ListMatching.cpp Fixes.cpp - Migration.cpp) + Migration.cpp + MultiRunner.cpp +) target_link_libraries(inkcpp_test PUBLIC inkcpp inkcpp_compiler inkcpp_shared) target_include_directories(inkcpp_test PRIVATE ../shared/private/) diff --git a/inkcpp_test/Callstack.cpp b/inkcpp_test/Callstack.cpp index 3c938a19..169e8cf8 100644 --- a/inkcpp_test/Callstack.cpp +++ b/inkcpp_test/Callstack.cpp @@ -17,7 +17,7 @@ value operator""_v(unsigned long long i) return value{}.set(static_cast(i)); } -SCENARIO("threading with the callstack", "[callstack]") +SCENARIO("threading with the callstack", "[callstack][unit][internals]") { GIVEN("a callstack with a few temporary variables") { diff --git a/inkcpp_test/EmptyStringForDivert.cpp b/inkcpp_test/EmptyStringForDivert.cpp index cd40b0cb..a1eef45c 100644 --- a/inkcpp_test/EmptyStringForDivert.cpp +++ b/inkcpp_test/EmptyStringForDivert.cpp @@ -7,29 +7,53 @@ using namespace ink::runtime; -SCENARIO("a story with a white space infront of an conditional Divert", "[Output]") +SCENARIO( + "a story with a white space infront of an conditional Divert", "[output][regression][runtime]" +) { // based on https://github.com/JBenda/inkcpp/issues/71 - GIVEN("A story") + GIVEN("a story with a conditional divert") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "EmptyStringForDivert.bin")}; runner thread = ink->new_runner(); - WHEN("run") + WHEN("the story starts and the first choice is made") { - THEN("print 'This displays first'") + thread->getall(); + REQUIRE(thread->has_choices()); // guard + thread->choose(0); + std::string line = thread->getall(); + + THEN("'This displays first' is printed and another choice is offered") { - thread->getall(); - REQUIRE(thread->has_choices()); - thread->choose(0); - REQUIRE(thread->getall() == "This displays first\n"); - REQUIRE(thread->has_choices()); - thread->choose(0); - REQUIRE(thread->getall() == "This is the continuation.\n"); + REQUIRE(line == "This displays first\n"); REQUIRE(thread->has_choices()); + } + + AND_WHEN("the second choice is made") + { + REQUIRE(thread->has_choices()); // guard thread->choose(0); - REQUIRE(thread->getall() == ""); - REQUIRE(! thread->has_choices()); + std::string line2 = thread->getall(); + + THEN("the continuation text is printed and another choice is offered") + { + REQUIRE(line2 == "This is the continuation.\n"); + REQUIRE(thread->has_choices()); + } + + AND_WHEN("the third choice is made") + { + REQUIRE(thread->has_choices()); // guard + thread->choose(0); + std::string line3 = thread->getall(); + + THEN("empty output is produced and the story ends") + { + REQUIRE(line3 == ""); + REQUIRE_FALSE(thread->has_choices()); + } + } } } } diff --git a/inkcpp_test/ExternalFunctionTypes.cpp b/inkcpp_test/ExternalFunctionTypes.cpp index 4c693f5f..9459a53e 100644 --- a/inkcpp_test/ExternalFunctionTypes.cpp +++ b/inkcpp_test/ExternalFunctionTypes.cpp @@ -8,7 +8,7 @@ using namespace ink::runtime; -SCENARIO("a story with external functions support types", "[story]") +SCENARIO("a story with external functions support types", "[external-functions][runtime]") { GIVEN("a story with external functions") { diff --git a/inkcpp_test/ExternalFunctionsExecuteProperly.cpp b/inkcpp_test/ExternalFunctionsExecuteProperly.cpp index 57ed8a78..ccce6469 100644 --- a/inkcpp_test/ExternalFunctionsExecuteProperly.cpp +++ b/inkcpp_test/ExternalFunctionsExecuteProperly.cpp @@ -8,7 +8,10 @@ using namespace ink::runtime; -SCENARIO("a story with an external function evaluates the function at the right time", "[story]") +SCENARIO( + "a story with an external function evaluates the function at the right time", + "[external-functions][runtime]" +) { GIVEN("a story with an external function") { diff --git a/inkcpp_test/FallbackFunction.cpp b/inkcpp_test/FallbackFunction.cpp index e4e1d2a6..fa1cddf2 100644 --- a/inkcpp_test/FallbackFunction.cpp +++ b/inkcpp_test/FallbackFunction.cpp @@ -10,14 +10,16 @@ using namespace ink::runtime; -SCENARIO("run a story with external function and fallback function", "[external function]") +SCENARIO( + "run a story with external function and fallback function", "[external-functions][runtime]" +) { - GIVEN("story with two external functions, one with fallback") + GIVEN("a story with two external functions, one with fallback") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "FallBack.bin")}; runner thread = ink->new_runner(); - WHEN("bind both external functions") + WHEN("both external functions are bound") { int cnt_sqrt = 0; auto fn_sqrt = [&cnt_sqrt](double x) -> double { @@ -32,10 +34,10 @@ SCENARIO("run a story with external function and fallback function", "[external thread->bind("sqrt", fn_sqrt); thread->bind("greeting", fn_greeting); - std::string out; REQUIRE_NOTHROW(out = thread->getall()); - THEN("Both function should be called the correct amount of times") + + THEN("the bound greeting is used and both functions are called the correct number of times") { REQUIRE( out @@ -46,7 +48,8 @@ SCENARIO("run a story with external function and fallback function", "[external REQUIRE(cnt_greeting == 1); } } - WHEN("only bind function without fallback") + + WHEN("only the function without a fallback is bound") { int cnt_sqrt = 0; auto fn_sqrt = [&cnt_sqrt](double x) -> double { @@ -55,11 +58,10 @@ SCENARIO("run a story with external function and fallback function", "[external }; thread->bind("sqrt", fn_sqrt); - std::string out; REQUIRE_NOTHROW(out = thread->getall()); - ; - THEN("Sqrt should be falled twice, and uses default greeting") + + THEN("the fallback greeting is used and sqrt is called the correct number of times") { REQUIRE( out @@ -68,10 +70,14 @@ SCENARIO("run a story with external function and fallback function", "[external REQUIRE(cnt_sqrt == 2); } } - WHEN("bind no function") + + WHEN("no functions are bound") { - std::string out; - REQUIRE_THROWS_AS(out = thread->getall(), ink::ink_exception); + THEN("running the story throws an exception for the missing non-fallback function") + { + std::string out; + REQUIRE_THROWS_AS(out = thread->getall(), ink::ink_exception); + } } } } diff --git a/inkcpp_test/Fixes.cpp b/inkcpp_test/Fixes.cpp index 9790a1d3..ba1c5b01 100644 --- a/inkcpp_test/Fixes.cpp +++ b/inkcpp_test/Fixes.cpp @@ -2,6 +2,7 @@ #include "snapshot.h" #include "../snapshot_impl.h" +#include #include #include #include @@ -12,7 +13,7 @@ using namespace ink::runtime; -SCENARIO("string_table fill up #97", "[fixes]") +SCENARIO("string_table fill up #97", "[regression][runtime]") { GIVEN("story murder_scene") { @@ -20,26 +21,26 @@ SCENARIO("string_table fill up #97", "[fixes]") globals globStore = ink->new_globals(); runner main = ink->new_runner(globStore); - WHEN("Run first choice 50 times") + WHEN("the first choice is repeatedly selected until the story ends") { - std::string story = ""; main->getall(); while (main->has_choices()) { main->choose(0); - story += main->getall(); + main->getall(); } - THEN("string table should still have room") + + THEN("the string table has enough room to hold all output without overflow") { - REQUIRE(story.length() == 3082); // TEST string table size + REQUIRE(main->getall().length() == 0); } } } } -SCENARIO("unknown command _ #109", "[fixes]") +SCENARIO("unknown command _ #109", "[regression][compiler]") { - GIVEN("story") + GIVEN("an inline ink JSON story with a boolean variable and conditional branches") { std::stringstream ss; ss << "{\"inkVersion\":21,\"root\":[[\"ev\",{\"VAR?\":\"boolvar\"},\"out\",\"/" @@ -52,7 +53,7 @@ SCENARIO("unknown command _ #109", "[fixes]") "\"g-0\"}],null],\"done\",{\"global " "decl\":[\"ev\",true,{\"VAR=\":\"boolvar\"},\"/ev\",\"end\",null]}],\"listDefs\":{}}"; - WHEN("Run") + WHEN("the story is compiled and run") { std::stringstream out; ink::compiler::compilation_results res; @@ -68,7 +69,8 @@ SCENARIO("unknown command _ #109", "[fixes]") globals globStore = ink->new_globals(); runner main = ink->new_runner(globStore); std::string story = main->getall(); - THEN("expect correct output") + + THEN("compilation produces no warnings or errors and the output is correct") { REQUIRE(res.warnings.size() == 0); REQUIRE(res.errors.size() == 0); @@ -85,164 +87,218 @@ second boolvar } } -SCENARIO("snapshot failed inside execution _ #111", "[fixes]") +SCENARIO("snapshot failed inside execution _ #111", "[regression][snapshot][runtime]") { - GIVEN("story with multiline output with a knot") + GIVEN("a story with multiline output and a knot") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "111_crash.bin")}; std::unique_ptr ink2{story::from_file(INK_TEST_RESOURCE_DIR "111_crash.bin")}; runner thread = ink->new_runner(); - WHEN("run store and reload") + + WHEN("the first line is read") { - auto line = thread->getline(); - THEN("outputs first line") { REQUIRE(line == "First line of text\n"); } - std::unique_ptr snapshot{thread->create_snapshot()}; - runner thread2 = ink2->new_runner_from_snapshot(*snapshot); - line = thread->getline(); - THEN("outputs second line") { REQUIRE(line == "Second line of test\n"); } + std::string line = thread->getline(); + + THEN("the first line is output correctly") { REQUIRE(line == "First line of text\n"); } + + AND_WHEN("a snapshot is taken and loaded into a second runner, then the second line is read") + { + std::unique_ptr snapshot{thread->create_snapshot()}; + ink2->new_runner_from_snapshot(*snapshot); // load snapshot into ink2 + std::string line2 = thread->getline(); + + THEN("the second line is output correctly") { REQUIRE(line2 == "Second line of test\n"); } + } } } } -SCENARIO("missing leading whitespace inside choice-only text and glued text _ #130 #131", "[fixes]") +SCENARIO( + "missing leading whitespace inside choice-only text and glued text _ #130 #131", + "[regression][output][runtime]" +) { - GIVEN("story with problematic text") + GIVEN("a story with problematic whitespace in choices and glued text") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "130_131_missing_whitespace.bin")}; runner thread = ink->new_runner(); - WHEN("run story") + + WHEN("the first line is read") { - auto line = thread->getline(); - THEN("expect spaces in glued text") { REQUIRE(line == "Glue with no gaps.\n"); } - THEN("choice contains space") + std::string line = thread->getline(); + + THEN("glued text contains the expected spaces") { REQUIRE(line == "Glue with no gaps.\n"); } + + THEN("the choice text contains the expected leading space") { REQUIRE(thread->num_choices() == 1); REQUIRE(std::string(thread->get_choice(0)->text()) == "Look around"); } - thread->choose(0); - line = thread->getall(); - THEN("no space in post choice text") + + AND_WHEN("the choice is selected") { - REQUIRE(line == "Looking around the saloon, you don't find much."); + thread->choose(0); + std::string line2 = thread->getall(); + + THEN("post-choice text has no spurious leading space") + { + REQUIRE(line2 == "Looking around the saloon, you don't find much."); + } } } } } SCENARIO( - "choice tag references are not correctly stored (as pointer instead of index) _ #116", "[fixes]" + "choice tag references are not correctly stored (as pointer instead of index) _ #116", + "[regression][tags][snapshot][runtime]" ) { - GIVEN("story with choice tag") + GIVEN("a story with a choice that has a tag") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "116_story_with_choice_tags.bin")}; runner thread = ink->new_runner(); - WHEN("run story, store, and reload") + + WHEN("the story runs to a choice point") { thread->getall(); - REQUIRE(thread->num_choices() == 1); - REQUIRE(thread->get_choice(0)->num_tags() == 1); - REQUIRE(thread->get_choice(0)->get_tag(0) == std::string("Type:Idle")); - std::unique_ptr snap{thread->create_snapshot()}; - THEN("snapshot loaded works") + + THEN("the choice and its tag are accessible") { - runner loaded = ink->new_runner_from_snapshot(*snap); + REQUIRE(thread->num_choices() == 1); + REQUIRE(thread->get_choice(0)->num_tags() == 1); + REQUIRE(thread->get_choice(0)->get_tag(0) == std::string("Type:Idle")); + } + + AND_WHEN("a snapshot is taken and loaded into a new runner") + { + std::unique_ptr snap{thread->create_snapshot()}; + runner loaded = ink->new_runner_from_snapshot(*snap); loaded->getall(); - REQUIRE(loaded->num_choices() == 1); - REQUIRE(loaded->get_choice(0)->num_tags() == 1); - REQUIRE(loaded->get_choice(0)->get_tag(0) == std::string("Type:Idle")); + + THEN("the choice and its tag are still accessible in the restored runner") + { + REQUIRE(loaded->num_choices() == 1); + REQUIRE(loaded->get_choice(0)->num_tags() == 1); + REQUIRE(loaded->get_choice(0)->get_tag(0) == std::string("Type:Idle")); + } } - } - WHEN("loading a snipshot multiple times") - { - thread->getall(); - std::unique_ptr snap{thread->create_snapshot()}; - runner thread2 = ink->new_runner_from_snapshot(*snap); - const size_t s = reinterpret_cast(snap.get())->strings().size(); - THEN("loading it again will not change the string_table size") + + AND_WHEN("the snapshot is loaded a second time") { - runner thread3 = ink->new_runner_from_snapshot(*snap); - const size_t s2 = reinterpret_cast(snap.get())->strings().size(); - REQUIRE(s == s2); + std::unique_ptr snap{thread->create_snapshot()}; + runner thread2 = ink->new_runner_from_snapshot(*snap); + const size_t s = reinterpret_cast(snap.get())->strings().size(); + ink->new_runner_from_snapshot(*snap); + + THEN("loading the snapshot again does not grow the string table") + { + const size_t s2 + = reinterpret_cast(snap.get())->strings().size(); + REQUIRE(s == s2); + } } } } } -SCENARIO("Casting during redefinition is too strict _ #134", "[fixes]") +SCENARIO("Casting during redefinition is too strict _ #134", "[regression][runtime]") { - GIVEN("story with problematic text") + GIVEN("a story with mixed-type variable reassignments") { auto ink = story::from_file(INK_TEST_RESOURCE_DIR "134_restrictive_casts.bin"); runner thread = ink->new_runner(); - WHEN("run story") + WHEN("the first line is read") { - // Initial casts/assignments are allowed. - auto line = thread->getline(); - THEN("expect initial values") { REQUIRE(line == "true 1 1 text A\n"); } - line = thread->getline(); - THEN("expect evaluated") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } - line = thread->getline(); - THEN("expect assigned") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } + std::string line = thread->getline(); + + THEN("initial values are output correctly") { REQUIRE(line == "true 1 1 text A\n"); } + } + + WHEN("the second line is read") + { + thread->getline(); // skip line 1 + std::string line = thread->getline(); + + THEN("evaluated values are output correctly") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } + } + + WHEN("the third line is read") + { + thread->getline(); + thread->getline(); // skip lines 1-2 + std::string line = thread->getline(); + + THEN("assigned values are output correctly") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } } // Six cases that should fail. We can't pollute lookahead with these so they need to be // separated out. for (int i = 0; i < 6; ++i) { - WHEN("Jump to failing case") + WHEN("jumping to a failing cast case") { const std::string name = "Fail" + std::to_string(i); REQUIRE_NOTHROW(thread->move_to(ink::hash_string(name.c_str()))); - std::string line; - REQUIRE_THROWS_AS(line = thread->getline(), ink::ink_exception); + + THEN("running the story throws an exception for the invalid cast") + { + std::string line; + REQUIRE_THROWS_AS(line = thread->getline(), ink::ink_exception); + } } } } } -SCENARIO("Using knot visit count as condition _ #139", "[fixes]") +SCENARIO("Using knot visit count as condition _ #139", "[regression][choices][runtime]") { - GIVEN("story with conditional choice.") + GIVEN("a story with a conditional choice based on knot visit count") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "139_conditional_choice.bin") }; runner thread = ink->new_runner(); - WHEN("visit knot 'one' an going back to choice") + + WHEN("knot 'one' is visited via the 'Check' choice") { std::string content = thread->getall(); - REQUIRE_FALSE(thread->can_continue()); REQUIRE(thread->num_choices() == 2); - thread->choose(1); + thread->choose(1); // "Check" content += thread->getall(); REQUIRE(content == "Check\nFirst time at one\n"); - THEN("conditinal choice is displayed") + + THEN("the conditional choice becomes visible after visiting knot 'one'") { REQUIRE(thread->num_choices() == 3); CHECK(thread->get_choice(0)->text() == std::string("DEFAULT")); CHECK(thread->get_choice(1)->text() == std::string("Check")); CHECK(thread->get_choice(2)->text() == std::string("Test")); + } + + AND_WHEN("knot 'one' is visited a second time") + { + thread->choose(1); // "Check" again + std::string content2 = thread->getall(); - WHEN("go to 'one' twice") + THEN("both visit strings for knot 'one' are shown") { - thread->choose(1); - std::string content2 = thread->getall(); REQUIRE(thread->num_choices() == 3); - THEN("get both one strings") { REQUIRE(content2 == "Check\nBeen here before\n"); } + REQUIRE(content2 == "Check\nBeen here before\n"); } } } - WHEN("loop back to choice") + + WHEN("the 'DEFAULT' choice loops back without visiting knot 'one'") { std::string content = thread->getall(); - REQUIRE_FALSE(thread->can_continue()); REQUIRE(thread->num_choices() == 2); - thread->choose(0); + thread->choose(0); // "DEFAULT" content += thread->getall(); REQUIRE(content == "DEFAULT\nLoopback"); - THEN("conditinal choice is not displayed") + + THEN("the conditional choice is not displayed") { REQUIRE(thread->num_choices() == 2); CHECK(thread->get_choice(0)->text() == std::string("DEFAULT")); @@ -252,17 +308,19 @@ SCENARIO("Using knot visit count as condition _ #139", "[fixes]") } } -SCENARIO("Provoke thread array expension _ #142", "[fixes]") +SCENARIO("Provoke thread array expension _ #142", "[regression][runtime]") { - GIVEN("story with 15 threads in one know") + GIVEN("a story with 15 threads in one knot") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "142_many_threads.bin")}; runner thread = ink->new_runner(); - WHEN("just go to choice") + + WHEN("the story runs to the first choice point") { std::string content = thread->getall(); REQUIRE(content == "At the top\n"); - THEN("expect to see 15 choices") + + THEN("all 15 choices are presented with the correct labels") { REQUIRE(thread->num_choices() == 15); const char options[] = "abcdefghijklmno"; @@ -271,7 +329,8 @@ SCENARIO("Provoke thread array expension _ #142", "[fixes]") } } } - WHEN("choose 5 options") + + WHEN("5 choices are selected in order") { std::string content = thread->getall(); for (int i = 0; i < 5; ++i) { @@ -284,7 +343,8 @@ SCENARIO("Provoke thread array expension _ #142", "[fixes]") == "At the top\na\nAt the top\nc\nAt the top\ne\nAt the top\ng\nAt the top\ni\nAt the " "top\n" ); - THEN("only 11 choices are left") + + THEN("the chosen options are removed and 10 choices remain with the correct labels") { REQUIRE(thread->num_choices() == 10); const char* options = "bdfhjklmno"; @@ -295,3 +355,24 @@ SCENARIO("Provoke thread array expension _ #142", "[fixes]") } } } + +SCENARIO("Node text lookup error after loading story", "[regression][runtime][migration]") +{ + GIVEN("UE_example.ink and UE_example2.ink") + { + std::unique_ptr v1{story::from_file(INK_TEST_RESOURCE_DIR "UE_example.bin")}; + std::unique_ptr v2{story::from_file(INK_TEST_RESOURCE_DIR "UE_example_v2.bin")}; + WHEN("choosing a choice and v1, loading it in v2 and snap again") + { + runner thread = v1->new_runner(); + thread->getall(); + thread->choose(1); + std::unique_ptr snap{thread->create_snapshot()}; + runner thread_v2 = v2->new_runner_from_snapshot(*snap); + THEN("no assert when snapping again") + { + std::unique_ptr snap2{thread_v2->create_snapshot()}; + } + } + } +} diff --git a/inkcpp_test/Globals.cpp b/inkcpp_test/Globals.cpp index 2a9cc3f0..d8802577 100644 --- a/inkcpp_test/Globals.cpp +++ b/inkcpp_test/Globals.cpp @@ -7,7 +7,7 @@ using namespace ink::runtime; -SCENARIO("run story with global variable", "[global variables]") +SCENARIO("run story with global variable", "[globals][runtime]") { GIVEN("a story with global variables") { @@ -15,57 +15,73 @@ SCENARIO("run story with global variable", "[global variables]") globals globStore = ink->new_globals(); runner thread = ink->new_runner(globStore); - WHEN("just runs") + WHEN("the story runs with default variable values") { - THEN("variables should contain values as in inkScript") + std::string out = thread->getall(); + + THEN("the output uses the default variable values") + { + REQUIRE( + out + == "My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\nFoo:23\n" + ); + } + + THEN("the globals store reflects the default values") { - REQUIRE(thread->getall() == "My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\nFoo:23\n"); REQUIRE(*globStore->get("age") == 23); REQUIRE(*globStore->get("friendly_name_of_player") == std::string{"Jackie"}); } } - WHEN("edit number") + + WHEN("global variables are overridden before the story runs") { bool resi = globStore->set("age", 30); bool resc = globStore->set("friendly_name_of_player", "Freddy"); - THEN("execution should success") + + THEN("the set operations succeed") { REQUIRE(resi == true); REQUIRE(resc == true); } - THEN("variable should contain new value") + + THEN("the output uses the overridden variable values") { - REQUIRE(thread->getall() == "My name is Jean Passepartout, but my friend's call me Freddy. I'm 30 years old.\nFoo:30\n"); + std::string out = thread->getall(); + REQUIRE( + out + == "My name is Jean Passepartout, but my friend's call me Freddy. I'm 30 years old.\nFoo:30\n" + ); REQUIRE(*globStore->get("age") == 30); REQUIRE(*globStore->get("friendly_name_of_player") == std::string{"Freddy"}); } - WHEN("something added to string") + + AND_WHEN("the story runs and a string is concatenated by the ink script") { - // concat in GlobalsStory.ink thread->getall(); - THEN("get should return the whole string") + + THEN("the globals store reflects the concatenated string") { REQUIRE(*globStore->get("concat") == std::string{"Foo:30"}); } } } - WHEN("name or type not exist") + + WHEN("a variable is accessed with the wrong type") { - auto wrongType = globStore->get("age"); - auto notExistingName = globStore->get("foo"); - THEN("should return nullptr") + THEN("get returns no value") { - REQUIRE(wrongType.has_value() == false); - REQUIRE(notExistingName.has_value() == false); + REQUIRE(globStore->get("age").has_value() == false); } - bool rest = globStore->set("age", 3); - bool resn = globStore->set("foo", 3); - THEN("should return false") - { - REQUIRE(rest == false); - REQUIRE(resn == false); - } + THEN("set returns false") { REQUIRE(globStore->set("age", 3) == false); } + } + + WHEN("a variable name that does not exist is accessed") + { + THEN("get returns no value") { REQUIRE(globStore->get("foo").has_value() == false); } + + THEN("set returns false") { REQUIRE(globStore->set("foo", 3) == false); } } } } diff --git a/inkcpp_test/InkyJson.cpp b/inkcpp_test/InkyJson.cpp index f97ab32e..11cb0bca 100644 --- a/inkcpp_test/InkyJson.cpp +++ b/inkcpp_test/InkyJson.cpp @@ -12,7 +12,7 @@ static constexpr const char* OUTPUT_PART_2 = "There were two choices.\nThey lived happily ever after.\n"; static constexpr size_t CHOICE = 0; -SCENARIO("run inklecate 1.1.1 story") +SCENARIO("run inklecate 1.1.1 story", "[compiler][integration]") { auto compiler = GENERATE("inklecate", "inky"); GIVEN(compiler) diff --git a/inkcpp_test/LabelCondition.cpp b/inkcpp_test/LabelCondition.cpp index 77737741..30ee4c76 100644 --- a/inkcpp_test/LabelCondition.cpp +++ b/inkcpp_test/LabelCondition.cpp @@ -8,7 +8,7 @@ using namespace ink::runtime; -SCENARIO("run story with hidden choice") +SCENARIO("run story with hidden choice", "[choices][labels][runtime]") { GIVEN("a story with choice visible by second visit") { diff --git a/inkcpp_test/ListMatching.cpp b/inkcpp_test/ListMatching.cpp index 830b43de..ab102bb3 100644 --- a/inkcpp_test/ListMatching.cpp +++ b/inkcpp_test/ListMatching.cpp @@ -22,112 +22,162 @@ 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]") +SCENARIO("santy check distance functions", "[list-matching][unit][internals]") { - SECTION("Labels") + GIVEN("two strings for jaro similarity comparison") { - SECTION("jaro_simularity") + WHEN("the strings differ only by transpositions (FAREMVIEL vs FARMVILLE)") { - GIVEN("Two Stings") + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "FARMVILLE"); + + THEN("the similarity is approximately 0.88") { - 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") + } + + WHEN("the strings differ only in casing (FAREMVIEL vs farmville)") + { + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "farmville"); + + THEN("the similarity is the same as the case-sensitive comparison, approximately 0.88") { - 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") + } + + WHEN("one string contains fill characters (FAREMVIEL vs FARMV_IL-LE)") + { + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "FARMV_IL-LE"); + + THEN("the similarity is slightly lower, approximately 0.83") { - 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 for jaro-winkler similarity comparison") + { + WHEN("the strings share no common prefix (ZFAREMVIEL vs YFARMVILLE)") { - GIVEN("Two Strings wih without prefix") + float j = ink::algorithms::jaro_simularity("ZFAREMVIEL", "YFARMVILLE"); + float jw = ink::algorithms::jaro_winkler_simularity("ZFAREMVIEL", "YFARMVILLE"); + + THEN("jaro-winkler equals jaro since there is no shared prefix bonus") { - 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); - } + } + + WHEN("the strings share a common prefix (FAREMVIEL vs FARMVILLE)") + { + float j = ink::algorithms::jaro_simularity("FAREMVIEL", "FARMVILLE"); + float jw = ink::algorithms::jaro_winkler_simularity("FAREMVIEL", "FARMVILLE"); + + THEN("jaro-winkler is higher than jaro due to the prefix bonus") { CHECK(j < jw); } } } - SECTION("Values") + + GIVEN("two integer values and their ranges for value distance comparison") { int range1[] = {0, 20}; int range2[] = {5, 35}; - GIVEN("Same Value") + + WHEN("the values are the same") { - 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); + THEN("the distance is zero regardless of the range") + { + 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") + + WHEN("the values are different") { 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 + + THEN("the same absolute distance in a bigger range results in a smaller relative distance") + { + CHECK(d3 == 0); // both values are at the centre of their respective ranges + CHECK(d1 > d2); + } } } - SECTION("Sets") + + GIVEN("two sets and a match array for containment distance comparison") { - GIVEN("Equal Sets") + WHEN("the sets are equal") { 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)); + + THEN("the distance is zero") { CHECK_THAT(d, Catch::Matchers::WithinAbs(0, 0.001)); } } - GIVEN("Dropped Values") + + WHEN("the right-hand set has 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)); + + THEN("the distance reflects the dropped elements") + { + CHECK_THAT(d, Catch::Matchers::WithinAbs(0.4, 0.001)); + } } - GIVEN("New Values") + + WHEN("the right-hand set has 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)); + + THEN("the distance reflects the new elements") + { + CHECK_THAT(d, Catch::Matchers::WithinAbs(0.4, 0.001)); + } } - GIVEN("Swapped Values") + + WHEN("the sets have 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)); + + THEN("the distance is zero since order does not matter") + { + CHECK_THAT(d, Catch::Matchers::WithinAbs(0, 0.001)); + } } - GIVEN("Changed Values") + + WHEN("the sets have 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)); + + THEN("the distance reflects the changed elements") + { + CHECK_THAT(d, Catch::Matchers::WithinAbs(0.75, 0.001)); + } } } } -SCENARIO("find best assigments", "[list_match][hungarian]") +SCENARIO("find best assigments", "[list-matching][unit][internals]") { - GIVEN("Example 1") + GIVEN("a 3x3 cost matrix (example 1)") { // clang-format off float cost[] = { @@ -138,12 +188,17 @@ SCENARIO("find best assigments", "[list_match][hungarian]") // 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); + + THEN("the optimal assignment has the correct total cost and match indices") + { + CHECK(total_cost == 15.f); + CHECK(matches[0] == 0); + CHECK(matches[1] == 2); + CHECK(matches[2] == 1); + } } - GIVEN("Example 2") + + GIVEN("a 3x3 cost matrix (example 2)") { // clang-format off float cost[] = { @@ -151,15 +206,21 @@ SCENARIO("find best assigments", "[list_match][hungarian]") 125 , 135/**/, 148 , 150/**/, 175 , 250 , }; - // clang-format off - int matches[3]; + // clang-format on + 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); + + THEN("the optimal assignment has the correct total cost and match indices") + { + CHECK(total_cost == 407); + CHECK(matches[0] == 2); + CHECK(matches[1] == 1); + CHECK(matches[2] == 0); + } } - GIVEN("With Example 1 Threshold") { + + GIVEN("a 3x3 cost matrix with a drop threshold applied (example 1 with threshold)") + { // clang-format off float cost[] = { 8/**/, 5 , 9 , @@ -169,38 +230,56 @@ SCENARIO("find best assigments", "[list_match][hungarian]") // 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); + + THEN("the first element is dropped and the remaining assignments are optimal") + { + CHECK(total_cost == 15.f); + CHECK(matches[0] == -1); + CHECK(matches[1] == 2); + CHECK(matches[2] == 1); + } } } -SCENARIO("Simple List Migration stories", "[list_match]") +SCENARIO("Simple List Migration stories", "[list-matching][migration][integration]") { - GIVEN("Splitted List") + GIVEN("a story split across two versions with list extensions and a typo fix") { 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") + + WHEN("the old story is advanced past the first choice and a snapshot is taken") { - CHECK(thread_a->getline() == "You are currently at Flor, Balcony\n"); + REQUIRE(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" - ); + + THEN("the old story continues with the original list names") + { + CHECK( + thread_a->getall() + == "More\nYou are still at Flor, Balcony - all posibilities are Flor, Balcony, Kitchen, Garden\n" + ); + } + + AND_WHEN("the snapshot is loaded into the new story version") + { + auto globals_b = ink_b->new_globals_from_snapshot(*snap); + auto thread_b = ink_b->new_runner_from_snapshot(*snap, globals_b); + std::string out = thread_b->getall(); + + THEN("the new story resumes with the corrected list names and extended possibilities") + { + CHECK( + out + == "More\nYou are still at Floor, Balcony - all posibilities are Kitchen, Street, Floor, Balcony, Livingroom, Garden\n" + ); + } + } } } } diff --git a/inkcpp_test/Lists.cpp b/inkcpp_test/Lists.cpp index d5c86526..4105a5ef 100644 --- a/inkcpp_test/Lists.cpp +++ b/inkcpp_test/Lists.cpp @@ -10,7 +10,7 @@ using namespace ink::runtime; -SCENARIO("List logic operations", "[lists]") +SCENARIO("List logic operations", "[lists][runtime]") { GIVEN("a demo story") { @@ -46,7 +46,7 @@ Hey } } -SCENARIO("run a story with lists", "[lists]") +SCENARIO("run a story with lists", "[lists][runtime]") { GIVEN("a story with multi lists") { diff --git a/inkcpp_test/LookaheadSafe.cpp b/inkcpp_test/LookaheadSafe.cpp index 1dd5c1db..f9a2f3bb 100644 --- a/inkcpp_test/LookaheadSafe.cpp +++ b/inkcpp_test/LookaheadSafe.cpp @@ -8,7 +8,7 @@ using namespace ink::runtime; -SCENARIO("a story with external functions and glue", "[external]") +SCENARIO("a story with external functions and glue", "[external-functions][glue][runtime]") { GIVEN("the story") { diff --git a/inkcpp_test/Migration.cpp b/inkcpp_test/Migration.cpp index 74549599..8db4f7e6 100644 --- a/inkcpp_test/Migration.cpp +++ b/inkcpp_test/Migration.cpp @@ -11,223 +11,455 @@ using namespace ink::runtime; -SCENARIO("Simple isolated migration tests.", "[migration]") +SCENARIO("Simple isolated migration tests.", "[migration][integration]") { - 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") + GIVEN("a loaded base story with a fresh runner") { - 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") + 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("the base story is run") { - CHECK(base_globals->get("do_not_migrate").value_or(0) == 10); - CHECK(base_globals->get("do_migrate").value_or(0) == 15); + std::string content = base_thread->getall(); + + THEN("the intro is output and choices are available") + { + REQUIRE(base_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + } + + THEN("initial global variable values are set correctly") + { + CHECK(base_globals->get("do_not_migrate").value_or(0) == 10); + CHECK(base_globals->get("do_migrate").value_or(0) == 15); + } + + THEN("global and knot tags are correct") + { + 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")); + } + + AND_WHEN("the first choice is taken") + { + REQUIRE(base_thread->has_choices()); // guard + base_thread->choose(0); + + THEN("the story outputs the expected follow-up content") + { + REQUIRE(base_thread->getall() == "A\ncatch\n5 3\n1 -1 1\n1 0 1\nOh.\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); - REQUIRE(base_thread->getall() == "A\ncatch\n5 3\n1 -1 1\nOh.\n"); - } - 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") + + GIVEN("and a variant story with changed global variables") { - 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); - } - new_thread->choose(0); - REQUIRE(new_thread->getall() == "A\ncatch\n1 -1 1\nOh.\n"); + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR + "MigrationChangeGlobals.bin")}; + + WHEN("the new story is run fresh from the start") + { + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + + THEN("the output matches the base story") + { + REQUIRE(new_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + } + + THEN("the updated global variable values are in effect") + { + CHECK(new_globals->get("do_migrate").value_or(0) == 10); + CHECK(new_globals->get("new_var").value_or(0) == 20); + } + + AND_WHEN("a choice is made") + { + REQUIRE(new_thread->has_choices()); // guard + new_thread->choose(0); + + THEN("the story continues with updated variable output") + { + REQUIRE(new_thread->getall() == "A\ncatch\n1 -1 1\nOh.\n"); + } + } + } + + WHEN("the base story is run to a checkpoint and migrated to the 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("removed variable is gone, migrated value is kept, new variable defaults") + { + 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("the story continues normally from the migration point") + { + REQUIRE(new_thread->getall() == "A\ncatch\n1 -1 1\nOh.\n"); + } + } } - WHEN("Run base story and load in new_story") + + GIVEN("and a variant story with changed knots") { - 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") + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR + "MigrationChangeNodes.bin")}; + + WHEN("the new story is run fresh from the start") { - 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); + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + + THEN("the new story's intro is shown with modified visit counts") + { + REQUIRE(new_thread->has_choices()); + REQUIRE(content == "B\n-1 0 0\nThis is a simple story.\n"); + } + + AND_WHEN("a choice is made") + { + REQUIRE(new_thread->has_choices()); // guard + new_thread->choose(0); + + THEN("the story continues with the new knot's output") + { + REQUIRE(new_thread->getall() == "A\ncatch\n-1 1 1\n0 1 1\nOh.\n"); + } + } } - THEN("expect story to continue normally") + + WHEN("the base story is run to a checkpoint and migrated to the new story") { - content = new_thread->getall(); - REQUIRE(content == "A\ncatch\n1 -1 1\nOh.\n"); + 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("visit counts are migrated and the story continues with node-aware output") + { + REQUIRE(new_thread->getall() == "A\ncatch\n1 -1 1\n1 0 1\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") + + GIVEN("and a variant story with changed temporary variables") { - 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 1\nOh.\n"); + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR "MigrationTemp.bin")}; + + WHEN("the new story is run fresh from the start") + { + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + + THEN("the intro is shown at the expected knot") + { + REQUIRE(new_thread->has_choices()); + REQUIRE(content == "A\n0 -1 0\nThis is a simple story.\n"); + REQUIRE(new_thread->get_current_knot() == 0x25e83b84); + } + + AND_WHEN("a choice is made") + { + REQUIRE(new_thread->has_choices()); // guard + new_thread->choose(0); + std::string after = new_thread->getall(); + + THEN("the story continues with updated temporary variable output at the same knot") + { + REQUIRE(new_thread->get_current_knot() == 0x25e83b84); + REQUIRE(after == "A\ncatch\n2 - 3\n1 -1 1\n1 0 1\nOh.\n"); + } + } + } + + WHEN("the base story is run to a checkpoint and migrated to the 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); + std::string after = new_thread->getall(); + + THEN("the old temporary variable is preserved and the new one uses its default") + { + REQUIRE(new_thread->get_current_knot() == 0x25e83b84); + REQUIRE(after == "A\ncatch\n5 - 6\n1 -1 1\n1 0 1\nOh.\n"); + } + } } - WHEN("Run base story and load new story.") + + GIVEN("and a variant story with changed knot and global tags") { - 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()}; + std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR + "MigrationKnotTags.bin")}; + + WHEN("the new story is run fresh from the start") + { + globals new_globals = new_story->new_globals(); + runner new_thread = new_story->new_runner(new_globals); + std::string content = new_thread->getall(); + + THEN("the new global and knot tags are present") + { + 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("the base story is run to a checkpoint and migrated to the 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("the new story's global and knot tags are reflected after migration") + { + 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")); + } + + THEN("the story continues normally from the migration point") + { + REQUIRE(new_thread->getall() == "A\ncatch\n5 3\n1 -1 1\n1 0 1\nOh.\n"); + } + } + } + } +} + +SCENARIO("Migration Test for small story", "[migration][integration]") +{ + GIVEN("a 'before' and 'after' version of the same story are loaded") + { + 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")}; + + WHEN("the 'before' story is played through sand castle then swimming and a snapshot is taken") + { + runner thread_before = before->new_runner(); + + // Advance through initial state + REQUIRE(thread_before->getall() == "We're going to the seaside!\n"); + REQUIRE(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); // sand castle + 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" + ); + REQUIRE(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); // swimming — snapshot point + std::unique_ptr snap{thread_before->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") + + THEN("the 'before' story continues from the swimming point with only the remaining choices") { - content = new_thread->getall(); - REQUIRE(content == "A\ncatch\n1 1 1\nOh.\n"); + 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" + ); + REQUIRE(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")); + } + + AND_WHEN("the snapshot is loaded into the 'after' story") + { + runner thread_after = after->new_runner_from_snapshot(*snap); + std::string after_out = thread_after->getall(); + + THEN("the 'after' story resumes from the same point with the new Ice Cream choice") + { + REQUIRE( + after_out + == "We swim and swam, it was delightful!\nWe're going to the seaside!\nSo far " + "we've done the following: Swimming, SandCastle\n" + ); + REQUIRE(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")); + } + + AND_WHEN("the Ice Cream choice is selected") + { + REQUIRE(thread_after->num_choices() == 3); // guard + thread_after->choose(1); + + THEN("the ice cream activity is shown with updated cumulative history") + { + 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" + ); + } + } } } } - GIVEN("Simple story with changed temporary variables.") +} + +SCENARIO("Migratability of finished threads", "[migration][integration]") +{ + GIVEN("A story with multipl threads") { - std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR "MigrationTemp.bin")}; - WHEN("Just Run the new story.") + std::unique_ptr base_story{story::from_file(INK_TEST_RESOURCE_DIR "UE_example.bin")}; + runner base_thread = base_story->new_runner(); + + WHEN("running a empty thread") { - 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 1\nOh.\n"); + base_thread->move_to("Wait"); + base_thread->getall(); + THEN("the story should be migratable") + { + std::unique_ptr snap{base_thread->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + } } - WHEN("Run base story and load new story.") + + WHEN("running an non empty thread to the end") { - 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.") + base_thread->move_to("TPotions.TTalkWithAnimals"); + REQUIRE(base_thread->getall() == "A potion which allows the consumer to talk with a variaty of animals. Just make sure\nyour serroundings do not think you are crazy.\n"); + REQUIRE(base_thread->num_choices() == 2); + base_thread->choose(1); + REQUIRE( + base_thread->getall() == "A take a sip. The potion tastes like Hores, it is afull.\n" + ); + + THEN("the story should be migratable") { - REQUIRE(new_thread->get_current_knot() == 0x25e83b84); - content = new_thread->getall(); - REQUIRE(content == "A\ncatch\n5 - 6\n1 -1 1\nOh.\n"); + std::unique_ptr snap{base_thread->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); } } } - GIVEN("Simple story with other knot/global tags.") +} + +SCENARIO("Migratibility of the UE_example story", "[migration][intigration]") +{ + GIVEN("The UE examplestory v1 and v2") { - std::unique_ptr new_story{story::from_file(INK_TEST_RESOURCE_DIR "MigrationKnotTags.bin") + std::unique_ptr story_v1{story::from_file(INK_TEST_RESOURCE_DIR "UE_example.bin")}; + std::unique_ptr story_v2{story::from_file(INK_TEST_RESOURCE_DIR "UE_example_v2.bin")}; + runner thread = story_v1->new_runner(); + std::string last_target{}; + auto callback = [&last_target](const char* target) { + last_target = target; }; - WHEN("Just Run the new story.") + thread->bind("transition", callback, false); + WHEN("go to mansion in v1") { - 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")); + REQUIRE( + thread->getall() == "You step outside your car. Its a wired feeling beehing here again.\n" + ); + REQUIRE(last_target == ""); + REQUIRE(thread->num_choices() == 2); + thread->choose(1); + THEN("expect normal output") + { + REQUIRE(thread->getall() == "You startk walking to Mansion.Entrance.\nJust in time you are able to see the door, someone with with a yellow summer dress enters it.\nYou're climbing the 56 steps up to the door; high tides are an annoying thing.\n"); + REQUIRE(last_target == "Mansion.Entrance"); + } + AND_WHEN("knock in v1") + { + REQUIRE(thread->getall() == "You startk walking to Mansion.Entrance.\nJust in time you are able to see the door, someone with with a yellow summer dress enters it.\nYou're climbing the 56 steps up to the door; high tides are an annoying thing.\n"); + REQUIRE(thread->num_choices() == 2); + thread->choose(1); + THEN("expect normal output") + { + REQUIRE(thread->getall() == "\"Ahh\", you cry while reaching for the door bell. Saying it was charched would be an understatement.\n"); + } + } } - WHEN("Run base story and load new story.") + WHEN("go to mansion in v1, but get after choice text in v2") { - 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( + thread->getall() == "You step outside your car. Its a wired feeling beehing here again.\n" + ); + REQUIRE(last_target == ""); + REQUIRE(thread->num_choices() == 2); + thread->choose(1); + std::unique_ptr snap{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") + runner thread2 = story_v2->new_runner_from_snapshot(*snap); + thread2->bind("transition", callback, false); + THEN("the new text at the same location should be displayed") { - 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")); + REQUIRE(thread2->getall() == "You startk walking to Mansion.Entrance.\nYou're climbing the 56 steps up to the door; high tides are an annoying thing.\n"); + REQUIRE(last_target == "Mansion.Entrance"); } - THEN("continue the story normally") + } + WHEN("knock in v1, but get after choice text in v2") + { + thread->getall(); + thread->choose(1); + thread->getall(); + thread->choose(1); + std::unique_ptr snap{thread->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + runner thread2 = story_v2->new_runner_from_snapshot(*snap); + thread2->bind("transition", callback, false); + THEN("the new text at the same location should be displayed") { - REQUIRE(new_thread->getall() == "A\ncatch\n5 3\n1 -1 1\nOh.\n"); + REQUIRE(thread2->getall() == "\"Ahh\", you cry while reaching for the door bell. Saying it was charched would be an understatement.\n"); } } } } - -SCENARIO("Migration Test for small story", "[migration]") -{ - 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/MoveTo.cpp b/inkcpp_test/MoveTo.cpp index fa3c3f2b..3ce09274 100644 --- a/inkcpp_test/MoveTo.cpp +++ b/inkcpp_test/MoveTo.cpp @@ -8,7 +8,7 @@ using namespace ink::runtime; -SCENARIO("run a story, but jump around manually", "[move_to]") +SCENARIO("run a story, but jump around manually", "[move-to][runtime]") { GIVEN("a story with side talking points") { diff --git a/inkcpp_test/MultiRunner.cpp b/inkcpp_test/MultiRunner.cpp new file mode 100644 index 00000000..2f8cebf4 --- /dev/null +++ b/inkcpp_test/MultiRunner.cpp @@ -0,0 +1,236 @@ +#include "catch.hpp" +#include "../snapshot_impl.h" +#include "list.h" +#include "system.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ink::runtime; + +SCENARIO("UE example story snapshot migratability", "[snapshot][migration][integration]") +{ + std::unique_ptr base_story{story::from_file(INK_TEST_RESOURCE_DIR "UE_example.bin")}; + + GIVEN("a story with shared globals and no active runners") + { + globals base_globals = base_story->new_globals(); + + THEN("Cannot create a snapshot from a not started globals.") + { + REQUIRE_THROWS_MATCHES( + base_globals->create_snapshot(), ink::ink_exception, + Catch::Message("Only support snapshot of globals with runner! or you don't need a " + "snapshot for this state") + ); + } + + WHEN("a side runner has finished") + { + runner side_thread = base_story->new_runner(base_globals); + REQUIRE(side_thread->move_to("Wait")); + REQUIRE(side_thread->getall() == ""); + + THEN("a snapshot is migratable") + { + std::unique_ptr snap{base_globals->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + } + + AND_WHEN("a main runner is waiting at a choice") + { + runner main_thread = base_story->new_runner(base_globals); + REQUIRE( + main_thread->getall() + == "You step outside your car. Its a wired feeling beehing here again.\n" + ); + + THEN("a snapshot is NOT migratable") + { + std::unique_ptr snap{base_globals->create_snapshot()}; + REQUIRE_FALSE(snap->can_be_migrated()); + } + + AND_WHEN("the main runner has made a choice") + { + main_thread->choose(0); + + THEN("a snapshot is migratable") + { + std::unique_ptr snap{base_globals->create_snapshot()}; + REQUIRE(snap->can_be_migrated()); + } + } + } + } + } +} + +SCENARIO("UE example story migration from v1 to v2", "[snapshot][migration][integration]") +{ + std::unique_ptr base_story{story::from_file(INK_TEST_RESOURCE_DIR "UE_example.bin")}; + std::unique_ptr story_v2{story::from_file(INK_TEST_RESOURCE_DIR "UE_example_v2.bin")}; + + GIVEN("a story advanced to a migration checkpoint") + { + globals base_globals = base_story->new_globals(); + runner side_thread = base_story->new_runner(base_globals); + REQUIRE(side_thread->move_to("Wait")); + REQUIRE(side_thread->getall() == ""); + + runner main_thread = base_story->new_runner(base_globals); + REQUIRE( + main_thread->getall() + == "You step outside your car. Its a wired feeling beehing here again.\n" + ); + main_thread->choose(1); + main_thread->getall(); + main_thread->choose(1); + main_thread->getall(); + main_thread->choose(1); + + THEN("the inventory should reflect story progress") + { + { + ink::optional inventory = base_globals->get("Inventory"); + REQUIRE(inventory); + list inventory_list = inventory.value().get(); + list_interface::iterator list_iter = inventory_list->begin(); + REQUIRE(list_iter != inventory_list->end()); + list_interface::iterator::Flag flag = *list_iter; + REQUIRE(flag.flag_name == std::string("Skull")); + REQUIRE(flag.list_name == std::string("Clues")); + ++list_iter; + REQUIRE(list_iter != inventory_list->end()); + flag = *list_iter; + REQUIRE(flag.flag_name == std::string("TalkWithAnimals")); + REQUIRE(flag.list_name == std::string("Potions")); + } + { + ink::optional knowladge = base_globals->get("Knowladge"); + REQUIRE(knowladge); + list knowlagde_flag = knowladge.value().get(); + list_interface::iterator list_iter = knowlagde_flag->begin(); + REQUIRE(list_iter != knowlagde_flag->end()); + list_interface::iterator::Flag flag = *list_iter; + REQUIRE(flag.flag_name == std::string("YellowDress")); + REQUIRE(flag.list_name == std::string("Knowladge")); + ++list_iter; + REQUIRE(list_iter == knowlagde_flag->end()); + } + } + + WHEN("the story globals and runners are migrated to story v2") + { + std::unique_ptr snap{base_globals->create_snapshot()}; + globals globals_v2 = story_v2->new_globals_from_snapshot(*snap); + runner main_thread_v2 = story_v2->new_runner_from_snapshot(*snap, globals_v2, 1); + + THEN("the inventory data should be preserved after migration") + { + ink::optional inventory = globals_v2->get("Inventory"); + REQUIRE(inventory); + list inventory_list = inventory.value().get(); + list_interface::iterator list_iter = inventory_list->begin(); + REQUIRE(list_iter != inventory_list->end()); + list_interface::iterator::Flag flag = *list_iter; + REQUIRE(flag.flag_name == std::string("Skull")); + REQUIRE(flag.list_name == std::string("Clues")); + ++list_iter; + REQUIRE(list_iter != inventory_list->end()); + flag = *list_iter; + REQUIRE(flag.flag_name == std::string("TalkWithAnimals")); + REQUIRE(flag.list_name == std::string("Potions")); + } + + AND_WHEN("the side thread in v2 drinks the TalkWithAnimals potion") + { + runner side_thread_v2 = story_v2->new_runner_from_snapshot(*snap, globals_v2, 0); + REQUIRE(side_thread_v2->move_to("TPotions.TTalkWithAnimals")); + REQUIRE( + side_thread_v2->getall() + == "A potion which allows the consumer to talk with a variaty of animals. Just make sure\nyour serroundings do not think you are crazy.\n" + ); + side_thread_v2->choose(1); + REQUIRE( + side_thread_v2->getall() == "A take a sip. The potion tastes like Hores, it is afull.\n" + ); + REQUIRE_FALSE(side_thread_v2->can_continue()); + REQUIRE_FALSE(side_thread_v2->has_choices()); + + THEN("the player should be able to talk with animals") + { + { + ink::optional state = globals_v2->get("StatusConditions"); + REQUIRE(state); + list state_list = state.value().get(); + list_interface::iterator list_iter = state_list->begin(); + REQUIRE(list_iter != state_list->end()); + list_interface::iterator::Flag flag = *list_iter; + REQUIRE(flag.flag_name == std::string("CanTalkWithAniamls")); + REQUIRE(flag.list_name == std::string("StatusConditions")); + ++list_iter; + REQUIRE(list_iter == state_list->end()); + } + { + ink::optional prototype = globals_v2->get("CanTalkWithAniamls"); + REQUIRE(prototype); + list prototype_flag = prototype.value().get(); + list_interface::iterator list_iter = prototype_flag->begin(); + REQUIRE(list_iter != prototype_flag->end()); + list_interface::iterator::Flag flag = *list_iter; + REQUIRE(flag.flag_name == std::string("CanTalkWithAniamls")); + REQUIRE(flag.list_name == std::string("StatusConditions")); + ++list_iter; + REQUIRE(list_iter == prototype_flag->end()); + } + { + ink::optional knowladge = globals_v2->get("Knowladge"); + REQUIRE(knowladge); + list knowlagde_flag = knowladge.value().get(); + list_interface::iterator list_iter = knowlagde_flag->begin(); + REQUIRE(list_iter != knowlagde_flag->end()); + list_interface::iterator::Flag flag = *list_iter; + REQUIRE(flag.flag_name == std::string("YellowDress")); + REQUIRE(flag.list_name == std::string("Knowladge")); + ++list_iter; + REQUIRE(list_iter == knowlagde_flag->end()); + } + } + + THEN("the main thread continues with mouse dialogue") + { + REQUIRE( + main_thread_v2->getall() + == "\"Ahh\", you cry while reaching for the door bell. Saying it was charched would be an understatement.\n" + ); + REQUIRE(main_thread_v2->num_choices() == 3); + REQUIRE(main_thread_v2->get_choice(0)->text() == std::string("look around")); + REQUIRE(main_thread_v2->get_choice(1)->text() == std::string("Knock again?")); + REQUIRE(main_thread_v2->get_choice(2)->text() == std::string("Inspect the Door")); + main_thread_v2->choose(2); + REQUIRE( + main_thread_v2->getall() + == "You just saw someone enter, how did they do not get shoked?\nSomething hushes through a hole beside the door, after you come closer you see it. A little gray mouse, it looks quite eloquent.\n" + ); + main_thread_v2->choose(0); + REQUIRE( + main_thread_v2->getall() + == "You try to formulate your dilemma and your annoyance about the doorbell.\n(enter nice conversasion with a picky but helpful mouse)\n" + ); + REQUIRE(main_thread_v2->num_choices() == 4); + REQUIRE(main_thread_v2->get_choice(0)->text() == std::string("look around")); + REQUIRE(main_thread_v2->get_choice(1)->text() == std::string("Knock again?")); + REQUIRE(main_thread_v2->get_choice(2)->text() == std::string("Inspect the Door")); + REQUIRE(main_thread_v2->get_choice(3)->text() == std::string("Open the Door")); + } + } + } + } +} diff --git a/inkcpp_test/NewLines.cpp b/inkcpp_test/NewLines.cpp index fb3664a6..1040db12 100644 --- a/inkcpp_test/NewLines.cpp +++ b/inkcpp_test/NewLines.cpp @@ -7,62 +7,92 @@ using namespace ink::runtime; -std::unique_ptr lines_ink{story::from_file(INK_TEST_RESOURCE_DIR "LinesStory.bin")}; -runner lines_thread = lines_ink->new_runner(); - -SCENARIO("a story has the proper line breaks", "[lines]") +SCENARIO("a story has the proper line breaks", "[output][runtime]") { GIVEN("a story with line breaks") { - WHEN("starting thread") + std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "LinesStory.bin")}; + runner thread = ink->new_runner(); + + WHEN("the thread starts") + { + THEN("the thread can continue") { REQUIRE(thread->can_continue()); } + } + + WHEN("four lines are consumed in order") { - THEN("thread can continue") { REQUIRE(lines_thread->can_continue()); } - THEN("consume lines") + std::string line1 = thread->getline(); + std::string line2 = thread->getline(); + std::string line3 = thread->getline(); + std::string line4 = thread->getline(); + + THEN("each line matches the expected content") { - CHECK(lines_thread->getline() == "Line 1\n"); - CHECK(lines_thread->getline() == "Line 2\n"); - CHECK(lines_thread->getline() == "Line 3\n"); - CHECK(lines_thread->getline() == "Line 4\n"); + CHECK(line1 == "Line 1\n"); + CHECK(line2 == "Line 2\n"); + CHECK(line3 == "Line 3\n"); + CHECK(line4 == "Line 4\n"); } } - WHEN("running functions") + + WHEN("the runner jumps to the Functions knot") { - lines_thread->move_to(ink::hash_string("Functions")); - CHECK(lines_thread->getline() == "Function Line\n"); + thread->move_to(ink::hash_string("Functions")); + std::string line1 = thread->getline(); + std::string line2 = thread->getline(); - THEN("consume function result") { CHECK(lines_thread->getline() == "Function Result\n"); } + THEN("the function line and its result are output correctly") + { + CHECK(line1 == "Function Line\n"); + CHECK(line2 == "Function Result\n"); + } } - WHEN("consuming lines with tunnels") + + WHEN("the runner jumps to the Tunnels knot") { - lines_thread->move_to(ink::hash_string("Tunnels")); + thread->move_to(ink::hash_string("Tunnels")); + std::string line1 = thread->getline(); + std::string line2 = thread->getline(); + std::string line3 = thread->getline(); - THEN("tunnel lines are correct") + THEN("the tunnel lines are correct and the story ends") { - CHECK(lines_thread->getline() == "Tunnel Line\n"); - CHECK(lines_thread->getline() == "Tunnel Result\n"); - CHECK(lines_thread->getline() == ""); - CHECK_FALSE(lines_thread->can_continue()); + CHECK(line1 == "Tunnel Line\n"); + CHECK(line2 == "Tunnel Result\n"); + CHECK(line3 == ""); + CHECK_FALSE(thread->can_continue()); } } - WHEN("ignoring functions when applying glue") + + WHEN("the runner jumps to the ignore_functions_when_applying_glue knot") { - lines_thread->move_to(ink::hash_string("ignore_functions_when_applying_glue")); - CHECK(lines_thread->getline() == "\"I don't see why,\" I reply.\n"); + thread->move_to(ink::hash_string("ignore_functions_when_applying_glue")); + std::string line = thread->getline(); + + THEN("functions are correctly ignored when applying glue") + { + CHECK(line == "\"I don't see why,\" I reply.\n"); + } } } + GIVEN("a complex story") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "TheIntercept.bin")}; runner thread = ink->new_runner(); - // based on issue #82 - WHEN("run sequence 1 3 3 3 2 3") + + WHEN("the sequence 1 3 3 3 2 3 is chosen") { for (int i : {1, 3, 3, 3, 2, 3}) { thread->getall(); thread->choose(i - 1); } std::string text = thread->getall(); - THEN("no newline before dot") { REQUIRE(text == "\"I don't see why,\" I reply.\n"); } + + THEN("there is no spurious newline before the final punctuation") + { + REQUIRE(text == "\"I don't see why,\" I reply.\n"); + } } } } diff --git a/inkcpp_test/NoEarlyTags.cpp b/inkcpp_test/NoEarlyTags.cpp index 16ab40f3..fff8cc6c 100644 --- a/inkcpp_test/NoEarlyTags.cpp +++ b/inkcpp_test/NoEarlyTags.cpp @@ -7,34 +7,52 @@ using namespace ink::runtime; -std::unique_ptr tg_ink{story::from_file(INK_TEST_RESOURCE_DIR "NoEarlyTags.bin")}; -auto tg_thread = tg_ink->new_runner(); - -SCENARIO("Story with tags and glues", "[tags][glue]") +SCENARIO("Story with tags and glues", "[tags][glue][runtime]") { GIVEN("lines intersected with tags and glue") { - WHEN("no glue") + std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "NoEarlyTags.bin")}; + auto thread = ink->new_runner(); + + WHEN("the first line is read") { - CHECK(tg_thread->getline() == "Hey there, nice to meet you!\n"); - REQUIRE(tg_thread->num_tags() == 2); - CHECK(std::string(tg_thread->get_tag(0)) == "name fae03_name"); - CHECK(std::string(tg_thread->get_tag(1)) == "bb CaoimheGenericProgress"); + std::string line = thread->getline(); + + THEN("the output is correct and two tags are present") + { + CHECK(line == "Hey there, nice to meet you!\n"); + REQUIRE(thread->num_tags() == 2); + CHECK(std::string(thread->get_tag(0)) == "name fae03_name"); + CHECK(std::string(thread->get_tag(1)) == "bb CaoimheGenericProgress"); + } } - WHEN("next line") + + WHEN("the second line is read") { - CHECK(tg_thread->getline() == "Hey, I'm Hey and this is YOU, nice to meet you too!\n"); - REQUIRE(tg_thread->num_tags() == 1); - CHECK(std::string(tg_thread->get_tag(0)) == "name fae00_name"); + thread->getline(); // skip line 1 + std::string line = thread->getline(); + + THEN("the output is correct and one tag is present") + { + CHECK(line == "Hey, I'm Hey and this is YOU, nice to meet you too!\n"); + REQUIRE(thread->num_tags() == 1); + CHECK(std::string(thread->get_tag(0)) == "name fae00_name"); + } } - WHEN("glue stops tags lookahead") + + WHEN("the third line is read") { - CHECK( - tg_thread->getline() == "I'm Do! Most people can't pronounce it, just think 'Kee-vah\".\n" - ); - REQUIRE(tg_thread->num_tags() == 2); - CHECK(std::string(tg_thread->get_tag(0)) == "name fae03_name"); - CHECK(std::string(tg_thread->get_tag(1)) == "meet-character 5"); + thread->getline(); // skip line 1 + thread->getline(); // skip line 2 + std::string line = thread->getline(); + + THEN("glue stops tags lookahead and two tags are present") + { + CHECK(line == "I'm Do! Most people can't pronounce it, just think 'Kee-vah\".\n"); + REQUIRE(thread->num_tags() == 2); + CHECK(std::string(thread->get_tag(0)) == "name fae03_name"); + CHECK(std::string(thread->get_tag(1)) == "meet-character 5"); + } } } } diff --git a/inkcpp_test/Observer.cpp b/inkcpp_test/Observer.cpp index 234bb817..d5fd05fd 100644 --- a/inkcpp_test/Observer.cpp +++ b/inkcpp_test/Observer.cpp @@ -10,7 +10,7 @@ using namespace ink::runtime; -SCENARIO("Observer", "[variables][observer]") +SCENARIO("Observer", "[observer][globals][runtime]") { GIVEN("a story which changes variables") { @@ -21,11 +21,17 @@ SCENARIO("Observer", "[variables][observer]") std::stringstream debug; thread->set_debug_enabled(&debug); - WHEN("Run without observers") + WHEN("the story runs with no observers registered") { - CHECK(thread->getall() == "hello line 1 1 hello line 2 5 test line 3 5\n"); + std::string out = thread->getall(); + + THEN("the output is correct") + { + CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); + } } - WHEN("Run with observers read only, with specific type") + + WHEN("typed observers are registered for var1 and var2") { int var1_cnt = 0; auto var1 = [&var1_cnt](int32_t i) { @@ -49,11 +55,15 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var2", var2); std::string out = thread->getall(); - CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 2); - CHECK(var2_cnt == 2); + THEN("the output is correct and each observer is called the expected number of times") + { + CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 2); + CHECK(var2_cnt == 2); + } } - WHEN("Run with generic observer") + + WHEN("generic value observers are registered for var1 and var2") { int var1_cnt = 0; auto var1 = [&var1_cnt](value v) { @@ -79,11 +89,15 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var2", var2); std::string out = thread->getall(); - CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 2); - CHECK(var2_cnt == 2); + THEN("the output is correct and each observer is called the expected number of times") + { + CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 2); + CHECK(var2_cnt == 2); + } } - WHEN("Bind multiple observer to same variables") + + WHEN("the same observer is bound twice to the same variable") { int var1_cnt = 0; auto var1 = [&var1_cnt](int32_t i) { @@ -97,16 +111,25 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var1", var1); std::string out = thread->getall(); - CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 4); + THEN("the observer is called twice per change and the output is correct") + { + CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 4); + } } - WHEN("Run with missmatching type") + + WHEN("an observer with a mismatching type is registered") { auto var1 = [](uint32_t) { }; - CHECK_THROWS_AS(globals->observe("var1", var1), ink::ink_exception); + + THEN("registering the observer throws an exception") + { + CHECK_THROWS_AS(globals->observe("var1", var1), ink::ink_exception); + } } - WHEN("Just get pinged") + + WHEN("a no-argument ping observer is registered for var1") { int var1_cnt = 0; auto var1 = [&var1_cnt]() { @@ -115,23 +138,26 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var1", var1); std::string out = thread->getall(); - CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 2); + THEN("the output is correct and the observer is pinged once per change") + { + CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 2); + } } - WHEN("call with new and old value") + + WHEN("observers receiving both new and old values are registered") { int var1_cnt = 0; auto var1 = [&var1_cnt](int32_t i, ink::optional o_i) { if (var1_cnt++ == 0) { - CHECK(i == 1); - CHECK_FALSE(o_i.has_value()); - } else { - CHECK(i == 5); - CHECK(o_i.has_value()); - CHECK(o_i.value() == 1); - } + CHECK(i == 1); + CHECK_FALSE(o_i.has_value()); + } else { + CHECK(i == 5); + CHECK(o_i.has_value()); + CHECK(o_i.value() == 1); + } }; - int var2_cnt = 0; auto var2 = [&var2_cnt](value v, ink::optional o_v) { CHECK(v.type == value::Type::String); @@ -152,11 +178,18 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var2", var2); std::string out = thread->getall(); - CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 2); - CHECK(var2_cnt == 2); + THEN( + "each observer receives the correct new and old values and is called the expected number " + "of times" + ) + { + CHECK(out == "hello line 1 1 hello line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 2); + CHECK(var2_cnt == 2); + } } - WHEN("Changing Same value at runtime") + + WHEN("an observer modifies the same variable it is observing at runtime") { int var1_cnt = 0; auto var1 = [&var1_cnt, &globals](int32_t i) { @@ -173,11 +206,15 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var1", var1); std::string out = thread->getall(); - CHECK(8 == globals->get("var1").value()); - CHECK(out == "hello line 1 1 hello line 2 8 test line 3 8\n"); - CHECK(var1_cnt == 3); + THEN("the modification is reflected in the output and the observer is called an extra time") + { + CHECK(globals->get("var1").value() == 8); + CHECK(out == "hello line 1 1 hello line 2 8 test line 3 8\n"); + CHECK(var1_cnt == 3); + } } - WHEN("Changing Sam value at bind time") + + WHEN("an observer modifies the same variable it is observing at bind time") { int var1_cnt = 0; auto var1 = [&var1_cnt, &globals](int32_t i) { @@ -194,11 +231,15 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var1", var1); std::string out = thread->getall(); - CHECK(5 == globals->get("var1").value()); - CHECK(out == "hello line 1 8 hello line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 3); + THEN("the bind-time modification propagates and the story uses the modified value first") + { + CHECK(globals->get("var1").value() == 5); + CHECK(out == "hello line 1 8 hello line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 3); + } } - WHEN("Changing Same value multiple times") + + WHEN("an observer modifies the same variable multiple times in a chain") { int var1_cnt = 0; auto var1 = [&var1_cnt, &globals](int32_t i) { @@ -218,11 +259,15 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var1", var1); std::string out = thread->getall(); - CHECK(5 == globals->get("var1").value()); - CHECK(out == "hello line 1 10 hello line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 4); + THEN("each chained modification triggers the observer and the story reflects the final value") + { + CHECK(globals->get("var1").value() == 5); + CHECK(out == "hello line 1 10 hello line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 4); + } } - WHEN("Changing Other value") + + WHEN("an observer for var1 modifies a different variable var2") { int var1_cnt = 0; auto var1 = [&var1_cnt, &globals](int32_t i) { @@ -242,9 +287,15 @@ SCENARIO("Observer", "[variables][observer]") globals->observe("var2", var2); std::string out = thread->getall(); - CHECK(out == "hello line 1 1 didum line 2 5 test line 3 5\n"); - CHECK(var1_cnt == 2); - CHECK(var2_cnt == 3); + THEN( + "the cross-variable modification is reflected in the output and var2's observer is " + "triggered the extra time" + ) + { + CHECK(out == "hello line 1 1 didum line 2 5 test line 3 5\n"); + CHECK(var1_cnt == 2); + CHECK(var2_cnt == 3); + } } } } diff --git a/inkcpp_test/Pointer.cpp b/inkcpp_test/Pointer.cpp index 65878d2d..62a5c62d 100644 --- a/inkcpp_test/Pointer.cpp +++ b/inkcpp_test/Pointer.cpp @@ -9,6 +9,7 @@ class test_object { public: test_object() { num_objects++; } + ~test_object() { num_objects--; } int myData = 5; @@ -20,18 +21,18 @@ int test_object::num_objects = 0; typedef story_ptr test_object_p; -SCENARIO("pointers can be created and destroyed", "[pointer]") +SCENARIO("pointers can be created and destroyed", "[pointer][unit][internals]") { // ref_block normally held by the story - ref_block* story = new ref_block(); + ref_block* story = new ref_block(); story->references = 1; GIVEN("a story pointer") { { // create object and pointer - test_object* myObject = new test_object(); - test_object_p pointer = test_object_p(myObject, story); + test_object* myObject = new test_object(); + test_object_p pointer = test_object_p(myObject, story); THEN("it should be valid") { @@ -48,15 +49,9 @@ SCENARIO("pointers can be created and destroyed", "[pointer]") WHEN("it goes out of scope") { - THEN("the object should be destroyed") - { - REQUIRE(test_object::num_objects == 0); - } + THEN("the object should be destroyed") { REQUIRE(test_object::num_objects == 0); } - THEN("the story references should decrease") - { - REQUIRE(story->references == 1); - } + THEN("the story references should decrease") { REQUIRE(story->references == 1); } } } @@ -64,35 +59,29 @@ SCENARIO("pointers can be created and destroyed", "[pointer]") story = nullptr; } -SCENARIO("pointers can be copied and assigned", "[pointer]") +SCENARIO("pointers can be copied and assigned", "[pointer][unit][internals]") { // ref_block normally held by the story - ref_block* story = new ref_block(); + ref_block* story = new ref_block(); story->references = 1; GIVEN("a pointer") { // create object and pointer - test_object* myObject = new test_object(); - test_object_p pointer = test_object_p(myObject, story); + test_object* myObject = new test_object(); + test_object_p pointer = test_object_p(myObject, story); WHEN("we create a copy") { { test_object_p ref = pointer; - THEN("there should still only be one object") - { - REQUIRE(test_object::num_objects == 1); - } + THEN("there should still only be one object") { REQUIRE(test_object::num_objects == 1); } } WHEN("that copy goes out of scope") { - THEN("we should still have one object") - { - REQUIRE(test_object::num_objects == 1); - } + THEN("we should still have one object") { REQUIRE(test_object::num_objects == 1); } } } } @@ -101,17 +90,14 @@ SCENARIO("pointers can be copied and assigned", "[pointer]") { { // create object and two pointers - test_object* myObject = new test_object(); - test_object_p pointer = test_object_p(myObject, story); - test_object_p ref = pointer; + test_object* myObject = new test_object(); + test_object_p pointer = test_object_p(myObject, story); + test_object_p ref = pointer; } WHEN("both go out of scope") { - THEN("we should have zero instances") - { - REQUIRE(test_object::num_objects == 0); - } + THEN("we should have zero instances") { REQUIRE(test_object::num_objects == 0); } } } @@ -119,40 +105,31 @@ SCENARIO("pointers can be copied and assigned", "[pointer]") { { // create object and two pointers - test_object* myObject = new test_object(); - test_object_p pointer = test_object_p(myObject, story); - test_object_p ref = test_object_p(pointer); + test_object* myObject = new test_object(); + test_object_p pointer = test_object_p(myObject, story); + test_object_p ref = test_object_p(pointer); } WHEN("both go out of scope") { - THEN("we should have zero instances") - { - REQUIRE(test_object::num_objects == 0); - } + THEN("we should have zero instances") { REQUIRE(test_object::num_objects == 0); } } } GIVEN("two pointers to different objects") { - test_object* myObject = new test_object(); - test_object* myOtherObject = new test_object(); - test_object_p pointerA = test_object_p(myObject, story); - test_object_p pointerB = test_object_p(myOtherObject, story); + test_object* myObject = new test_object(); + test_object* myOtherObject = new test_object(); + test_object_p pointerA = test_object_p(myObject, story); + test_object_p pointerB = test_object_p(myOtherObject, story); - THEN("we should have two instances") - { - REQUIRE(test_object::num_objects == 2); - } + THEN("we should have two instances") { REQUIRE(test_object::num_objects == 2); } WHEN("we assign one pointer to the other") { pointerB = pointerA; - THEN("we should only have one object") - { - REQUIRE(test_object::num_objects == 1); - } + THEN("we should only have one object") { REQUIRE(test_object::num_objects == 1); } } } @@ -160,32 +137,26 @@ SCENARIO("pointers can be copied and assigned", "[pointer]") story = nullptr; } -SCENARIO("pointers become invalid when the story dies", "[pointer]") +SCENARIO("pointers become invalid when the story dies", "[pointer][unit][internals]") { // ref_block normally held by the story - ref_block* story = new ref_block(); + ref_block* story = new ref_block(); story->references = 1; GIVEN("a pointer") { - test_object* myObject = new test_object(); - test_object_p pointer = test_object_p(myObject, story); + test_object* myObject = new test_object(); + test_object_p pointer = test_object_p(myObject, story); - THEN("it should be valid") - { - REQUIRE(pointer); - } + THEN("it should be valid") { REQUIRE(pointer); } WHEN("the story becomes invalid") { story->valid = false; - THEN("the pointer should be invalid") - { - REQUIRE(!pointer); - } + THEN("the pointer should be invalid") { REQUIRE(! pointer); } } } delete story; -} \ No newline at end of file +} diff --git a/inkcpp_test/README.md b/inkcpp_test/README.md new file mode 100644 index 00000000..204cbcbc --- /dev/null +++ b/inkcpp_test/README.md @@ -0,0 +1,101 @@ +# inkcpp_test + +Catch2-based test suite for inkcpp. Tests are written in BDD style using +`SCENARIO` / `GIVEN` / `WHEN` / `AND_WHEN` / `THEN` / `AND_THEN` macros. + +## Running tests + +```sh +# inside the build directory +# Run all tests +ctest -C Release + +# Run test without bindings +ctest -V -C Release -R UnitTests +# or +inkcpp_test\Release\inkcpp_test.exe +# or inkcpp_test\Debug\inkcpp_test.exe +# or inkcpp_test\inkcpp_test.exe +# dependent on your build system + + +# Run a single test by scenario name +inkcpp_test.exe "Scenario: UE example story snapshot migratability" + +# Run all tests matching a tag expression +inkcpp_test.exe "[migration]" +inkcpp_test.exe "[regression]" +inkcpp_test.exe "[migration][integration]" +``` + +## Tag scheme + +Every `SCENARIO` is tagged along three independent dimensions. Tags compose +freely, so you can narrow a run to any intersection (e.g. `[regression][runtime]` +re-runs only runtime regression tests). + +| Dimension | Purpose | Values | +|---------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Feature** | What capability is under test | `[array]`, `[callstack]`, `[choices]`, `[compiler]`, `[external-functions]`, `[globals]`, `[glue]`, `[labels]`, `[list-matching]`, `[lists]`, `[migration]`, `[move-to]`, `[observer]`, `[output]`, `[pointer]`, `[restorable]`, `[snapshot]`, `[stack]`, `[tags]`, `[utf-8]`, `[values]` | +| **Component** | Which subsystem owns the behaviour | `[compiler]`, `[internals]`, `[runtime]` | +| **Type** | What kind of test it is | `[integration]`, `[regression]`, `[unit]` | + +### Feature tags + +| Tag | What it covers | +|------------------------|-----------------------------------------------------------------| +| `[array]` | `allocated_restorable_array` internals | +| `[callstack]` | Thread forking and collapse on the internal callstack | +| `[choices]` | Choice presentation, selection, and bracketed choice edge cases | +| `[compiler]` | Ink JSON → binary compilation | +| `[external-functions]` | Binding and calling external (C++) functions from Ink | +| `[globals]` | Global variable get/set and observer callbacks | +| `[glue]` | Glue (`<>`) joining lines across knots and functions | +| `[labels]` | Label-based conditional choices | +| `[list-matching]` | Hungarian solver and list similarity distance functions | +| `[lists]` | Ink list creation, modification, and iteration | +| `[migration]` | Snapshot migration between story versions | +| `[move-to]` | `runner::move_to()` — jumping to named knots at runtime | +| `[observer]` | Variable observer callbacks (typed, generic, ping, new/old) | +| `[output]` | Line output formatting, newlines, whitespace, and glue | +| `[pointer]` | `story_ptr` reference-counted pointer internals | +| `[regression]` | Tests added to prevent a specific filed bug from recurring | +| `[restorable]` | `restorable` collection save/restore/forget | +| `[snapshot]` | Snapshot creation, serialisation, and restoration | +| `[stack]` | `simple_restorable_stack` push/pop/save/restore | +| `[tags]` | Ink tag reading (global, knot, inline, choice tags) | +| `[utf-8]` | Multi-byte character handling throughout the pipeline | +| `[values]` | Internal `value` type arithmetic and equality | + +### Component tags + +| Tag | What it covers | +|---------------|-----------------------------------------------------------------------| +| `[compiler]` | `ink::compiler` — Ink JSON → inkcpp binary | +| `[internals]` | `ink::runtime::internal` — data structures not part of the public API | +| `[runtime]` | `ink::runtime` — story, runner, globals public API | + +### Type tags + +| Tag | What it covers | +|-----------------|---------------------------------------------------------------------------------------| +| `[integration]` | Tests that exercise multiple components together (e.g. compiler + runtime + snapshot) | +| `[regression]` | Tests tied to a specific GitHub issue, named `_ #NNN` in the scenario | +| `[unit]` | Tests of a single class or function in isolation, usually with no story file | + +## BDD conventions + +- **`GIVEN`** — sets up the state of the world (story loaded, globals created, etc.). + Contains no assertions. +- **`WHEN`** — performs a single action (run a line, make a choice, take a snapshot). + Contains no assertions. +- **`AND_WHEN`** — a dependent action that follows a prior `WHEN`. Placed *inside* the + `WHEN` it depends on. +- **`THEN`** — asserts one observable outcome. Contains no story-advancing calls. +- **`AND_THEN`** — an additional assertion that belongs to the same observable moment. + +Multiple independent `WHEN` blocks inside one `GIVEN` are run as separate test +paths — Catch2 re-enters the `GIVEN` once per `WHEN`. + +For further reference see [`test-cases-and-sections.md`](https://github.com/catchorg/Catch2/blob/devel/docs/test-cases-and-sections.md) +in the repository root. diff --git a/inkcpp_test/Restorable.cpp b/inkcpp_test/Restorable.cpp index 5cb43c6c..7d43ea39 100644 --- a/inkcpp_test/Restorable.cpp +++ b/inkcpp_test/Restorable.cpp @@ -4,7 +4,7 @@ using ink::runtime::internal::restorable; -SCENARIO("a restorable collection can operate like a stack", "[restorable]") +SCENARIO("a restorable collection can operate like a stack", "[restorable][unit][internals]") { GIVEN("an empty restorable collection") { @@ -120,7 +120,10 @@ void ForgetAndVerifyStack( } } -SCENARIO("a collection can be restored no matter how many times you push or pop", "[restorable]") +SCENARIO( + "a collection can be restored no matter how many times you push or pop", + "[restorable][unit][internals]" +) { // Create the collection constexpr size_t size = 128; @@ -186,7 +189,7 @@ SCENARIO("a collection can be restored no matter how many times you push or pop" } } -SCENARIO("saving does not disrupt iteration", "[restorable]") +SCENARIO("saving does not disrupt iteration", "[restorable][unit][internals]") { // Create the collection constexpr size_t size = 128; @@ -271,7 +274,7 @@ SCENARIO("saving does not disrupt iteration", "[restorable]") } } -SCENARIO("save points can be forgotten", "[restorable]") +SCENARIO("save points can be forgotten", "[restorable][unit][internals]") { // Create the collection constexpr size_t size = 128; diff --git a/inkcpp_test/SpaceAfterBracketChoice.cpp b/inkcpp_test/SpaceAfterBracketChoice.cpp index c4e692c6..9a908d35 100644 --- a/inkcpp_test/SpaceAfterBracketChoice.cpp +++ b/inkcpp_test/SpaceAfterBracketChoice.cpp @@ -7,35 +7,35 @@ using namespace ink::runtime; -SCENARIO("a story with bracketed choices and spaces can choose correctly", "[choices]") +SCENARIO("a story with bracketed choices and spaces can choose correctly", "[choices][runtime]") { - GIVEN("a story with line breaks") + GIVEN("a story with bracketed choices") { std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "ChoiceBracketStory.bin")}; runner thread = ink->new_runner(); thread->getall(); - WHEN("start thread") + + WHEN("the story is at the first choice point") { - THEN("thread has choices") - { - thread->getall(); - REQUIRE(thread->has_choices()); - } - WHEN("choose choice 1") + thread->getall(); + + THEN("choices are available") { REQUIRE(thread->has_choices()); } + + AND_WHEN("choice 1 is made") { thread->choose(0); thread->getall(); - THEN("still has choices") - { - thread->getall(); - REQUIRE(thread->has_choices()); - } + thread->getall(); + + THEN("choices are still available after choice 1") { REQUIRE(thread->has_choices()); } } - WHEN("choose choice 2") + + AND_WHEN("choice 2 is made") { thread->choose(1); thread->getall(); - THEN("still has choices") { REQUIRE(thread->has_choices()); } + + THEN("choices are still available after choice 2") { REQUIRE(thread->has_choices()); } } } } diff --git a/inkcpp_test/Stack.cpp b/inkcpp_test/Stack.cpp index e0bc8c22..da78413b 100644 --- a/inkcpp_test/Stack.cpp +++ b/inkcpp_test/Stack.cpp @@ -5,14 +5,14 @@ using ink::runtime::internal::simple_restorable_stack; template -using fixed_restorable_stack = ink::runtime::internal::managed_restorable_stack; +using fixed_restorable_stack = ink::runtime::internal::managed_restorable_stack; template void stack_matches(const simple_restorable_stack& stack, T (&expected)[SIZE]) { // Iterate stack and check that it matches the expected array - int count = 0; - const int* iter = nullptr; + int count = 0; + const int* iter = nullptr; while (stack.iter(iter)) { REQUIRE(count < SIZE); REQUIRE(*iter == expected[SIZE - count - 1]); @@ -20,47 +20,46 @@ void stack_matches(const simple_restorable_stack& stack, T (&expected)[SIZE]) } } -SCENARIO("simple_restorable_stack can be pushed and popped", "[stack]") { +SCENARIO("simple_restorable_stack can be pushed and popped", "[stack][unit][internals]") +{ - GIVEN("An empty stack") { + GIVEN("An empty stack") + { fixed_restorable_stack stack(~0); - WHEN("items are added") { + WHEN("items are added") + { stack.push(1); stack.push(2); stack.push(3); - THEN("the size has increased") { - REQUIRE(!stack.empty()); + THEN("the size has increased") + { + REQUIRE(! stack.empty()); REQUIRE(stack.size() == 3); } - THEN("the correct item is at the top") { - REQUIRE(stack.top() == 3); - } + THEN("the correct item is at the top") { REQUIRE(stack.top() == 3); } - THEN("we can iterate the stack") { - int expected[] = { 1, 2, 3 }; + THEN("we can iterate the stack") + { + int expected[] = {1, 2, 3}; stack_matches(stack, expected); } - WHEN("they are popped") { + WHEN("they are popped") + { int pop = stack.pop(); - THEN("the correct item is popped") { - REQUIRE(pop == 3); - } + THEN("the correct item is popped") { REQUIRE(pop == 3); } - THEN("there are fewer items") { - REQUIRE(stack.size() == 2); - } + THEN("there are fewer items") { REQUIRE(stack.size() == 2); } - THEN("there is a new item on top") { - REQUIRE(stack.top() == 2); - } + THEN("there is a new item on top") { REQUIRE(stack.top() == 2); } - THEN("we can still iterate") { - int expected[] = { 1, 2 }; + THEN("we can still iterate") + { + int expected[] = {1, 2}; stack_matches(stack, expected); } } @@ -68,82 +67,92 @@ SCENARIO("simple_restorable_stack can be pushed and popped", "[stack]") { } } -SCENARIO("simple_restorable_stack supports save/restore", "[stack]") { - GIVEN("a stack with a few items that has been saved") { +SCENARIO("simple_restorable_stack supports save/restore", "[stack][unit][internals]") +{ + GIVEN("a stack with a few items that has been saved") + { fixed_restorable_stack stack(~0); - stack.push(1); + stack.push(1); stack.push(2); stack.push(3); stack.save(); auto check_restore = [&stack]() { - WHEN("we restore the stack") { + WHEN("we restore the stack") + { stack.restore(); - THEN("we should be back to having 4 items") { - REQUIRE(stack.size() == 3); - } + THEN("we should be back to having 4 items") { REQUIRE(stack.size() == 3); } - THEN("they should match exactly the original items") { - int expected[] = { 1, 2, 3 }; + THEN("they should match exactly the original items") + { + int expected[] = {1, 2, 3}; stack_matches(stack, expected); } } }; - WHEN("new items are pushed") { + WHEN("new items are pushed") + { stack.push(4); - THEN("the pushed item is on top") { - REQUIRE(stack.top() == 4); - } - THEN("the size has increased") { - REQUIRE(stack.size() == 4); - } + THEN("the pushed item is on top") { REQUIRE(stack.top() == 4); } + THEN("the size has increased") { REQUIRE(stack.size() == 4); } - WHEN("the state is restored") { + WHEN("the state is restored") + { stack.restore(); - THEN("the original item is back on top") { + THEN("the original item is back on top") + { REQUIRE(stack.size() == 3); REQUIRE(stack.top() == 3); } } - WHEN("the state is finalized") { + WHEN("the state is finalized") + { stack.forget(); - THEN("the new item is still on top") { + THEN("the new item is still on top") + { REQUIRE(stack.size() == 4); REQUIRE(stack.top() == 4); } } } - WHEN("items are popped") { - stack.pop(); stack.pop(); + WHEN("items are popped") + { + stack.pop(); + stack.pop(); - THEN("the stack has shrunk") { + THEN("the stack has shrunk") + { REQUIRE(stack.size() == 1); REQUIRE(stack.top() == 1); } - WHEN("more items are pushed") { + WHEN("more items are pushed") + { stack.push(90); stack.push(91); - THEN("the stack has the correct size") { + THEN("the stack has the correct size") + { REQUIRE(stack.size() == 3); REQUIRE(stack.top() == 91); } - THEN("iteration only covers items that ought to be in the stack") { - int expected[] = { 1, 90, 91 }; + THEN("iteration only covers items that ought to be in the stack") + { + int expected[] = {1, 90, 91}; stack_matches(stack, expected); } - THEN("we can pop all items from the stack") { + THEN("we can pop all items from the stack") + { REQUIRE(stack.pop() == 91); REQUIRE(stack.pop() == 90); REQUIRE(stack.pop() == 1); @@ -157,7 +166,7 @@ SCENARIO("simple_restorable_stack supports save/restore", "[stack]") { // Push more items stack.push(100); stack.push(101); - int expected[] = { 100, 101 }; + int expected[] = {100, 101}; stack_matches(stack, expected); // Check that we can still restore @@ -168,17 +177,21 @@ SCENARIO("simple_restorable_stack supports save/restore", "[stack]") { // Check that the stack can restore check_restore(); - THEN("we can finalize the stack") { + THEN("we can finalize the stack") + { stack.forget(); - int expected[] = { 1, 90, 91 }; + int expected[] = {1, 90, 91}; stack_matches(stack, expected); } - WHEN("we pop them back off") { - stack.pop(); stack.pop(); + WHEN("we pop them back off") + { + stack.pop(); + stack.pop(); - THEN("the correct entry is on top") { + THEN("the correct entry is on top") + { REQUIRE(stack.top() == 1); REQUIRE(stack.size() == 1); } @@ -201,7 +214,7 @@ SCENARIO("simple_restorable_stack supports save/restore", "[stack]") { THEN("the stack should be considered 'empty'") { - REQUIRE(stack.size() == 0); + REQUIRE(stack.size() == 0); REQUIRE(stack.empty()); } } @@ -218,7 +231,6 @@ SCENARIO("simple_restorable_stack supports save/restore", "[stack]") { REQUIRE(stack.size() == 0); REQUIRE(stack.empty()); } - } } } diff --git a/inkcpp_test/Tags.cpp b/inkcpp_test/Tags.cpp index 17ffb06d..02e425d9 100644 --- a/inkcpp_test/Tags.cpp +++ b/inkcpp_test/Tags.cpp @@ -10,291 +10,317 @@ using namespace ink::runtime; -SCENARIO("tags", "[ahf]") +SCENARIO("tags", "[tags][runtime]") { - std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "AHF.bin")}; - runner thread = ink->new_runner(); - thread->move_to(ink::hash_string("test_knot")); - while (thread->can_continue()) { - auto line = thread->getline(); + GIVEN("a story moved to test_knot") + { + std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "AHF.bin")}; + runner thread = ink->new_runner(); + thread->move_to(ink::hash_string("test_knot")); + + WHEN("all lines are consumed") + { + while (thread->can_continue()) { + thread->getline(); + } + + THEN("the thread cannot continue") { REQUIRE(thread->can_continue() == false); } + } } - REQUIRE(thread->can_continue() == false); } -SCENARIO("run story with tags", "[tags][story]") +SCENARIO("run story with tags", "[tags][runtime]") { GIVEN("a story with tags") { - std::unique_ptr _ink{story::from_file(INK_TEST_RESOURCE_DIR "TagsStory.bin")}; - runner _thread = _ink->new_runner(); - WHEN("starting the thread") + std::unique_ptr ink{story::from_file(INK_TEST_RESOURCE_DIR "TagsStory.bin")}; + runner thread = ink->new_runner(); + + THEN("initially there are no tags and the runner has not moved to any knot") { - CHECK_FALSE(_thread->has_tags()); - CHECK(_thread->get_current_knot() == 0); + CHECK_FALSE(thread->has_tags()); + CHECK(thread->get_current_knot() == 0); } - WHEN("on the first line") + + WHEN("the first line is read") { - CHECK(_thread->getline() == "First line has global tags only\n"); - THEN("it has the global tags") + std::string line = thread->getline(); + + THEN("the output is correct and only global tags are present") { - CHECK(_thread->get_current_knot() == ink::hash_string("global_tags_only")); - CHECK(_thread->has_global_tags()); - CHECK_FALSE(_thread->has_knot_tags()); - CHECK(_thread->has_tags()); - REQUIRE(_thread->num_global_tags() == 1); - CHECK(std::string(_thread->get_global_tag(0)) == "global_tag"); - REQUIRE(_thread->num_tags() == 1); - CHECK(std::string(_thread->get_tag(0)) == "global_tag"); + REQUIRE(line == "First line has global tags only\n"); + CHECK(thread->get_current_knot() == ink::hash_string("global_tags_only")); + CHECK(thread->has_global_tags()); + CHECK_FALSE(thread->has_knot_tags()); + CHECK(thread->has_tags()); + REQUIRE(thread->num_global_tags() == 1); + CHECK(std::string(thread->get_global_tag(0)) == "global_tag"); + REQUIRE(thread->num_tags() == 1); + CHECK(std::string(thread->get_tag(0)) == "global_tag"); } } - WHEN("on the second line") + + WHEN("the second line is read") { - _thread->getline(); - CHECK(_thread->getline() == "Second line has one tag\n"); - THEN("it has one tag") + // skip line 1 + thread->getline(); + std::string line = thread->getline(); + + THEN("the output is correct and one inline tag is present") { - CHECK(_thread->get_current_knot() == ink::hash_string("global_tags_only")); - CHECK(_thread->has_tags()); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->num_knot_tags() == 0); - REQUIRE(_thread->num_tags() == 1); - CHECK(std::string(_thread->get_tag(0)) == "tagged"); + REQUIRE(line == "Second line has one tag\n"); + CHECK(thread->get_current_knot() == ink::hash_string("global_tags_only")); + CHECK(thread->has_tags()); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->num_knot_tags() == 0); + REQUIRE(thread->num_tags() == 1); + CHECK(std::string(thread->get_tag(0)) == "tagged"); } } - WHEN("on the third line") + + WHEN("the third line is read") { - _thread->getline(); - _thread->getline(); - CHECK(_thread->getline() == "Third line has two tags\n"); - THEN("it has two tags") + // skip lines 1-2 + for (int i = 0; i < 2; ++i) { + thread->getline(); + } + std::string line = thread->getline(); + + THEN("the output is correct and two inline tags are present") { - CHECK(_thread->get_current_knot() == ink::hash_string("global_tags_only")); - CHECK(_thread->has_tags()); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->num_knot_tags() == 0); - REQUIRE(_thread->num_tags() == 2); - CHECK(std::string(_thread->get_tag(0)) == "tag next line"); - CHECK(std::string(_thread->get_tag(1)) == "more tags"); + REQUIRE(line == "Third line has two tags\n"); + CHECK(thread->get_current_knot() == ink::hash_string("global_tags_only")); + CHECK(thread->has_tags()); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->num_knot_tags() == 0); + REQUIRE(thread->num_tags() == 2); + CHECK(std::string(thread->get_tag(0)) == "tag next line"); + CHECK(std::string(thread->get_tag(1)) == "more tags"); } } - WHEN("on the fourth line") + + WHEN("the fourth line is read") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - CHECK(_thread->getline() == "Fourth line has three tags\n"); + // skip lines 1-3 + for (int i = 0; i < 3; ++i) { + thread->getline(); + } + std::string line = thread->getline(); - THEN("it has three tags") + THEN("the output is correct and three inline tags are present") { - CHECK(_thread->get_current_knot() == ink::hash_string("global_tags_only")); - CHECK(_thread->has_tags()); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->num_knot_tags() == 0); - REQUIRE(_thread->num_tags() == 3); - CHECK(std::string(_thread->get_tag(0)) == "above"); - CHECK(std::string(_thread->get_tag(1)) == "side"); - CHECK(std::string(_thread->get_tag(2)) == "across"); + REQUIRE(line == "Fourth line has three tags\n"); + CHECK(thread->get_current_knot() == ink::hash_string("global_tags_only")); + CHECK(thread->has_tags()); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->num_knot_tags() == 0); + REQUIRE(thread->num_tags() == 3); + CHECK(std::string(thread->get_tag(0)) == "above"); + CHECK(std::string(thread->get_tag(1)) == "side"); + CHECK(std::string(thread->get_tag(2)) == "across"); } } - WHEN("entering a knot") + + WHEN("the knot entry line is read") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - CHECK(_thread->getline() == "Hello\n"); - THEN("it has four tags") + // skip lines 1-4 + for (int i = 0; i < 4; ++i) { + thread->getline(); + } + std::string line = thread->getline(); + + THEN("the output is correct and knot tags are combined with global and inline tags") { - CHECK(_thread->get_current_knot() == ink::hash_string("start")); - CHECK(_thread->has_tags()); - CHECK(_thread->num_global_tags() == 1); - REQUIRE(_thread->num_tags() == 4); - CHECK(std::string(_thread->get_tag(0)) == "knot_tag_start"); - CHECK(std::string(_thread->get_tag(1)) == "second_knot_tag_start"); - CHECK(std::string(_thread->get_tag(2)) == "third_knot_tag"); - CHECK(std::string(_thread->get_tag(3)) == "output_tag_h"); - REQUIRE(_thread->num_knot_tags() == 3); - CHECK(std::string(_thread->get_knot_tag(0)) == "knot_tag_start"); - CHECK(std::string(_thread->get_knot_tag(1)) == "second_knot_tag_start"); - CHECK(std::string(_thread->get_knot_tag(2)) == "third_knot_tag"); + REQUIRE(line == "Hello\n"); + CHECK(thread->get_current_knot() == ink::hash_string("start")); + CHECK(thread->has_tags()); + CHECK(thread->num_global_tags() == 1); + REQUIRE(thread->num_tags() == 4); + CHECK(std::string(thread->get_tag(0)) == "knot_tag_start"); + CHECK(std::string(thread->get_tag(1)) == "second_knot_tag_start"); + CHECK(std::string(thread->get_tag(2)) == "third_knot_tag"); + CHECK(std::string(thread->get_tag(3)) == "output_tag_h"); + REQUIRE(thread->num_knot_tags() == 3); + CHECK(std::string(thread->get_knot_tag(0)) == "knot_tag_start"); + CHECK(std::string(thread->get_knot_tag(1)) == "second_knot_tag_start"); + CHECK(std::string(thread->get_knot_tag(2)) == "third_knot_tag"); } } - WHEN("on the next line") + + WHEN("the line after the knot header is read") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - CHECK(_thread->getline() == "Second line has no tags\n"); - THEN("it has no tags") + // skip lines 1-5 + for (int i = 0; i < 5; ++i) { + thread->getline(); + } + std::string line = thread->getline(); + + THEN("the output is correct and no inline tags are present") { - CHECK(_thread->get_current_knot() == ink::hash_string("start")); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->num_knot_tags() == 3); - CHECK_FALSE(_thread->has_tags()); - REQUIRE(_thread->num_tags() == 0); + REQUIRE(line == "Second line has no tags\n"); + CHECK(thread->get_current_knot() == ink::hash_string("start")); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->num_knot_tags() == 3); + CHECK_FALSE(thread->has_tags()); + REQUIRE(thread->num_tags() == 0); } } - WHEN("at the first choice list") + + WHEN("all six lines are consumed and the first choice list appears") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - CHECK_FALSE(_thread->can_continue()); - - REQUIRE(std::distance(_thread->begin(), _thread->end()) == 2); - auto choice_list = _thread->begin(); - - THEN("check tags on choices") + for (int i = 0; i < 6; ++i) { + thread->getline(); + } + auto choices = thread->begin(); + + THEN("the runner is at a choice point with two correctly tagged options") { - CHECK(_thread->get_current_knot() == ink::hash_string("start")); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->num_knot_tags() == 3); - CHECK(std::string(choice_list[0].text()) == "a"); - CHECK_FALSE(choice_list[0].has_tags()); - REQUIRE(choice_list[0].num_tags() == 0); - - CHECK(std::string(choice_list[1].text()) == "b"); - CHECK(choice_list[1].has_tags()); - REQUIRE(choice_list[1].num_tags() == 2); - CHECK(std::string(choice_list[1].get_tag(0)) == "choice_tag_b"); - CHECK(std::string(choice_list[1].get_tag(1)) == "choice_tag_b_2"); + CHECK_FALSE(thread->can_continue()); + CHECK(thread->get_current_knot() == ink::hash_string("start")); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->num_knot_tags() == 3); + REQUIRE(std::distance(thread->begin(), thread->end()) == 2); + + CHECK(std::string(choices[0].text()) == "a"); + CHECK_FALSE(choices[0].has_tags()); + REQUIRE(choices[0].num_tags() == 0); + + CHECK(std::string(choices[1].text()) == "b"); + CHECK(choices[1].has_tags()); + REQUIRE(choices[1].num_tags() == 2); + CHECK(std::string(choices[1].get_tag(0)) == "choice_tag_b"); + CHECK(std::string(choices[1].get_tag(1)) == "choice_tag_b_2"); } } - WHEN("selecting the second choice") + + WHEN("choice 'b' is selected at the first choice list") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->choose(1); - - CHECK(_thread->getline() == "Knot2\n"); - THEN("it has two tags") + for (int i = 0; i < 6; ++i) { + thread->getline(); + } + thread->choose(1); + std::string line = thread->getline(); + + THEN("the output is correct and the new knot's tags are reflected") { - CHECK(_thread->get_current_knot() == ink::hash_string("knot2.sub")); - INFO(_thread->get_knot_tag(0)); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->has_tags()); - REQUIRE(_thread->num_tags() == 2); - CHECK(std::string(_thread->get_tag(0)) == "knot_tag_2"); - CHECK(std::string(_thread->get_tag(1)) == "output_tag_k"); - CHECK(_thread->has_knot_tags()); - REQUIRE(_thread->num_knot_tags() == 1); - CHECK(std::string(_thread->get_knot_tag(0)) == "knot_tag_2"); + REQUIRE(line == "Knot2\n"); + CHECK(thread->get_current_knot() == ink::hash_string("knot2.sub")); + INFO(thread->get_knot_tag(0)); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->has_tags()); + REQUIRE(thread->num_tags() == 2); + CHECK(std::string(thread->get_tag(0)) == "knot_tag_2"); + CHECK(std::string(thread->get_tag(1)) == "output_tag_k"); + CHECK(thread->has_knot_tags()); + REQUIRE(thread->num_knot_tags() == 1); + CHECK(std::string(thread->get_knot_tag(0)) == "knot_tag_2"); } } - WHEN("jumping to a knot") + + WHEN("the runner jumps directly to knot2") { - _thread->move_to(ink::hash_string("knot2")); - REQUIRE(_thread->getline() == "Knot2\n"); - THEN("global tags are missing") + thread->move_to(ink::hash_string("knot2")); + std::string line = thread->getline(); + + THEN("the output is correct and global tags are absent since the global section was skipped") { - CHECK(_thread->get_current_knot() == ink::hash_string("knot2.sub")); - CHECK(_thread->num_global_tags() == 0); - CHECK(_thread->has_tags()); - REQUIRE(_thread->num_tags() == 2); - CHECK(std::string(_thread->get_tag(0)) == "knot_tag_2"); - CHECK(std::string(_thread->get_tag(1)) == "output_tag_k"); - CHECK(_thread->has_knot_tags()); - REQUIRE(_thread->num_knot_tags() == 1); - CHECK(std::string(_thread->get_knot_tag(0)) == "knot_tag_2"); + REQUIRE(line == "Knot2\n"); + CHECK(thread->get_current_knot() == ink::hash_string("knot2.sub")); + CHECK(thread->num_global_tags() == 0); + CHECK(thread->has_tags()); + REQUIRE(thread->num_tags() == 2); + CHECK(std::string(thread->get_tag(0)) == "knot_tag_2"); + CHECK(std::string(thread->get_tag(1)) == "output_tag_k"); + CHECK(thread->has_knot_tags()); + REQUIRE(thread->num_knot_tags() == 1); + CHECK(std::string(thread->get_knot_tag(0)) == "knot_tag_2"); } } - WHEN("at the second choice list") + + WHEN("choice 'b' is selected and the second choice list appears") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->choose(1); - _thread->getline(); - CHECK(! _thread->can_continue()); - - REQUIRE(std::distance(_thread->begin(), _thread->end()) == 3); - auto choice_list = _thread->begin(); - - THEN("check tags on choices") + for (int i = 0; i < 6; ++i) { + thread->getline(); + } + thread->choose(1); + thread->getline(); // consume "Knot2" + auto choices = thread->begin(); + + THEN("three choices are present with correct tags") { - CHECK(_thread->get_current_knot() == ink::hash_string("knot2.sub")); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->num_knot_tags() == 1); - CHECK(_thread->num_tags() == 2); - CHECK(std::string(choice_list[0].text()) == "e"); - CHECK_FALSE(choice_list[0].has_tags()); - REQUIRE(choice_list[0].num_tags() == 0); - - CHECK(std::string(choice_list[1].text()) == "f with detail"); - CHECK(choice_list[1].has_tags()); - REQUIRE(choice_list[1].num_tags() == 4); - CHECK(std::string(choice_list[1].get_tag(0)) == "shared_tag"); - CHECK(std::string(choice_list[1].get_tag(1)) == "shared_tag_2"); - CHECK(std::string(choice_list[1].get_tag(2)) == "choice_tag"); - CHECK(std::string(choice_list[1].get_tag(3)) == "choice_tag_2"); - - CHECK(std::string(choice_list[2].text()) == "g"); - CHECK(choice_list[2].has_tags()); - REQUIRE(choice_list[2].num_tags() == 1); - CHECK(std::string(choice_list[2].get_tag(0)) == "choice_tag_g"); + CHECK_FALSE(thread->can_continue()); + CHECK(thread->get_current_knot() == ink::hash_string("knot2.sub")); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->num_knot_tags() == 1); + CHECK(thread->num_tags() == 2); + REQUIRE(std::distance(thread->begin(), thread->end()) == 3); + + CHECK(std::string(choices[0].text()) == "e"); + CHECK_FALSE(choices[0].has_tags()); + REQUIRE(choices[0].num_tags() == 0); + + CHECK(std::string(choices[1].text()) == "f with detail"); + CHECK(choices[1].has_tags()); + REQUIRE(choices[1].num_tags() == 4); + CHECK(std::string(choices[1].get_tag(0)) == "shared_tag"); + CHECK(std::string(choices[1].get_tag(1)) == "shared_tag_2"); + CHECK(std::string(choices[1].get_tag(2)) == "choice_tag"); + CHECK(std::string(choices[1].get_tag(3)) == "choice_tag_2"); + + CHECK(std::string(choices[2].text()) == "g"); + CHECK(choices[2].has_tags()); + REQUIRE(choices[2].num_tags() == 1); + CHECK(std::string(choices[2].get_tag(0)) == "choice_tag_g"); } } - WHEN("selecting the choice with shared tags") + + WHEN("choice 'b' then choice 'f with detail' is selected") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->choose(1); - _thread->getline(); - _thread->choose(1); - - REQUIRE(_thread->getline() == "f and content\n"); - THEN("it has four tags") + for (int i = 0; i < 6; ++i) { + thread->getline(); + } + thread->choose(1); + thread->getline(); // "Knot2" + thread->choose(1); + std::string line = thread->getline(); + + THEN("the output is correct and four tags combining shared and content tags are present") { - CHECK(_thread->get_current_knot() == ink::hash_string("knot2.sub")); - CHECK(_thread->num_global_tags() == 1); - CHECK(_thread->num_knot_tags() == 1); - CHECK(_thread->has_tags()); - REQUIRE(_thread->num_tags() == 4); - CHECK(std::string(_thread->get_tag(0)) == "shared_tag"); - CHECK(std::string(_thread->get_tag(1)) == "shared_tag_2"); - CHECK(std::string(_thread->get_tag(2)) == "content_tag"); - CHECK(std::string(_thread->get_tag(3)) == "content_tag_2"); + REQUIRE(line == "f and content\n"); + CHECK(thread->get_current_knot() == ink::hash_string("knot2.sub")); + CHECK(thread->num_global_tags() == 1); + CHECK(thread->num_knot_tags() == 1); + CHECK(thread->has_tags()); + REQUIRE(thread->num_tags() == 4); + CHECK(std::string(thread->get_tag(0)) == "shared_tag"); + CHECK(std::string(thread->get_tag(1)) == "shared_tag_2"); + CHECK(std::string(thread->get_tag(2)) == "content_tag"); + CHECK(std::string(thread->get_tag(3)) == "content_tag_2"); } } - WHEN("on the last line") + + WHEN("the story is played through to the final line") { - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->getline(); - _thread->choose(1); - _thread->getline(); - _thread->choose(1); - _thread->getline(); - CHECK(_thread->getline() == "out\n"); - THEN("it has one tag") + for (int i = 0; i < 6; ++i) { + thread->getline(); + } + thread->choose(1); + thread->getline(); // "Knot2" + thread->choose(1); + thread->getline(); // "f and content" + std::string line = thread->getline(); + + THEN("the output is correct and one closing tag is present alongside global and knot tags") { - CHECK(_thread->get_current_knot() == ink::hash_string("knot2.sub")); - CHECK(_thread->num_global_tags() == 1); - CHECK(std::string(_thread->get_global_tag(0)) == "global_tag"); - REQUIRE(_thread->num_knot_tags() == 1); - CHECK(std::string(_thread->get_knot_tag(0)) == "knot_tag_2"); - CHECK(_thread->has_tags()); - REQUIRE(_thread->num_tags() == 1); - CHECK(std::string(_thread->get_tag(0)) == "close_tag"); + REQUIRE(line == "out\n"); + CHECK(thread->get_current_knot() == ink::hash_string("knot2.sub")); + CHECK(thread->num_global_tags() == 1); + CHECK(std::string(thread->get_global_tag(0)) == "global_tag"); + REQUIRE(thread->num_knot_tags() == 1); + CHECK(std::string(thread->get_knot_tag(0)) == "knot_tag_2"); + CHECK(thread->has_tags()); + REQUIRE(thread->num_tags() == 1); + CHECK(std::string(thread->get_tag(0)) == "close_tag"); } } } diff --git a/inkcpp_test/ThirdTierChoiceAfterBrackets.cpp b/inkcpp_test/ThirdTierChoiceAfterBrackets.cpp index 207baae1..a643c859 100644 --- a/inkcpp_test/ThirdTierChoiceAfterBrackets.cpp +++ b/inkcpp_test/ThirdTierChoiceAfterBrackets.cpp @@ -9,7 +9,7 @@ using namespace ink::runtime; SCENARIO( "a story with a bracketed choice as a second choice, and then a third choice, chooses properly", - "[choices]" + "[choices][runtime]" ) { GIVEN("a story with brackets and nested choices") @@ -18,21 +18,34 @@ SCENARIO( "ThirdTierChoiceAfterBracketsStory.bin")}; runner thread = ink->new_runner(); - WHEN("start thread") + WHEN("the story starts") { - THEN("thread doesn't error") + thread->getall(); + + THEN("the first tier choices are presented") { REQUIRE(thread->has_choices()); } + + AND_WHEN("the first choice is made") { - thread->getall(); - REQUIRE(thread->has_choices()); - thread->choose(0); - thread->getall(); - REQUIRE(thread->has_choices()); - thread->choose(0); - thread->getall(); - REQUIRE(thread->has_choices()); thread->choose(0); thread->getall(); - REQUIRE(! thread->has_choices()); + + THEN("the second tier choices are presented") { REQUIRE(thread->has_choices()); } + + AND_WHEN("the second choice is made") + { + thread->choose(0); + thread->getall(); + + THEN("the third tier choices are presented") { REQUIRE(thread->has_choices()); } + + AND_WHEN("the third choice is made") + { + thread->choose(0); + thread->getall(); + + THEN("the story ends with no further choices") { REQUIRE_FALSE(thread->has_choices()); } + } + } } } } diff --git a/inkcpp_test/UTF8.cpp b/inkcpp_test/UTF8.cpp index 4b2c7beb..732a9c2f 100644 --- a/inkcpp_test/UTF8.cpp +++ b/inkcpp_test/UTF8.cpp @@ -11,7 +11,7 @@ using namespace ink::runtime; -SCENARIO("a story supports UTF-8", "[utf-8]") +SCENARIO("a story supports UTF-8", "[utf-8][compiler][runtime]") { GIVEN("a story with UTF8 characters") { diff --git a/inkcpp_test/Value.cpp b/inkcpp_test/Value.cpp index e2615cfd..56039bc1 100644 --- a/inkcpp_test/Value.cpp +++ b/inkcpp_test/Value.cpp @@ -37,7 +37,7 @@ void cp_str(char* dst, const char* src) *dst = 0; } -SCENARIO("compare concatenated values") +SCENARIO("compare concatenated values", "[values][unit][internals]") { string_table str_table; list_table lst_table{}; diff --git a/inkcpp_test/ink/MigrationBase.ink b/inkcpp_test/ink/MigrationBase.ink index 21f6ab51..c372f1df 100644 --- a/inkcpp_test/ink/MigrationBase.ink +++ b/inkcpp_test/ink/MigrationBase.ink @@ -22,5 +22,6 @@ This is a simple story. - catch {tKeep} {tOld} {TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +{Node1} {OldNode} {Main} Oh. ->DONE diff --git a/inkcpp_test/ink/MigrationChangeNodes.ink b/inkcpp_test/ink/MigrationChangeNodes.ink index 62c7ca4e..50603ada 100644 --- a/inkcpp_test/ink/MigrationChangeNodes.ink +++ b/inkcpp_test/ink/MigrationChangeNodes.ink @@ -14,5 +14,6 @@ This is a simple story. * B - catch {TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> NewNode)} {TURNS_SINCE(-> Main)} +{Node1} {NewNode} {Main} Oh. ->DONE diff --git a/inkcpp_test/ink/MigrationKnotTags.ink b/inkcpp_test/ink/MigrationKnotTags.ink index 9c4037b0..acf68777 100644 --- a/inkcpp_test/ink/MigrationKnotTags.ink +++ b/inkcpp_test/ink/MigrationKnotTags.ink @@ -22,5 +22,6 @@ This is a simple story. - catch {tKeep} {tOld} {TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +{Node1} {OldNode} {Main} Oh. ->DONE diff --git a/inkcpp_test/ink/MigrationTemp.ink b/inkcpp_test/ink/MigrationTemp.ink index 422829f9..23afde11 100644 --- a/inkcpp_test/ink/MigrationTemp.ink +++ b/inkcpp_test/ink/MigrationTemp.ink @@ -22,5 +22,6 @@ This is a simple story. - catch {tKeep} - {tNew} {TURNS_SINCE(-> Node1)} {TURNS_SINCE(-> OldNode)} {TURNS_SINCE(-> Main)} +{Node1} {OldNode} {Main} Oh. ->DONE diff --git a/inkcpp_test/ink/UE_example.ink b/inkcpp_test/ink/UE_example.ink new file mode 100644 index 00000000..71613f8d --- /dev/null +++ b/inkcpp_test/ink/UE_example.ink @@ -0,0 +1,105 @@ +# date:2025.03.22 +LIST Potions = TalkWithAnimals, Invisibility +LIST Clues = Skull, Feather +LIST Knowladge = YellowDress +VAR Inventory = (Skull, TalkWithAnimals) +VAR Health = 100 +LIST StatusConditions = CanTalkWithAniamls, IsInvisible + +-> Mansion.Car + +EXTERNAL transition(to) +=== function transition(to) === +~ return 0 + +=== function walking(to) === +You startk walking to {to}. +~ transition(to) +~ return + +=== Wait +-> DONE + +=== TClues += TSkull + A human skull, why do I have this again? + -> END += TFeather + A Large Feather found insede the dining room, I wounder from which bird it is. + -> END +=== TPotions += TTalkWithAnimals + A potion which allows the consumer to talk with a variaty of animals. Just make sure + your serroundings do not think you are crazy. + {StatusConditions ? (CanTalkWithAniamls): Drinking more of it will not increase the effect.} + + [Put it Back] + You put the potion back into your pouch. + -> END + + {not (StatusConditions ? CanTalkWithAniamls)} [Drink] + ~ StatusConditions += CanTalkWithAniamls + A take a sip. The potion tastes like Hores, it is afull. + -> END += TInvisibility + A potion which allows the consumer to stay unseen for the human eye. Not tested + wtih other speccies. + {StatusConditions ? (IsInvisible): Drinking more of it will not increase the effect.} + + [Put it Back] + You put the potion back into your pouch. + -> END + + {not (StatusConditions ? (IsInvisible))}[Drink] + ~ StatusConditions += IsInvisible + You put a small drop on your thoungh. It feels like liking a battery, such a nice feeling + -> END + +=== Faint +# background:Unconscious +You collapse, the next thing you can remember is how you are given to the ambulance. +No further action for today ... +-> DONE + +=== Mansion += Car +# background:Car +You step outside your car. Its a wired feeling beehing here again. +-> Car_cycle += Car_cycle +# background:Car ++ (look_around)[look around # Type:Idle] + It is a strange day. Despite it beeing spring, the sky is one massive gray soup. # style:Gray + -> Car_cycle ++ [go to the mension] + ~ walking("Mansion.Entrance") + -> Entrance + += Entrance +# background:Mansion +{not Mansion.look_around: + ~ Knowladge += YellowDress + Just in time you are able to see the door, someone with with a yellow summer dress enters it. +} +You're climbing the 56 steps up to the door; high tides are an annoying thing. +-> Entrance_cycle += Entrance_cycle +# background:Mansion ++ (look_around) [look around # Type:Idle ] + While watching around you, <> + {Inventory hasnt Invisibility: + see a small bottle in the Pot next to the door. + -else: + see nothing of intrest + } + -> Entrance_cycle +* {look_around} [Pick up the bottle] + ~ Inventory += Invisibility + You pick up the bottle and inspect it more. It is labeld "Invisible, just this one word written with and edding. + -> Entrance_cycle ++ (knock)[Knock {knock: again?} # {knock: Type:Danger} ] + ~ Health -= 20 + "Ahh", you cry while reaching for the door bell. Saying it was charched would be an understatement. + { Health <= 0: -> Faint} + -> Entrance_cycle ++ {knock && Knowladge ? (YellowDress)} [Inspect the Door] + You just saw someone enter, how did they do not get shoked? + It seems the developr run out of time, maybe in the next version we can continue? + -> Entrance_cycle +-> DONE diff --git a/inkcpp_test/ink/UE_example_v2.ink b/inkcpp_test/ink/UE_example_v2.ink new file mode 100644 index 00000000..89feeab3 --- /dev/null +++ b/inkcpp_test/ink/UE_example_v2.ink @@ -0,0 +1,121 @@ +# date:2025.03.22 +LIST Potions = TalkWithAnimals, Invisibility +LIST Clues = Skull, Feather +LIST Knowladge = YellowDress +VAR Inventory = (Skull, TalkWithAnimals) +VAR Health = 100 +LIST StatusConditions = CanTalkWithAniamls, IsInvisible + +-> Mansion.Car + +EXTERNAL transition(to) +=== function transition(to) === +~ return 0 + +=== function walking(to) === +You startk walking to {to}. +~ transition(to) +~ return + +=== Wait +-> DONE + +=== TClues += TSkull + A human skull, why do I have this again? + -> END += TFeather + A Large Feather found insede the dining room, I wounder from which bird it is. + -> END +=== TPotions += TTalkWithAnimals + A potion which allows the consumer to talk with a variaty of animals. Just make sure + your serroundings do not think you are crazy. + {StatusConditions ? (CanTalkWithAniamls): Drinking more of it will not increase the effect.} + + [Put it Back] + You put the potion back into your pouch. + -> END + + {not (StatusConditions ? CanTalkWithAniamls)} [Drink] + ~ StatusConditions += CanTalkWithAniamls + A take a sip. The potion tastes like Hores, it is afull. + -> END += TInvisibility + A potion which allows the consumer to stay unseen for the human eye. Not tested + wtih other speccies. + {StatusConditions ? (IsInvisible): Drinking more of it will not increase the effect.} + + [Put it Back] + You put the potion back into your pouch. + -> END + + {not (StatusConditions ? (IsInvisible))}[Drink] + ~ StatusConditions += IsInvisible + You put a small drop on your thoungh. It feels like liking a battery, such a nice feeling + -> END + +=== Faint +# background:Unconscious +You collapse, the next thing you can remember is how you are given to the ambulance. +No further action for today ... +-> DONE + +VAR MansionFrontDoorLocked = true +=== Mansion += Car +# background:Car +At first there is anger about the traffic jam and your 1h delay. But your memories of this place let this feeling fade away. +-> Car_cycle += Car_cycle +# background:Car ++ (look_around)[look around # Type:Idle] + It is a strange day. Despite it beeing spring, the sky is one massive gray soup. # style:Gray + -> Car_cycle ++ [go to the mension] + ~ walking("Mansion.Entrance") + -> Entrance + += Entrance +# background:Mansion +You're climbing the 56 steps up to the door; high tides are an annoying thing. +-> Entrance_cycle += Entrance_cycle +# background:Mansion ++ (look_around) [look around # Type:Idle ] + While watching around you, <> + {Inventory hasnt Invisibility: + see a small bottle in the Pot next to the door. + -else: + see nothing of intrest + } + -> Entrance_cycle +* {look_around} [Pick up the bottle] + ~ Inventory += Invisibility + You pick up the bottle and inspect it more. It is labeld "Invisible, just this one word written with and edding. + -> Entrance_cycle ++ (knock)[Knock {knock: again?} # {knock: Type:Danger} ] + ~ Health -= 20 + "Ahh", you cry while reaching for the door bell. Saying it was charched would be an understatement. + { Health <= 0: -> Faint} + -> Entrance_cycle ++ {knock && Knowladge ? YellowDress} [Inspect the Door] + You just saw someone enter, how did they do not get shoked? + -> Entrance_mouse -> + -> Entrance_cycle ++ {not MansionFrontDoorLocked} [Open the Door] + On high alert you press the door handle, it opens. Quite smooth and easy. + You step inside. + ToBeContinued + -> DONE + += Entrance_mouse + Something hushes through a hole beside the door, after you come closer you see it. A little gray mouse, it looks quite eloquent. + + [Ask the mouse for help] + You try to formulate your dilemma and your annoyance about the doorbell. + {StatusConditions ? (CanTalkWithAniamls): + ~ MansionFrontDoorLocked = false + (enter nice conversasion with a picky but helpful mouse) + -else: + The mouse squeaked, then was silent. It seems it tried to answer you, but you are unable to understand it. + } + + A ugly little creature, ignore it. + - ->-> + +-> DONE diff --git a/scripts/bump-version.py b/scripts/bump-version.py new file mode 100644 index 00000000..3ef0ad44 --- /dev/null +++ b/scripts/bump-version.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +"""bump-version.py — Bump the inkcpp project version or add a new Unreal Engine version. + +Usage: + bump-version.py inkcpp + bump-version.py ue + +Examples: + python scripts/bump-version.py inkcpp 0.1.11 + python scripts/bump-version.py ue 5.8 + +inkcpp subcommand updates: + - CMakeLists.txt (project(inkcpp VERSION ...)) + - setup.py (version="...") + +ue subcommand (adds new version, keeps all prior versions in CI): + - unreal/CMakeLists.txt (default INKCPP_UNREAL_TARGET_VERSION + comment) + - .github/workflows/build.yml (Install UE run block + new Upload step) + - .github/workflows/release.yml (download / zip / release artifact lists) + Also warns if Documentation/unreal/InkCPP_DEMO.zip targets a different UE version. +""" + +import json +import re +import sys +import zipfile + +from docopt import docopt +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +ARGS = docopt(__doc__, version="1.0") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _ok(rel_path: str, detail: str = "") -> None: + suffix = f" ({detail})" if detail else "" + print(f" [OK] {rel_path}{suffix}") + + +def _warn(rel_path: str, msg: str) -> None: + print(f" [WARN] {rel_path} — {msg}") + + +def _info(msg: str) -> None: + print(f" [INFO] {msg}") + + +def _read(rel: str) -> tuple[Path, str]: + path = REPO_ROOT / rel + return path, path.read_text(encoding="utf-8") + + +def _write(path: Path, text: str) -> None: + path.write_text(text, encoding="utf-8") + + +# --------------------------------------------------------------------------- +# inkcpp version bump +# --------------------------------------------------------------------------- + +INKCPP_FILES = [ + ( + "CMakeLists.txt", + r"(project\(inkcpp VERSION\s+)\d+\.\d+\.\d+", + r"\g<1>{v}", + ), + ( + "setup.py", + r'(version=")[^"]+(")', + r"\g<1>{v}\2", + ), +] + + +def current_inkcpp_version() -> str: + _, text = _read("CMakeLists.txt") + m = re.search(r"project\(inkcpp VERSION\s+(\d+\.\d+\.\d+)", text) + if not m: + raise RuntimeError("Could not parse inkcpp version from CMakeLists.txt") + return m.group(1) + + +def bump_inkcpp(new_ver: str) -> None: + if not re.fullmatch(r"\d+\.\d+\.\d+", new_ver): + sys.exit(f"ERROR: inkcpp version must be MAJOR.MINOR.PATCH, got: {new_ver!r}") + + old_ver = current_inkcpp_version() + if old_ver == new_ver: + print(f"inkcpp version is already {old_ver} — nothing to do.") + return + + print(f"Bumping inkcpp {old_ver} → {new_ver}\n") + changed = [] + + for rel, pattern, tmpl in INKCPP_FILES: + path, original = _read(rel) + replacement = tmpl.replace("{v}", new_ver) + updated, count = re.subn(pattern, replacement, original) + if count == 0: + _warn(rel, "pattern not matched — skipped") + else: + _write(path, updated) + _ok(rel, f"{count} replacement{'s' if count != 1 else ''}") + changed.append(rel) + + print() + if changed: + files = " ".join(changed) + print("Suggested commit:") + print(f" git add {files}") + print(f' git commit -m "chore: bump inkcpp version {old_ver} → {new_ver}"') + + +# --------------------------------------------------------------------------- +# UE version bump +# --------------------------------------------------------------------------- + + +def current_ue_version() -> str: + _, text = _read("unreal/CMakeLists.txt") + m = re.search(r'set\(INKCPP_UNREAL_TARGET_VERSION\s+"(\d+\.\d+)"', text) + if not m: + raise RuntimeError("Could not parse UE version from unreal/CMakeLists.txt") + return m.group(1) + + +def _update_unreal_cmake(old_ue: str, new_ue: str) -> bool: + rel = "unreal/CMakeLists.txt" + path, text = _read(rel) + + # Update cached default value + text, n1 = re.subn( + r'(set\(INKCPP_UNREAL_TARGET_VERSION\s+")' + re.escape(old_ue) + r'"', + r"\g<1>" + new_ue + '"', + text, + ) + # Update the e.g. comment so it shows the previous default (old_ue) + text, n2 = re.subn( + r'(CACHE STRING "[^"]*e\.g: )[\d.]+(")', + r"\g<1>" + old_ue + r"\2", + text, + ) + + if n1 == 0: + _warn(rel, f"default version {old_ue!r} not found — skipped") + return False + _write(path, text) + _ok(rel, f"default {old_ue} → {new_ue}" + (", comment updated" if n2 else "")) + return True + + +def _update_build_yml(old_ue: str, new_ue: str) -> bool: + rel = ".github/workflows/build.yml" + path, text = _read(rel) + + old_u = old_ue.replace(".", "_") + new_u = new_ue.replace(".", "_") + + # Guard: already present? + if f'INKCPP_UNREAL_TARGET_VERSION="{new_ue}"' in text: + _warn(rel, f"UE {new_ue} already present — skipped") + return False + + # 1. Prepend new cmake build+install pair before the existing first pair + old_cmake_pair = ( + f' cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="{old_ue}" -DINKCPP_UNREAL=ON\n' + f" cmake --install . --config $BUILD_TYPE --prefix comp_unreal_{old_u} --component unreal" + ) + new_cmake_pair = ( + f' cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="{new_ue}" -DINKCPP_UNREAL=ON\n' + f" cmake --install . --config $BUILD_TYPE --prefix comp_unreal_{new_u} --component unreal\n" + ) + if old_cmake_pair not in text: + _warn( + rel, f"cmake install lines for {old_ue} not found — run block not updated" + ) + return False + text = text.replace(old_cmake_pair, new_cmake_pair + old_cmake_pair, 1) + + # 2. Insert new Upload step immediately before the existing Upload UE {old_ue} step + old_upload_anchor = f" - name: Upload UE {old_ue}\n" + new_upload_block = ( + f" - name: Upload UE {new_ue}\n" + f" if: ${{{{ matrix.unreal }}}}\n" + f" uses: actions/upload-artifact@v4\n" + f" with:\n" + f" name: unreal_{new_u}\n" + f" path: build/comp_unreal_{new_u}/\n" + ) + if old_upload_anchor not in text: + _warn(rel, f"'Upload UE {old_ue}' step not found — upload step not inserted") + return False + text = text.replace(old_upload_anchor, new_upload_block + old_upload_anchor, 1) + + _write(path, text) + _ok(rel, f"added cmake pair + Upload UE {new_ue} step") + return True + + +def _update_release_yml(old_ue: str, new_ue: str) -> bool: + rel = ".github/workflows/release.yml" + path, text = _read(rel) + + old_u = old_ue.replace(".", "_") + new_u = new_ue.replace(".", "_") + + # Guard: already present? + if f"unreal_{new_u}" in text: + _warn(rel, f"unreal_{new_u} already present — skipped") + return False + + ok = True + + # 1. Download step: -n unreal_OLD → -n unreal_NEW -n unreal_OLD + old_flag = f"-n unreal_{old_u}" + if old_flag not in text: + _warn(rel, f"download flag '-n unreal_{old_u}' not found") + ok = False + else: + text = text.replace(old_flag, f"-n unreal_{new_u} {old_flag}", 1) + + # 2. Zip for-loop: unreal_OLD → unreal_NEW unreal_OLD (only inside the for line) + for_pat = r"(for f in [^\n]*?)(" + re.escape(f"unreal_{old_u}") + r")" + text, n = re.subn(for_pat, r"\1unreal_" + new_u + r" \2", text, count=1) + if n == 0: + _warn(rel, f"unreal_{old_u} not found in zip for-loop") + ok = False + + # 3. gh release create asset list: "unreal_OLD.zip" → "unreal_NEW.zip" "unreal_OLD.zip" + old_asset = f'"unreal_{old_u}.zip"' + if old_asset not in text: + _warn(rel, f"release asset {old_asset} not found") + ok = False + else: + text = text.replace(old_asset, f'"unreal_{new_u}.zip" {old_asset}', 1) + + if not ok: + return False + + _write(path, text) + _ok(rel, "download / zip / release lists updated") + return True + + +def _check_demo_zip(new_ue: str) -> None: + zip_rel = "Documentation/unreal/InkCPP_DEMO.zip" + zip_path = REPO_ROOT / zip_rel + if not zip_path.exists(): + _info(f"{zip_rel} not found locally — skipping demo check") + return + + try: + with zipfile.ZipFile(zip_path) as zf: + uproject = next((n for n in zf.namelist() if n.endswith(".uproject")), None) + if not uproject: + _warn(zip_rel, "no .uproject file found inside the zip") + return + data = json.loads(zf.read(uproject).decode("utf-8")) + engine_assoc = data.get("EngineAssociation", "") + except Exception as exc: + _warn(zip_rel, f"could not inspect zip: {exc}") + return + + if engine_assoc == new_ue: + _ok(zip_rel, f"EngineAssociation already {engine_assoc!r}") + else: + print(f" [WARN] {zip_rel}") + print( + f" EngineAssociation is {engine_assoc!r} but new default is {new_ue!r}." + ) + print(f" Action: open the project in UE {new_ue}, let it convert,") + print(" re-export it, and replace the zip.") + + +def bump_ue(new_ue: str) -> None: + if not re.fullmatch(r"\d+\.\d+", new_ue): + sys.exit(f"ERROR: UE version must be MAJOR.MINOR (e.g. 5.8), got: {new_ue!r}") + + old_ue = current_ue_version() + if old_ue == new_ue: + print(f"UE default is already {old_ue} — nothing to do.") + return + + print(f"Adding UE {new_ue} (new default; {old_ue} and earlier remain in CI)\n") + + changed = [] + if _update_unreal_cmake(old_ue, new_ue): + changed.append("unreal/CMakeLists.txt") + if _update_build_yml(old_ue, new_ue): + changed.append(".github/workflows/build.yml") + if _update_release_yml(old_ue, new_ue): + changed.append(".github/workflows/release.yml") + + print() + print("Demo project check:") + _check_demo_zip(new_ue) + + if changed: + files = " ".join(changed) + print() + print("Suggested commit:") + print(f" git add {files}") + print( + f' git commit -m "chore: add UE {new_ue} support (default; {old_ue} still built)"' + ) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + if ARGS["inkcpp"]: + bump_inkcpp(ARGS[""]) + elif ARGS["ue"]: + bump_ue(ARGS[""]) diff --git a/shared/private/header.h b/shared/private/header.h index 43383f15..198b989c 100644 --- a/shared/private/header.h +++ b/shared/private/header.h @@ -37,7 +37,7 @@ struct header { } } - bool verify() const; + bool validate() const; struct section_t { uint32_t _start = 0; diff --git a/shared/public/config.h b/shared/public/config.h index 76d58f42..504b917f 100644 --- a/shared/public/config.h +++ b/shared/public/config.h @@ -32,77 +32,97 @@ // Only turn on if you have json.hpp and you want to use it with the compiler // #define INK_EXPOSE_JSON - +/** + * set limitations which are required to minimize heap allocations. + * if required you can set them to -x then, the system will use dynamic + * allocation for this type, with an initial size of x. + */ namespace ink::config { -/// set limitations which are required to minimize heap allocations. -/// if required you can set them to -x then, the system will use dynamic -/// allocation for this type, with an initial size of x. -static constexpr int limitGlobalVariables = -50; -static constexpr int limitGlobalVariableObservers = -10; -static constexpr int limitThreadDepth = -10; -static constexpr int limitEvalStackDepth = -20; -static constexpr int limitContainerDepth = -20; +/** amount of global variables in the script. */ +constexpr int limitGlobalVariables = -50; +/** amount of simustanly registerd variable observers. */ +constexpr int limitGlobalVariableObservers = -10; +/** maximum amount of tunnel/choice inception. */ +constexpr int limitThreadDepth = -10; +/** maximum size of the evaluation stack. + * Each operation inside an expression needs at least 3 slots. + * Also string building for choices with @c [] syntax will use the stack. + */ +constexpr int limitEvalStackDepth = -20; +/** maximum number of cascaded nodes. + * beside stitches and knots, choices are also containers. + */ +constexpr int limitContainerDepth = -20; /** number of lists which can be accessed with get_var * before the story must continue * @attention list vars are only valid until the story continuous! */ -static constexpr int limitEditableLists = -5; -/// number of simultaneous active tags -static constexpr int limitActiveTags = -10; -// temporary variables and call stack; - -static constexpr int limitRuntimeStack = -20; -// references and call stack -static constexpr int limitReferenceStack = -20; -// max number of elements in one output (a string is one element) -static constexpr int limitOutputSize = -100; -// maximum number of text fragments between choices -static constexpr int limitStringTable = -100; -// max number of choices per choice -static constexpr int maxChoices = -10; -// max number of list types, and there total amount of flags -static constexpr int maxListTypes = -20; -static constexpr int maxFlags = -200; -// number of max initialized lists -static constexpr int maxLists = -50; -// max number of arguments for external functions (dynamic not possible) -static constexpr int maxArrayCallArity = 10; +constexpr int limitEditableLists = -5; +/** number of simultaneous active tags. */ +constexpr int limitActiveTags = -10; +/** temporary variables and call stack. */ +constexpr int limitRuntimeStack = -20; +/** references and call stack. */ +constexpr int limitReferenceStack = -20; +/** max number of elements in one output (a string is one element). */ +constexpr int limitOutputSize = -100; +/** maximum number of text fragments between choices. */ +constexpr int limitStringTable = -100; +/** max number of choices per choice. */ +constexpr int maxChoices = -10; +/** max number of list types, and there total amount of flags. */ +constexpr int maxListTypes = -20; +/** maximum number of defined list flags. */ +constexpr int maxFlags = -200; +/** number of max initialized lists. */ +constexpr int maxLists = -50; +/** max number of arguments for external functions (dynamic not possible). */ +constexpr int maxArrayCallArity = 10; +/** Staistiac data for different game elements. + * use this to set you config settings appropriate to your scenario or just to get some insight. + */ namespace statistics { + /** Statistic data for an container data type. */ struct container { - int capacity; - int size; + int capacity; /**< current capacity of the container. This does typicall only inceares over the + runtime. */ + int size; /**< current size aka activly used elements inside the container. */ }; + /** Statistics for managed lists, including static and dynamic enties. */ struct list_table { - container editable_lists; /** based on @ref limitEditableLists */ - container list_types; /** based on @ref maxListTypes */ - container flags; /** based on @ref maxFlags */ - container lists; /** based on @ref maxLists */ + container editable_lists; /**< based on @ref ink::config::limitEditableLists */ + container list_types; /**< based on @ref ink::config::maxListTypes */ + container flags; /**< based on @ref ink::config::maxFlags */ + container lists; /**< based on @ref ink::config::maxLists */ }; + /** Statistiacs to managed strings, which are build at runtime. */ struct string_table { - container string_refs; /** based on @ref limitStringTable */ + container string_refs; /**< based on @ref limitStringTable */ }; + /** Stastics for state managed for one runtime. */ struct global { - container variables; /** based on @ref limitGlobalVariables */ - container variables_observers; /** based on @ref limitGlobalVariableObservers */ - list_table lists; - string_table strings; + container variables; /**< based on @ref ink::config::limitGlobalVariables */ + container variables_observers; /**< based on @ref ink::config::limitGlobalVariableObservers */ + list_table lists; /**< Staistics for all lists associated with this runtime. */ + string_table strings; /**< Staistics for all strings associtade with this runtime. */ }; + /** Stastics for state managed for one thread inside a runtime. */ struct runner { - container threads; /** based on @ref limitThreadDepth */ - container evaluation_stack; /** based on @ref limitEvalStackDepth */ - container container_stack; /** based on @ref limitContainerDepth */ - container active_tags; /** based on @ref limitActiveTags */ - container runtime_stack; /** based on @ref limitContainerDepth */ - container runtime_ref_stack; /** based on @ref limitReferenceStack */ - container output; /** based on @ref limitOutputSize */ - container choices; /** based on @ref limitContainerDepth */ + container threads; /**< based on @ref ink::config::limitThreadDepth */ + container evaluation_stack; /**< based on @ref ink::config::limitEvalStackDepth */ + container container_stack; /**< based on @ref ink::config::limitContainerDepth */ + container active_tags; /**< based on @ref ink::config::limitActiveTags */ + container runtime_stack; /**< based on @ref ink::config::limitContainerDepth */ + container runtime_ref_stack; /**< based on @ref ink::config::limitReferenceStack */ + container output; /**< based on @ref ink::config::limitOutputSize */ + container choices; /**< based on @ref ink::config::limitContainerDepth */ }; } // namespace statistics } // namespace ink::config diff --git a/shared/public/system.h b/shared/public/system.h index 82259cbd..dc8f1eb9 100644 --- a/shared/public/system.h +++ b/shared/public/system.h @@ -40,13 +40,26 @@ # define FORMAT_STRING_STR "%s" #endif +/** + * @def inkAssert(condition, format_string, args...) + * @ingroup cpp + * Compile argument agnostic assert macro. + * behaves diffrent base on if it is an UnrealEngine compilation or standalone. + * Also respects INKCPP_NO_RTTI, INKCPP_NO_STD and INKCPP_NO_EXCEPTIONS. + */ +/** + * @def inkFail(format_string, args...) + * @ingroup cpp + * Compile argument agnostic assert macro (always asserts). + * @sa inkAssert + */ + #ifdef INK_ENABLE_UNREAL # define inkAssert(condition, text, ...) checkf(condition, TEXT(text), ##__VA_ARGS__) # define inkFail(text, ...) checkf(false, TEXT(text), ##__VA_ARGS__) #else -# define inkAssert ink::ink_assert -# define inkFail(...) ink::ink_assert(false, __VA_ARGS__) - +# define inkAssert(...) ink::ink_assert(__VA_ARGS__) +# define inkFail(...) ink::ink_assert(false, __VA_ARGS__) #endif @@ -115,13 +128,6 @@ struct list_flag { bool operator!=(const list_flag& o) const { return ! (*this == o); } }; -inline list_flag read_list_flag(const char*& ptr) -{ - list_flag result = *reinterpret_cast(ptr); - ptr += sizeof(list_flag); - return result; -} - /** value of an unset list_flag */ constexpr list_flag null_flag{-1, -1}; /** value representing an empty list */ @@ -133,6 +139,12 @@ inline hash_t hash_string(const char* string) { return CityHash32(string, FCStringAnsi::Strlen(string)); } + +/** Simple hash for detcting changes in binary data. (e.g. Changes in the story file) */ +inline hash_t hash_data(const unsigned char* data, size_t len) +{ + return CityHash32(reinterpret_cast(data), len); +} #else hash_t hash_string(const char* string); hash_t hash_data(const unsigned char* data, size_t len); @@ -143,10 +155,8 @@ namespace internal #ifdef __GNUC__ #else # pragma warning(push) -# pragma warning( \ - disable : 4514, \ - justification : "functions are defined in header file, they do not need to be used." \ - ) +// functions are defined in header file, they do not need to be used. +# pragma warning(disable : 4514) #endif /** Checks if a string starts with a given prefix*/ static inline constexpr bool starts_with(const char* string, const char* prefix) @@ -231,12 +241,11 @@ class ink_exception # pragma GCC diagnostic ignored "-Wunused-parameter" #else # pragma warning(push) -# pragma warning( \ - disable : 4100, \ - justification : "dependend on rtti, exception and stl support not all arguments are needed" \ - ) +// dependend on rtti, exception and stl support not all arguments are needed +# pragma warning(disable : 4100) #endif -// assert +/** Assert helper, not to be used directly, please use @ref inkAssert and @ref inkFail to be + * enviroment agnostic. */ template void ink_assert(bool condition, const char* msg = nullptr, Args... args) { @@ -258,6 +267,8 @@ void ink_assert(bool condition, const char* msg = nullptr, Args... args) #elif defined(INK_ENABLE_CSTD) fprintf(stderr, "Ink Assert: %s\n", msg); abort(); +#elif defined(INK_ENABLE_UNREAL) + // TODO: implement UE exception handling #else # warning no assertion handling this could lead to invalid code paths #endif @@ -269,6 +280,8 @@ void ink_assert(bool condition, const char* msg = nullptr, Args... args) # pragma warning(pop) #endif +/** Assert helper, not to be used directly, please use @ref inkAssert and @ref inkFail to be + * enviroment agnostic. */ template [[noreturn]] inline void ink_assert(const char* msg = nullptr, Args... args) { diff --git a/unreal/CMakeLists.txt b/unreal/CMakeLists.txt index 3000f45e..6b304697 100644 --- a/unreal/CMakeLists.txt +++ b/unreal/CMakeLists.txt @@ -1,64 +1,92 @@ -set(INKCPP_UNREAL_TARGET_VERSION "5.6" CACHE STRING "Unreal engine version the plugin should target (e.g: 5.6)") -set(INKCPP_UNREAL_RunUAT_PATH CACHE FILEPATH "Path to Unreal engine installation RunUAT file. Used to automatcally build the plugin.") +set(INKCPP_UNREAL_TARGET_VERSION + "5.7" + CACHE STRING "Unreal engine version the plugin should target (e.g: 5.6)") +set(INKCPP_UNREAL_RunUAT_PATH + CACHE FILEPATH + "Path to Unreal engine installation RunUAT file. Used to automatcally build the plugin.") option(INKCPP_UNREAL "Prepare sourcefiles for a UE Plugin (this will download " OFF) - option(INKCPP_DOC_BlueprintUE "Building doxygen documentation with BlueprintUE visualisation for unreal blueprints. (Requires node js)" ON) -set(INKCPP_UNREAL_TARGET_PLATFORM "Win64" CACHE STRING "Target platform for the UE Plugin one of Win64, Mac, Linux") +option(INKCPP_DOC_BlueprintUE "Building doxygen documentation with BlueprintUE visualisation \ + for unreal blueprints. (Requires node js)" ON) +set(INKCPP_UNREAL_TARGET_PLATFORM + "Win64" + CACHE STRING "Target platform for the UE Plugin one of Win64, Mac, Linux") set_property(CACHE INKCPP_UNREAL_TARGET_PLATFORM PROPERTY STRINGS "Win64" "Mac" "Linux") -if (INKCPP_UNREAL) +if(INKCPP_UNREAL) include(FetchContent) - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/inkcpp.uplugin.in" - "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/inkcpp.uplugin") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/inkcpp.uplugin.in" + "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/inkcpp.uplugin") # download inklecate for unreal plugin FetchContent_MakeAvailable(inklecate_mac inklecate_windows inklecate_linux) set(FETCHCONTENT_QUIET OFF) set(CMAKE_TLS_VERIFY true) if(NOT inklecate_windows_SOURCE_DIR) - message(WARNING "failed to download inklecate for windows, " - "the unreal plugin will be unable use a .ink file as asset directly") + message(WARNING "failed to download inklecate for windows, " + "the unreal plugin will be unable use a .ink file as asset directly") else() - set(INKLECATE_CMD_WIN "Source/ThirdParty/inklecate/windows/inklecate.exe") - file(COPY "${CMAKE_BINARY_DIR}/inklecate/windows" DESTINATION "inkcpp/Source/ThirdParty/inklecate/") + set(INKLECATE_CMD_WIN "Source/ThirdParty/inklecate/windows/inklecate.exe") + file(COPY "${CMAKE_BINARY_DIR}/inklecate/windows" + DESTINATION "inkcpp/Source/ThirdParty/inklecate/") endif() if(NOT inklecate_mac_SOURCE_DIR) message(WARNING "failed to download inklecate for MacOS, " - "the unreal plugin will be unable use a .ink file as asset directly") + "the unreal plugin will be unable use a .ink file as asset directly") else() - set(INKLECATE_CMD_MAC "Source/ThirdParty/inklecate/mac/inklecate") - file(COPY "${CMAKE_BINARY_DIR}/inklecate/mac" DESTINATION "inkcpp/Source/ThirdParty/inklecate/") + set(INKLECATE_CMD_MAC "Source/ThirdParty/inklecate/mac/inklecate") + file(COPY "${CMAKE_BINARY_DIR}/inklecate/mac" DESTINATION "inkcpp/Source/ThirdParty/inklecate/") endif() if(NOT inklecate_linux_SOURCE_DIR) message(WARNING "failed to download inklecate for linux, " - "the unreal plugin will be unable use a .ink file as asset directly") + "the unreal plugin will be unable use a .ink file as asset directly") else() - set(INKLECATE_CMD_LINUX "Source/ThirdParty/inklecate/linux/inklecate") - file(COPY "${CMAKE_BINARY_DIR}/inklecate/linux" DESTINATION "inkcpp/Source/ThirdParty/inklecate/") + set(INKLECATE_CMD_LINUX "Source/ThirdParty/inklecate/linux/inklecate") + file(COPY "${CMAKE_BINARY_DIR}/inklecate/linux" + DESTINATION "inkcpp/Source/ThirdParty/inklecate/") endif() configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in" - "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp" - ) - file(GLOB_RECURSE SOURCE_FILES LIST_DIRECTORIES TRUE RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/*") + "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp") + file( + GLOB_RECURSE SOURCE_FILES + LIST_DIRECTORIES TRUE + RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/*") list(FILTER SOURCE_FILES EXCLUDE REGEX ".*\.in$") - foreach(SRC_FILE IN LISTS SOURCE_FILES) - if (NOT IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${SRC_FILE}") - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/${SRC_FILE}" "${CMAKE_CURRENT_BINARY_DIR}/${SRC_FILE}" COPYONLY) + foreach(src_file IN LISTS SOURCE_FILES) + if(NOT IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${src_file}") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/${src_file}" + "${CMAKE_CURRENT_BINARY_DIR}/${src_file}" COPYONLY) endif() endforeach() - install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/" DESTINATION "inkcpp" COMPONENT unreal EXCLUDE_FROM_ALL) + install( + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/" + DESTINATION "inkcpp" + COMPONENT unreal + EXCLUDE_FROM_ALL) file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Plugins/inkcpp/") - if ((NOT DEFINED INKCPP_UNREAL_RunUAT_PATH) OR (NOT "${INKCPP_UNREAL_RunUAT_PATH}" STREQUAL "")) - if (NOT IS_READABLE "${INKCPP_UNREAL_RunUAT_PATH}") - message(WARNING "Unable to find RunUAT script at >${INKCPP_UNREAL_RunUAT_PATH}<, will not be able to build target `unreal` set the filepath with the variable INKCPP_UNREAL_RunUAT_PATH") + if((NOT DEFINED INKCPP_UNREAL_RunUAT_PATH) OR (NOT "${INKCPP_UNREAL_RunUAT_PATH}" STREQUAL "")) + if(NOT IS_READABLE "${INKCPP_UNREAL_RunUAT_PATH}") + message( + WARNING "Unable to find RunUAT script at >${INKCPP_UNREAL_RunUAT_PATH}<, will not be able \ + to build target `unreal` set the filepath with the variable INKCPP_UNREAL_RunUAT_PATH") endif() else() - message(WARNING, "To directly build the plugin with `cmake --build . --target unreal` please set INKCPP_UNREAL_RunUAT_PATH to point to unreals RunUAT script.") + message(WARNING, "To directly build the plugin with `cmake --build . --target unreal` please \ + set INKCPP_UNREAL_RunUAT_PATH to point to unreals RunUAT script.") endif() - add_custom_target(unreal - "${INKCPP_UNREAL_RunUAT_PATH}" BuildPlugin "-plugin=${CMAKE_CURRENT_BINARY_DIR}/inkcpp/inkcpp.uplugin" "-package=${CMAKE_CURRENT_BINARY_DIR}/Plugins/inkcpp" "-TargetPlatforms=${INKCPP_UNREAL_TARGET_PLATFORM}" + add_custom_target( + unreal + "${INKCPP_UNREAL_RunUAT_PATH}" + BuildPlugin + "-plugin=${CMAKE_CURRENT_BINARY_DIR}/inkcpp/inkcpp.uplugin" + "-package=${CMAKE_CURRENT_BINARY_DIR}/Plugins/inkcpp" + "-TargetPlatforms=${INKCPP_UNREAL_TARGET_PLATFORM}" COMMENT "Compile UE Plugin.") - install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Plugins/inkcpp/" DESTINATION "inkcpp" COMPONENT unreal_plugin EXCLUDE_FROM_ALL PATTERN "Intermediate/*" EXCLUDE) + install( + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Plugins/inkcpp/" + DESTINATION "inkcpp" + COMPONENT unreal_plugin + EXCLUDE_FROM_ALL + PATTERN "Intermediate/*" EXCLUDE) # TODO: update documenation endif() - diff --git a/unreal/UE_example.ink b/unreal/UE_example.ink index 14ff0249..71613f8d 100644 --- a/unreal/UE_example.ink +++ b/unreal/UE_example.ink @@ -1,10 +1,10 @@ # date:2025.03.22 LIST Potions = TalkWithAnimals, Invisibility LIST Clues = Skull, Feather +LIST Knowladge = YellowDress VAR Inventory = (Skull, TalkWithAnimals) VAR Health = 100 -VAR can_talk_with_animals = false -VAR is_insible = false +LIST StatusConditions = CanTalkWithAniamls, IsInvisible -> Mansion.Car @@ -31,24 +31,24 @@ You startk walking to {to}. = TTalkWithAnimals A potion which allows the consumer to talk with a variaty of animals. Just make sure your serroundings do not think you are crazy. - {can_talk_with_animals: Drinking more of it will not increase the effect.} + {StatusConditions ? (CanTalkWithAniamls): Drinking more of it will not increase the effect.} + [Put it Back] You put the potion back into your pouch. -> END - + {not can_talk_with_animals} [Drink] + + {not (StatusConditions ? CanTalkWithAniamls)} [Drink] + ~ StatusConditions += CanTalkWithAniamls A take a sip. The potion tastes like Hores, it is afull. - ~ can_talk_with_animals = true -> END = TInvisibility A potion which allows the consumer to stay unseen for the human eye. Not tested wtih other speccies. - {is_insible: Drinking more of it will not increase the effect.} + {StatusConditions ? (IsInvisible): Drinking more of it will not increase the effect.} + [Put it Back] You put the potion back into your pouch. -> END - + {not is_insible}[Drink] + + {not (StatusConditions ? (IsInvisible))}[Drink] + ~ StatusConditions += IsInvisible You put a small drop on your thoungh. It feels like liking a battery, such a nice feeling - ~ is_insible = true -> END === Faint @@ -63,6 +63,7 @@ No further action for today ... You step outside your car. Its a wired feeling beehing here again. -> Car_cycle = Car_cycle +# background:Car + (look_around)[look around # Type:Idle] It is a strange day. Despite it beeing spring, the sky is one massive gray soup. # style:Gray -> Car_cycle @@ -72,10 +73,14 @@ You step outside your car. Its a wired feeling beehing here again. = Entrance # background:Mansion -{not Mansion.look_around: Just in time you are able to see the door, someone with with a yellow summer dress enters it.} -You climbing the 56 steps up to the door, high water is a dump thing. +{not Mansion.look_around: + ~ Knowladge += YellowDress + Just in time you are able to see the door, someone with with a yellow summer dress enters it. +} +You're climbing the 56 steps up to the door; high tides are an annoying thing. -> Entrance_cycle = Entrance_cycle +# background:Mansion + (look_around) [look around # Type:Idle ] While watching around you, <> {Inventory hasnt Invisibility: @@ -89,8 +94,12 @@ You climbing the 56 steps up to the door, high water is a dump thing. You pick up the bottle and inspect it more. It is labeld "Invisible, just this one word written with and edding. -> Entrance_cycle + (knock)[Knock {knock: again?} # {knock: Type:Danger} ] - "Ahh", you cry while reaching for the door bell. Saying it was charched would be an understatement. ~ Health -= 20 + "Ahh", you cry while reaching for the door bell. Saying it was charched would be an understatement. { Health <= 0: -> Faint} -> Entrance_cycle ++ {knock && Knowladge ? (YellowDress)} [Inspect the Door] + You just saw someone enter, how did they do not get shoked? + It seems the developr run out of time, maybe in the next version we can continue? + -> Entrance_cycle -> DONE diff --git a/unreal/blueprint_filter.js b/unreal/blueprint_filter.js index 39b17c1f..63457ff8 100644 --- a/unreal/blueprint_filter.js +++ b/unreal/blueprint_filter.js @@ -273,10 +273,32 @@ Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="" End Object ` } -function construct_ueasset(type, signature) { +function build_actionbase(type, name, args) { + for (const key in args) { + args[key] = build_pin(args[key]) + } + return ` +Begin Object Class=/Script/BlueprintGraph.K2Node_AsyncAction Name="" + ProxyFactoryFunctionName="${name}" + NodePosX=0 + NodePosY=0 + CustomProperties Pin (PinName="execute",PinType.PinCategory="exec",) + CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec",) + CustomProperties Pin (PinName="Completed",PinFriendlyName=NSLOCTEXT("UObjectDisplayNames", "", "Completed"),Direction="EGPD_Output",PinType.PinCategory="exec",) + CustomProperties Pin (PinName="Snapshot",,Direction="EGPD_Output",PinType.PinCategory="struct",) + ${args.join("")} +End Object +` +} +function construct_ueasset(type, signature, doxy_blueprint_args) { var builder = undefined; switch (type) { - case "BlueprintCallable": builder = build_call; break; + case "BlueprintCallable": + if (doxy_blueprint_args.length >= 2 && doxy_blueprint_args[0] == "ActionBase") { + builder = build_actionbase; + } else { + builder = build_call; + } break; case "BlueprintImplementableEvent": builder = build_event; break; case "BlueprintPure": builder = build_pure; break; default: throw new Exception(`unknown type: '${type}'`); @@ -345,15 +367,16 @@ for (const arg of argv) { var input = fs.readFileSync(arg).toString(); var out_str = input; var offset = 0; - var re = /(DOC_UF|UFUNCTION)\(\s*(?[^,]*),[^]*?\).*\s*\/\*\*[^]*?(?@blueprint)[^]*?\*\/\s*(?[^;]*);/gmd; + var re = /(DOC_UF|UFUNCTION)\(\s*(?[^,]*),[^]*?\).*\s*\/\*\*[^]*?(?@blueprint(\{(?[^}]+)\})?)[^]*?\*\/\s*(?[^;]*);/gmd; while ((m = re.exec(input)) != null) { let type = m.groups.type; let signature = m.groups.signature; let pos = m.indices.groups.pos; + let blueprintParams = (!!m.groups.blueprintParams && m.groups.blueprintParams.split(",").map(item => item.trim())) || []; let output = new HTMLElement("body"); let document = new Document(); new window.blueprintUE.render.Main( - construct_ueasset(type, signature), + construct_ueasset(type, signature, blueprintParams), output, { height: "643px" } @@ -374,4 +397,3 @@ new window.blueprintUE.render.Main( } ).start(); console.log(`${prefix}${output.querySelector('.node')}${suffix}`); - diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkChoice.cpp b/unreal/inkcpp/Source/inkcpp/Private/InkChoice.cpp index 365d2b4f..5c48b6b7 100644 --- a/unreal/inkcpp/Source/inkcpp/Private/InkChoice.cpp +++ b/unreal/inkcpp/Source/inkcpp/Private/InkChoice.cpp @@ -8,17 +8,20 @@ #include "ink/choice.h" -FString UInkChoice::GetText() const { return data->text(); } - UInkChoice::UInkChoice() { tags = NewObject(); } -int UInkChoice::GetIndex() const { return data->index(); } +FString UInkChoice::GetText() const { return Text; } + +int UInkChoice::GetIndex() const { return Index; } const UTagList* UInkChoice::GetTags() const { return tags; } void UInkChoice::Initialize(const ink::runtime::choice* c) { - data = c; + // Copy all data out of the runner immediately — the pointer is only valid + // until the next getline() or choose() call. + Text = FString(UTF8_TO_TCHAR(c->text())); + Index = c->index(); if (c->has_tags()) { TArray fstring_tags{}; for (unsigned i = 0; i < c->num_tags(); ++i) { diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkExecutionScope.h b/unreal/inkcpp/Source/inkcpp/Private/InkExecutionScope.h new file mode 100644 index 00000000..5cd14d68 --- /dev/null +++ b/unreal/inkcpp/Source/inkcpp/Private/InkExecutionScope.h @@ -0,0 +1,28 @@ +/* Copyright (c) 2024 Julian Benda + * + * This file is part of inkCPP which is released under MIT license. + * See file LICENSE.txt or go to + * https://github.com/JBenda/inkcpp for full license details. + */ +#pragma once + +class UInkThread; + +/** Thread-local pointer to the UInkThread currently executing inside Execute(). + * Set exclusively via FInkExecutionScope — never write it directly. + * Used by FInkVar to register newly created UInkList wrappers so the thread + * can invalidate them before the next choose() call. + */ +extern thread_local UInkThread* GExecutingInkThread; + +/** RAII guard that sets/clears GExecutingInkThread for the lifetime of a + * UInkThread::Execute() call. Impossible to forget to clear. + */ +struct FInkExecutionScope { + explicit FInkExecutionScope(UInkThread* thread) { GExecutingInkThread = thread; } + + ~FInkExecutionScope() { GExecutingInkThread = nullptr; } + + FInkExecutionScope(const FInkExecutionScope&) = delete; + FInkExecutionScope& operator=(const FInkExecutionScope&) = delete; +}; diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkList.cpp b/unreal/inkcpp/Source/inkcpp/Private/InkList.cpp index 17ec5680..24d6090e 100644 --- a/unreal/inkcpp/Source/inkcpp/Private/InkList.cpp +++ b/unreal/inkcpp/Source/inkcpp/Private/InkList.cpp @@ -4,19 +4,22 @@ * See file LICENSE.txt or go to * https://github.com/JBenda/inkcpp for full license details. */ -#pragma once - #include "InkList.h" +#include "inkcpp.h" #include #include "ink/list.h" bool UInkList::ContainsFlag(const FString& flag_name) const { + if (! ensureMsgf(IsValid(), TEXT("UInkList::ContainsFlag called on an invalid (stale) list"))) + return false; return list_data->contains(TCHAR_TO_UTF8(*flag_name)); } bool UInkList::ContainsEnum(const UEnum* Enum, const uint8& value) const { + if (! ensureMsgf(IsValid(), TEXT("UInkList::ContainsEnum called on an invalid (stale) list"))) + return false; if (! Enum) { UE_LOG( InkCpp, Warning, @@ -31,6 +34,8 @@ bool UInkList::ContainsEnum(const UEnum* Enum, const uint8& value) const TArray UInkList::ElementsOf(const UEnum* Enum) const { TArray ret; + if (! ensureMsgf(IsValid(), TEXT("UInkList::ElementsOf called on an invalid (stale) list"))) + return ret; if (! Enum) { UE_LOG(InkCpp, Warning, TEXT("Failed to provide enum for elements of!")); return ret; @@ -63,6 +68,10 @@ TArray UInkList::ElementsOf(const UEnum* Enum) const TArray UInkList::ElementsOfAsString(const UEnum* Enum) const { TArray ret; + if (! ensureMsgf( + IsValid(), TEXT("UInkList::ElementsOfAsString called on an invalid (stale) list") + )) + return ret; FString EnumName = Enum->GetFName().ToString(); for (auto itr = list_data->begin(TCHAR_TO_UTF8(*EnumName)); itr != list_data->end(); ++itr) { @@ -74,6 +83,8 @@ TArray UInkList::ElementsOfAsString(const UEnum* Enum) const TArray UInkList::Elements() const { TArray ret; + if (! ensureMsgf(IsValid(), TEXT("UInkList::Elements called on an invalid (stale) list"))) + return ret; for (auto itr = list_data->begin(); itr != list_data->end(); ++itr) { ret.Add(FListFlag{ .list_name = FString((*itr).list_name), @@ -85,5 +96,7 @@ TArray UInkList::Elements() const bool UInkList::ContainsList(const FString& name) const { + if (! ensureMsgf(IsValid(), TEXT("UInkList::ContainsList called on an invalid (stale) list"))) + return false; return list_data->begin(TCHAR_TO_UTF8(*name)) != list_data->end(); } diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkRuntime.cpp b/unreal/inkcpp/Source/inkcpp/Private/InkRuntime.cpp index b549a643..e36114a4 100644 --- a/unreal/inkcpp/Source/inkcpp/Private/InkRuntime.cpp +++ b/unreal/inkcpp/Source/inkcpp/Private/InkRuntime.cpp @@ -6,6 +6,8 @@ */ #include "InkRuntime.h" +#include "Async/Async.h" + // Game includes #include "inkcpp.h" #include "InkThread.h" @@ -19,6 +21,7 @@ #include "ink/snapshot.h" #include "system.h" #include "types.h" +#include namespace ink { @@ -36,10 +39,10 @@ AInkRuntime::AInkRuntime() AInkRuntime::~AInkRuntime() { - if (mSnapshot) { - delete mpSnapshot; + if (mStableSnapshot.IsValid()) { + mStableSnapshot->SetValue(FInkSnapshot()); + mStableSnapshot.Reset(); } - mSnapshot.Reset(); } // Called when the game starts or when spawned @@ -60,8 +63,6 @@ void AInkRuntime::BeginPlay() } else { mpGlobals = mpRuntime->new_globals(); } - // initialize globals - mpRuntime->new_runner(mpGlobals); } else { UE_LOG(InkCpp, Warning, TEXT("No story asset assigned.")); } @@ -69,6 +70,27 @@ void AInkRuntime::BeginPlay() Super::BeginPlay(); } +void AInkRuntime::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + mThreads.Empty(); + mExclusiveStack.Empty(); + + mpGlobals = ink::runtime::globals{}; + + if (mpSnapshot) { + delete mpSnapshot; + mpSnapshot = nullptr; + } + mSnapshot.Reset(); + + if (mpRuntime) { + delete mpRuntime; + mpRuntime = nullptr; + } +} + // Called every frame void AInkRuntime::Tick(float DeltaTime) { @@ -118,19 +140,33 @@ void AInkRuntime::Tick(float DeltaTime) } } -void AInkRuntime::HandleTagFunction(UInkThread* Caller, const TArray& Params) +FInkHandle + AInkRuntime::RegisterTagFunction(FName functionName, const FTagFunctionDelegate& function) { - // Look for method and execute with parameters - FGlobalTagFunctionMulticastDelegate* function = mGlobalTagFunctions.Find(FName(*Params[0])); - if (function != nullptr) { - function->Broadcast(Caller, Params); - } + TSharedPtr token = MakeShared(true); + mTagFunctionTokens.FindOrAdd(functionName).Add(token); + mTagFunctionDelegates.FindOrAdd(functionName).Add(function); + mObserverTokens.Add(token); + return FInkHandle(token); } -void AInkRuntime::RegisterTagFunction(FName functionName, const FTagFunctionDelegate& function) +void AInkRuntime::HandleTagFunction(UInkThread* Caller, const TArray& Params) { - // Register tag function - mGlobalTagFunctions.FindOrAdd(functionName).Add(function); + FName name(*Params[0]); + auto* tokens = mTagFunctionTokens.Find(name); + auto* delegates = mTagFunctionDelegates.Find(name); + if (! tokens || ! delegates) + return; + + // Fire active delegates, compact dead entries lazily + for (int32 i = tokens->Num() - 1; i >= 0; --i) { + if ((*tokens)[i].IsValid() && *(*tokens)[i]) { + (*delegates)[i].ExecuteIfBound(Caller, Params); + } else { + tokens->RemoveAtSwap(i); + delegates->RemoveAtSwap(i); + } + } } UInkThread* @@ -153,19 +189,71 @@ FInkSnapshot AInkRuntime::Snapshot() { ink::runtime::snapshot* inkSnapshot = mpGlobals->create_snapshot(); FInkSnapshot snapshot( - reinterpret_cast(inkSnapshot->get_data()), inkSnapshot->get_data_len() + reinterpret_cast(inkSnapshot->get_data()), inkSnapshot->get_data_len(), + inkSnapshot->can_be_migrated() ); delete inkSnapshot; return snapshot; } +TFuture AInkRuntime::MigratableSnapshot() +{ + // Fast path: already stable + FInkSnapshot snapshot = Snapshot(); + + if (snapshot.Migratable) { + TPromise Immediate; + Immediate.SetValue(snapshot); + return Immediate.GetFuture(); + } + + // Slow path: wait for stability + TSharedRef, ESPMode::ThreadSafe> Promise + = MakeShared, ESPMode::ThreadSafe>(); + + mStableSnapshot = Promise; + + return Promise->GetFuture(); +} + +void AInkRuntime::RunnerEnterStableState(UInkThread* thread) +{ + if (mStableSnapshot.IsValid()) { + thread->Yield(); + mYieldedThreadsForSnapshot.Add(thread); + FInkSnapshot snapshot = Snapshot(); + if (snapshot.Migratable) { + mStableSnapshot->SetValue(snapshot); + mStableSnapshot.Reset(); + for (auto& _thread : mYieldedThreadsForSnapshot) { + _thread->Resume(); + } + mYieldedThreadsForSnapshot.Empty(); + } + } +} + void AInkRuntime::LoadSnapshot(const FInkSnapshot& snapshot) { + if (mpSnapshot) { + delete mpSnapshot; + mpSnapshot = nullptr; + } mSnapshot = snapshot; mpSnapshot = ink::runtime::snapshot::from_binary( reinterpret_cast(mSnapshot->data.GetData()), mSnapshot->data.Num(), false ); mpGlobals = mpRuntime->new_globals_from_snapshot(*mpSnapshot); + if (! mpGlobals.is_valid()) { + UE_LOG(InkCpp, Error, TEXT("Failed to load snapshot.")); + if (! mpSnapshot->can_be_migrated()) { + UE_LOG( + InkCpp, Error, + TEXT("Unable to load snapshot. The story has changed and the snapshot was taken at in " + "instable moment.") + ); + } + } } UInkThread* @@ -176,6 +264,18 @@ UInkThread* return nullptr; } + if (! mpGlobals.is_valid()) { + if (mSnapshot) { + UE_LOG( + InkCpp, Error, + TEXT("Failed to start existing, due to invalid state after failed snapshot loading.") + ); + } else { + UE_LOG(InkCpp, Warning, TEXT("Failed to start existing")); + } + return nullptr; + } + // remove handle if it still exists mThreads.Remove(thread); mExclusiveStack.Remove(thread); @@ -199,7 +299,8 @@ UInkThread* // If we're not starting immediately, just queue if (! startImmediately || - // Even if we want to start immediately, don't if there's an exclusive thread and it's not us + // Even if we want to start immediately, don't if there's an exclusive thread and it's not + // us (mExclusiveStack.Num() > 0 && mExclusiveStack.Top() != thread)) { mThreads.Add(thread); return thread; @@ -258,32 +359,49 @@ void AInkRuntime::SetGlobalVariable(const FString& name, const FInkVar& value) } } -void AInkRuntime::ObserverVariable(const FString& name, const FVariableCallbackDelegate& callback) +FInkHandle + AInkRuntime::ObserverVariable(const FString& name, const FVariableCallbackDelegate& callback) { - mpGlobals->observe(TCHAR_TO_UTF8(*name), [callback]() { callback.Execute(); }); + TSharedPtr token = MakeShared(true); + mObserverTokens.Add(token); + // Capture token by value; if it is set to false the callback is skipped. + // Use TWeakObjectPtr for the bound UObject inside the delegate to avoid + // keeping it alive longer than the GC would otherwise. + mpGlobals->observe(TCHAR_TO_UTF8(*name), [token, callback]() { + if (token.IsValid() && *token) { + callback.ExecuteIfBound(); + } + }); + return FInkHandle(token); } -void AInkRuntime::ObserverVariableEvent( +FInkHandle AInkRuntime::ObserverVariableEvent( const FString& name, const FVariableCallbackDelegateNewValue& callback ) { - mpGlobals->observe(TCHAR_TO_UTF8(*name), [callback](ink::runtime::value x) { - callback.Execute(FInkVar(x)); + TSharedPtr token = MakeShared(true); + mObserverTokens.Add(token); + mpGlobals->observe(TCHAR_TO_UTF8(*name), [token, callback](ink::runtime::value x) { + if (token.IsValid() && *token) { + callback.ExecuteIfBound(FInkVar(x)); + } }); + return FInkHandle(token); } -void AInkRuntime::ObserverVariableChange( +FInkHandle AInkRuntime::ObserverVariableChange( const FString& name, const FVariableCallbackDelegateNewOldValue& callback ) { + TSharedPtr token = MakeShared(true); + mObserverTokens.Add(token); mpGlobals->observe( TCHAR_TO_UTF8(*name), - [callback](ink::runtime::value x, ink::optional y) { - if (y.has_value()) { - callback.Execute(FInkVar(x), FInkVar(y.value())); - } else { - callback.Execute(FInkVar(x), FInkVar()); + [token, callback](ink::runtime::value x, ink::optional y) { + if (token.IsValid() && *token) { + callback.ExecuteIfBound(FInkVar(x), y.has_value() ? FInkVar(y.value()) : FInkVar()); } } ); + return FInkHandle(token); } diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkSnapshot.cpp b/unreal/inkcpp/Source/inkcpp/Private/InkSnapshot.cpp new file mode 100644 index 00000000..385eab3a --- /dev/null +++ b/unreal/inkcpp/Source/inkcpp/Private/InkSnapshot.cpp @@ -0,0 +1,41 @@ +#include "InkSnapshot.h" +#include "InkRuntime.h" + +#include "Async/Async.h" + +UInkMigratableSnapshotAsync* UInkMigratableSnapshotAsync::GetMigratableSnapshot(AInkRuntime* Runtime +) +{ + UInkMigratableSnapshotAsync* Node = NewObject(); + Node->Runtime = Runtime; + return Node; +} + +void UInkMigratableSnapshotAsync::Activate() +{ + if (! Runtime) { + Completed.Broadcast(FInkSnapshot()); + SetReadyToDestroy(); + return; + } + + TFuture Future = Runtime->MigratableSnapshot(); + + TWeakObjectPtr WeakThis(this); + + Future.Next([WeakThis](FInkSnapshot Snapshot) { + AsyncTask(ENamedThreads::GameThread, [WeakThis, Snapshot]() { + if (! WeakThis.IsValid()) + return; + + WeakThis->Completed.Broadcast(Snapshot); + WeakThis->SetReadyToDestroy(); + }); + }); +} + +void UInkMigratableSnapshotAsync::HandleResult(const FInkSnapshot& Snapshot) +{ + Completed.Broadcast(Snapshot); + SetReadyToDestroy(); +} diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkThread.cpp b/unreal/inkcpp/Source/inkcpp/Private/InkThread.cpp index 119ef2c9..e39ad303 100644 --- a/unreal/inkcpp/Source/inkcpp/Private/InkThread.cpp +++ b/unreal/inkcpp/Source/inkcpp/Private/InkThread.cpp @@ -11,10 +11,13 @@ #include "TagList.h" #include "InkChoice.h" #include "ink/runner.h" +#include "InkExecutionScope.h" // Unreal includes #include "Internationalization/Regex.h" +thread_local UInkThread* GExecutingInkThread = nullptr; + UInkThread::UInkThread() : mbHasRun(false) , mnChoiceToChoose(-1) @@ -26,6 +29,23 @@ UInkThread::UInkThread() UInkThread::~UInkThread() {} +void UInkThread::RegisterLiveList(UInkList* list) +{ + if (list) { + mLiveLists.Add(list); + } +} + +void UInkThread::InvalidateLiveLists() +{ + for (auto& weak : mLiveLists) { + if (weak.IsValid()) { + weak->Invalidate(); + } + } + mLiveLists.Reset(); +} + void UInkThread::Yield() { mnYieldCounter++; } bool UInkThread::IsYielding() { return mnYieldCounter > 0; } @@ -54,24 +74,81 @@ const UTagList* UInkThread::GetGlobalTags() void UInkThread::Resume() { mnYieldCounter--; } -void UInkThread::RegisterTagFunction(FName functionName, const FTagFunctionDelegate& function) +FInkHandle UInkThread::RegisterTagFunction(FName functionName, const FTagFunctionDelegate& function) { - // Register tag function - mTagFunctions.FindOrAdd(functionName).Add(function); + TSharedPtr token = MakeShared(true); + mTagFunctionTokens.FindOrAdd(functionName).Add(token); + mTagFunctionDelegates.FindOrAdd(functionName).Add(function); + return FInkHandle(token); } -void UInkThread::RegisterExternalFunction( +FInkHandle UInkThread::RegisterExternalFunction( const FString& functionName, const FExternalFunctionDelegate& function, bool lookaheadSafe ) { - mpRunner->bind_delegate(ink::hash_string(TCHAR_TO_UTF8(*functionName)), function, lookaheadSafe); + TSharedPtr token = MakeShared(true); + uint32 nameHash = ink::hash_string(TCHAR_TO_UTF8(*functionName)); + // If a previous binding exists for this name, invalidate it + if (auto* prev = mExternalFunctionTokens.Find(nameHash)) { + if (prev->IsValid()) { + **prev = false; + } + } + mExternalFunctionTokens.Add(nameHash, token); + // Bind a wrapper lambda that checks the token before forwarding + mpRunner->bind( + nameHash, + [token, function](size_t argc, const ink::runtime::value* argv) -> ink::runtime::value { + if (token.IsValid() && *token) { + TArray args; + for (size_t i = 0; i < argc; ++i) { + args.Add(FInkVar(argv[i])); + } + return function.Execute(args).to_value(); + } + return ink::runtime::value{}; + }, + lookaheadSafe + ); + return FInkHandle(token); } -void UInkThread::RegisterExternalEvent( +FInkHandle UInkThread::RegisterExternalEvent( const FString& functionName, const FExternalFunctionVoidDelegate& function, bool lookaheadSafe ) { - mpRunner->bind_delegate(ink::hash_string(TCHAR_TO_UTF8(*functionName)), function, lookaheadSafe); + TSharedPtr token = MakeShared(true); + uint32 nameHash = ink::hash_string(TCHAR_TO_UTF8(*functionName)); + if (auto* prev = mExternalFunctionTokens.Find(nameHash)) { + if (prev->IsValid()) { + **prev = false; + } + } + mExternalFunctionTokens.Add(nameHash, token); + mpRunner->bind( + nameHash, + [token, function](size_t argc, const ink::runtime::value* argv) { + if (token.IsValid() && *token) { + TArray args; + for (size_t i = 0; i < argc; ++i) { + args.Add(FInkVar(argv[i])); + } + function.ExecuteIfBound(args); + } + }, + lookaheadSafe + ); + return FInkHandle(token); +} + +void UInkThread::ClearExternalFunctions() +{ + for (auto& pair : mExternalFunctionTokens) { + if (pair.Value.IsValid()) { + *pair.Value = false; + } + } + mExternalFunctionTokens.Empty(); } void UInkThread::Initialize(FString path, AInkRuntime* runtime, ink::runtime::runner thread) @@ -87,7 +164,10 @@ void UInkThread::Initialize(FString path, AInkRuntime* runtime, ink::runtime::ru mpTags = NewObject(); mkTags = NewObject(); mgTags = NewObject(); - mTagFunctions.Reset(); + mTagFunctionTokens.Reset(); + mTagFunctionDelegates.Reset(); + // Note: external function tokens are intentionally NOT cleared here. + // Call ClearExternalFunctions() explicitly before reuse via StartExisting(). mCurrentChoices.Reset(); mnChoiceToChoose = -1; mbHasRun = false; @@ -127,7 +207,11 @@ bool UInkThread::ExecuteInternal() // Handle pending choice if (mnChoiceToChoose != -1) { if (ensure(mpRunner->num_choices() > 0)) { + // Invalidate all UInkList objects created from runner memory + // before the runner state changes. + InvalidateLiveLists(); mpRunner->choose(mnChoiceToChoose); + mpRuntime->RunnerEnterStableState(this); } mnChoiceToChoose = -1; mCurrentChoices.Empty(); @@ -223,10 +307,20 @@ bool UInkThread::ExecuteInternal() void UInkThread::ExecuteTagMethod(const TArray& Params) { - // Look for method and execute with parameters - FTagFunctionMulticastDelegate* function = mTagFunctions.Find(FName(*Params[0])); - if (function != nullptr) { - function->Broadcast(this, Params); + FName name(*Params[0]); + + // Fire thread-local tag functions + auto* tokens = mTagFunctionTokens.Find(name); + auto* delegates = mTagFunctionDelegates.Find(name); + if (tokens && delegates) { + for (int32 i = tokens->Num() - 1; i >= 0; --i) { + if ((*tokens)[i].IsValid() && *(*tokens)[i]) { + (*delegates)[i].ExecuteIfBound(this, Params); + } else { + tokens->RemoveAtSwap(i); + delegates->RemoveAtSwap(i); + } + } } // Forward to runtime @@ -235,17 +329,17 @@ void UInkThread::ExecuteTagMethod(const TArray& Params) bool UInkThread::Execute() { - // Execute thread + // RAII guard: sets GExecutingInkThread for this scope so that FInkVar + // constructors can register newly created UInkList wrappers with this + // thread. Automatically cleared on return (including via exception). + FInkExecutionScope scope(this); + bool finished = ExecuteInternal(); - // If we've finished, run callback if (finished) { - // Allow outsiders to subscribe - // TODO: OnThreadShutdown.Broadcast(); OnShutdown(); } - // Return result return finished; } diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkVar.cpp b/unreal/inkcpp/Source/inkcpp/Private/InkVar.cpp index 0a6d399b..4b6032f9 100644 --- a/unreal/inkcpp/Source/inkcpp/Private/InkVar.cpp +++ b/unreal/inkcpp/Source/inkcpp/Private/InkVar.cpp @@ -5,134 +5,148 @@ * https://github.com/JBenda/inkcpp for full license details. */ #include "InkVar.h" +#include "InkExecutionScope.h" +#include "InkThread.h" #include "ink/types.h" #include "Misc/AssertionMacros.h" -FInkVar::FInkVar(ink::runtime::value val) : FInkVar() { +extern thread_local UInkThread* GExecutingInkThread; + +FInkVar::FInkVar(ink::runtime::value val) + : FInkVar() +{ using v_types = ink::runtime::value::Type; - switch(val.type) { - case v_types::Bool: value.SetSubtype(val.get()); break; + switch (val.type) { + case v_types::Bool: + BoolVal = val.get(); + VarType = EInkVarType::Bool; + break; case v_types::Uint32: - UE_LOG(InkCpp, Warning, TEXT("Converting uint to int, this will cause trouble if writing it back to ink (with SetGlobalVariable)!")); - value.SetSubtype(val.get()); + UE_LOG( + InkCpp, Warning, + TEXT("Converting uint to int, this will cause trouble if writing it back to ink (with " + "SetGlobalVariable)!") + ); + IntVal = ( int32 ) val.get(); + VarType = EInkVarType::Int; + break; + case v_types::Int32: + IntVal = val.get(); + VarType = EInkVarType::Int; + break; + case v_types::String: + StringVal = FString(UTF8_TO_TCHAR(val.get())); + VarType = EInkVarType::String; + break; + case v_types::Float: + FloatVal = val.get(); + VarType = EInkVarType::Float; break; - case v_types::Int32: value.SetSubtype(val.get()); break; - case v_types::String: value.SetSubtype(FString(val.get())); break; - case v_types::Float: value.SetSubtype(val.get()); break; case v_types::List: { - UInkList* list = NewObject(); - list->SetList(val.get()); - value.SetSubtype(list); + ListVal = NewObject(); + ListVal->SetList(val.get()); + VarType = EInkVarType::List; + // Register with the executing thread (if any) via the RAII-guarded + // thread-local so the thread can invalidate this list before choose(). + // GExecutingInkThread is null when called outside Execute() (e.g. + // GetGlobalVariable), in which case the list is globals-lifetime and + // needs no per-choice invalidation. + if (GExecutingInkThread) { + GExecutingInkThread->RegisterLiveList(ListVal); + } break; } - default: - inkFail("unknown type!, failed to convert ink::value to InkVar"); + default: inkFail("unknown type!, failed to convert ink::value to InkVar"); + } +} + +FInkVar::FInkVar(UInkList& List) + : VarType(EInkVarType::List) + , IntVal(0) + , ListVal(&List) +{ + if (GExecutingInkThread) { + GExecutingInkThread->RegisterLiveList(ListVal); } } - -ink::runtime::value FInkVar::to_value() const { - switch(type()) { - case EInkVarType::Int: - return ink::runtime::value(value.GetSubtype()); - case EInkVarType::Float: - return ink::runtime::value(value.GetSubtype()); + +ink::runtime::value FInkVar::to_value() const +{ + switch (VarType) { + case EInkVarType::Int: return ink::runtime::value(IntVal); + case EInkVarType::Float: return ink::runtime::value(FloatVal); case EInkVarType::String: - return ink::runtime::value(reinterpret_cast(utf8.GetData())); - case EInkVarType::Bool: - return ink::runtime::value(value.GetSubtype()); - case EInkVarType::UInt: - return ink::runtime::value(value.GetSubtype()); - case EInkVarType::List: - return ink::runtime::value(value.GetSubtype()->GetData()); - default: - inkFail("Unsupported type"); - return ink::runtime::value(); + return ink::runtime::value(reinterpret_cast(Utf8.GetData())); + case EInkVarType::Bool: return ink::runtime::value(BoolVal); + case EInkVarType::UInt: return ink::runtime::value(UIntVal); + case EInkVarType::List: return ink::runtime::value(ListVal->GetData()); + default: inkFail("Unsupported type"); return ink::runtime::value(); } - } -EInkVarType UInkVarLibrary::InkVarType(const FInkVar& InkVar) { return InkVar.type(); } +EInkVarType UInkVarLibrary::InkVarType(const FInkVar& InkVar) { return InkVar.VarType; } + FString UInkVarLibrary::Conv_InkVarString(const FInkVar& InkVar) { - if (ensureMsgf(InkVar.type() == EInkVarType::String, TEXT("InkVar is not a String Type!"))) - return InkVar.value.GetSubtype(); + if (ensureMsgf(InkVar.VarType == EInkVarType::String, TEXT("InkVar is not a String Type!"))) + return InkVar.StringVal; return FString(TEXT("")); } int UInkVarLibrary::Conv_InkVarInt(const FInkVar& InkVar) { - if (ensureMsgf(InkVar.type() == EInkVarType::Int, TEXT("InkVar is not an Int Type!"))) - return InkVar.value.GetSubtype(); + if (ensureMsgf(InkVar.VarType == EInkVarType::Int, TEXT("InkVar is not an Int Type!"))) + return InkVar.IntVal; return 0; } float UInkVarLibrary::Conv_InkVarFloat(const FInkVar& InkVar) { - if (ensureMsgf(InkVar.type() == EInkVarType::Float, TEXT("InkVar is not a Float Type!"))) - return InkVar.value.GetSubtype(); + if (ensureMsgf(InkVar.VarType == EInkVarType::Float, TEXT("InkVar is not a Float Type!"))) + return InkVar.FloatVal; return 0.f; } FName UInkVarLibrary::Conv_InkVarName(const FInkVar& InkVar) { - if (ensureMsgf(InkVar.type() == EInkVarType::String, TEXT("InkVar is not a String Type!"))) - return FName(*InkVar.value.GetSubtype()); + if (ensureMsgf(InkVar.VarType == EInkVarType::String, TEXT("InkVar is not a String Type!"))) + return FName(*InkVar.StringVal); return NAME_None; } FText UInkVarLibrary::Conv_InkVarText(const FInkVar& InkVar) { - if (ensureMsgf(InkVar.type() == EInkVarType::String, TEXT("InkVar is not a String Type!"))) - return FText::FromString(InkVar.value.GetSubtype()); + if (ensureMsgf(InkVar.VarType == EInkVarType::String, TEXT("InkVar is not a String Type!"))) + return FText::FromString(InkVar.StringVal); return FText::GetEmpty(); } bool UInkVarLibrary::Conv_InkVarBool(const FInkVar& InkVar) { - if (ensureMsgf(InkVar.type() == EInkVarType::Bool, TEXT("InkVar is not an Bool Type!"))) - return InkVar.value.GetSubtype(); + if (ensureMsgf(InkVar.VarType == EInkVarType::Bool, TEXT("InkVar is not an Bool Type!"))) + return InkVar.BoolVal; return false; } const UInkList* UInkVarLibrary::Conv_InkVarInkList(const FInkVar& InkVar) { - if (ensureMsgf(InkVar.type() == EInkVarType::List, TEXT("InkVar is not an List Type!"))) - return InkVar.value.GetSubtype(); + if (ensureMsgf(InkVar.VarType == EInkVarType::List, TEXT("InkVar is not an List Type!"))) + return InkVar.ListVal; return nullptr; } +FInkVar UInkVarLibrary::Conv_StringInkVar(const FString& String) { return FInkVar(String); } +FInkVar UInkVarLibrary::Conv_IntInkVar(int Number) { return FInkVar(Number); } -FInkVar UInkVarLibrary::Conv_StringInkVar(const FString& String) -{ - return FInkVar(String); -} +FInkVar UInkVarLibrary::Conv_FloatInkVar(float Number) { return FInkVar(Number); } -FInkVar UInkVarLibrary::Conv_IntInkVar(int Number) -{ - return FInkVar(Number); -} +FInkVar UInkVarLibrary::Conv_TextInkVar(const FText& Text) { return FInkVar(Text.ToString()); } -FInkVar UInkVarLibrary::Conv_FloatInkVar(float Number) -{ - return FInkVar(Number); -} +FInkVar UInkVarLibrary::Conv_NameInkVar(const FName& Name) { return FInkVar(Name.ToString()); } -FInkVar UInkVarLibrary::Conv_TextInkVar(const FText& Text) -{ - return FInkVar(Text.ToString()); -} - -FInkVar UInkVarLibrary::Conv_NameInkVar(const FName& Name) -{ - return FInkVar(Name.ToString()); -} - -FInkVar UInkVarLibrary::Conv_BoolInkVar(bool Boolean) -{ - return FInkVar(Boolean); -} +FInkVar UInkVarLibrary::Conv_BoolInkVar(bool Boolean) { return FInkVar(Boolean); } FInkVar UInkVarLibrary::Conv_ListInkVar(UInkList* List) { diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkChoice.h b/unreal/inkcpp/Source/inkcpp/Public/InkChoice.h index b256ddd6..16d140da 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkChoice.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkChoice.h @@ -9,29 +9,33 @@ #include "UObject/Object.h" +#include "ink/choice.h" + #include "InkChoice.generated.h" -namespace ink::runtime { class choice; } + class UTagList; /** Representing a Ink Choice in the story flow * @ingroup unreal */ UCLASS(BlueprintType) -class UInkChoice : public UObject + +class INKCPP_API UInkChoice : public UObject { GENERATED_BODY() + public: UInkChoice(); - UFUNCTION(BlueprintPure, Category="Ink") + UFUNCTION(BlueprintPure, Category = "Ink") /** Access context of choice. * @return text contained in choice * @blueprint */ FString GetText() const; - UFUNCTION(BlueprintPure, Category="Ink") + UFUNCTION(BlueprintPure, Category = "Ink") /** Get identifier for @ref UInkThread::PickChoice() * @return id used in @ref UInkThread::PickChoice() * @@ -39,7 +43,7 @@ class UInkChoice : public UObject */ int GetIndex() const; - UFUNCTION(BlueprintPure, Category="Ink") + UFUNCTION(BlueprintPure, Category = "Ink") /** Tags associated with the choice. * @return with choice associated tags * @@ -49,10 +53,13 @@ class UInkChoice : public UObject protected: friend class UInkThread; - /** @private */ - void Initialize(const ink::runtime::choice*); + /** Copies choice data out of the runner. Safe to call after the runner advances. + * @private + */ + void Initialize(const ink::runtime::choice* c); private: - const ink::runtime::choice* data; + FString Text; + int Index = -1; TObjectPtr tags; }; diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkHandles.h b/unreal/inkcpp/Source/inkcpp/Public/InkHandles.h new file mode 100644 index 00000000..8f9971d0 --- /dev/null +++ b/unreal/inkcpp/Source/inkcpp/Public/InkHandles.h @@ -0,0 +1,55 @@ +/* Copyright (c) 2024 Julian Benda + * + * This file is part of inkCPP which is released under MIT license. + * See file LICENSE.txt or go to + * https://github.com/JBenda/inkcpp for full license details. + */ +#pragma once + +#include "CoreMinimal.h" +#include "Templates/SharedPointer.h" + +#include "InkHandles.generated.h" + +/** + * Generic registration handle returned by all Register* functions on + * @ref AInkRuntime and @ref UInkThread. + * + * Call @ref Cancel() to cancel the registration. Letting the handle go out of scope does NOT + * automatically cancel — you must call Cancel() explicitly. + * For Blueprint graphs, pass the handle to @ref AInkRuntime::Unregister() or + * @ref UInkThread::Unregister() — those are thin wrappers around Cancel(). + * + * The same handle type is used for variable observers, tag functions, + * and external functions — the registering function name already makes + * the context clear at the call site. + * + * @ingroup unreal + */ +USTRUCT(BlueprintType) + +struct INKCPP_API FInkHandle { + GENERATED_BODY() + + /** @private */ + FInkHandle() {} + + /** @private */ + explicit FInkHandle(TSharedPtr token) + : Token(MoveTemp(token)) + { + } + + /** Returns true if this handle refers to an active registration. */ + bool IsValid() const { return Token.IsValid() && *Token; } + + /** Cancels the registration this handle refers to. Safe to call multiple times. */ + void Cancel() const + { + if (Token.IsValid()) + *Token = false; + } + + /** @private */ + TSharedPtr Token; +}; diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkList.h b/unreal/inkcpp/Source/inkcpp/Public/InkList.h index 7bb6099a..ecd79b2e 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkList.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkList.h @@ -6,18 +6,22 @@ */ #pragma once +#include "CoreMinimal.h" + #include "InkList.generated.h" -namespace ink::runtime { - class list_interface; - using list = list_interface*; -} +namespace ink::runtime +{ +class list_interface; +using list = list_interface*; +} // namespace ink::runtime /** A single flag of a list. * @ingroup unreal */ USTRUCT(BlueprintType) -struct FListFlag { + +struct INKCPP_API FListFlag { GENERATED_BODY() UPROPERTY(BlueprintReadOnly, Category = "Ink") /** name of the list, the flag is part of */ @@ -29,9 +33,16 @@ struct FListFlag { /** * Allows reading ink lists. + * + * A UInkList wraps a pointer into runner-owned memory. It is only valid until + * the runner advances (i.e. until the next getline() or choose()). After that + * @ref IsValid() returns false and all accessor methods become no-ops that + * return empty/default values. Always check @ref IsValid() before using a list + * that was not obtained in the same Blueprint event. * @ingroup unreal */ UCLASS(Blueprintable, BlueprintType) + class INKCPP_API UInkList : public UObject { GENERATED_BODY() @@ -41,15 +52,37 @@ class INKCPP_API UInkList : public UObject UInkList() {} /** @private */ - UInkList(ink::runtime::list list) { list_data = list; } + UInkList(ink::runtime::list list) + : list_data(list) + { + } /** @private */ - void SetList(ink::runtime::list list) { list_data = list; } + void SetList(ink::runtime::list list) + { + list_data = list; + bValid = true; + } + + /** @private — called by UInkThread before advancing the runner */ + void Invalidate() { bValid = false; } + + UFUNCTION(BlueprintPure, Category = "Ink") + + /** + * Returns whether this list object still points to valid runner memory. + * A list becomes invalid once the runner advances past the line or variable + * read that produced it. Do not call any other methods on an invalid list. + * + * @blueprint + */ + bool IsValid() const { return bValid && list_data != nullptr; } UFUNCTION(BlueprintPure, Category = "Ink") /** Checks if a flag is contained (by name) * @attention If the flag name is not unique please use the full flag name aka * `list_name.flag_name` + * @retval false if the list is no longer valid * * @blueprint */ @@ -58,6 +91,7 @@ class INKCPP_API UInkList : public UObject UFUNCTION(BlueprintPure, Category = "Ink") /** checks if flag with the same spelling then the enum `value` is set in the list * @retval true if flag is contained in list + * @retval false if the list is no longer valid * * @blueprint */ @@ -65,13 +99,15 @@ class INKCPP_API UInkList : public UObject UFUNCTION(BlueprintPure, Category = "Ink") /** returns all values of a list with the same name as the enum + * @retval empty array if the list is no longer valid * * @blueprint */ TArray ElementsOf(const UEnum* Enum) const; UFUNCTION(BlueprintPure, Category = "Ink") - /** returns all flag as string contained in the list from a list with the same name as the Enum` + /** returns all flag as string contained in the list from a list with the same name as the Enum + * @retval empty array if the list is no longer valid * * @blueprint */ @@ -79,6 +115,7 @@ class INKCPP_API UInkList : public UObject UFUNCTION(BlueprintPure, Category = "Ink") /** returns all `list_name` `flag_name` tuples + * @retval empty array if the list is no longer valid * * @blueprint */ @@ -87,6 +124,7 @@ class INKCPP_API UInkList : public UObject UFUNCTION(BlueprintPure, Category = "Ink") /** check if at least one value of the given list is included, OR the list is empty * and associated with the list + * @retval false if the list is no longer valid * * @blueprint */ @@ -98,5 +136,6 @@ class INKCPP_API UInkList : public UObject ink::runtime::list GetData() const { return list_data; } - ink::runtime::list list_data; + ink::runtime::list list_data = nullptr; + bool bValid = true; }; diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkRuntime.h b/unreal/inkcpp/Source/inkcpp/Public/InkRuntime.h index 27f2905b..a5098972 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkRuntime.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkRuntime.h @@ -9,9 +9,11 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Misc/Optional.h" +#include "Async/Future.h" #include "InkDelegates.h" #include "InkSnapshot.h" +#include "InkHandles.h" #include "ink/types.h" #include "ink/globals.h" @@ -21,17 +23,22 @@ class UInkThread; struct FInkVar; -namespace ink::runtime { class story; } + +namespace ink::runtime +{ +class story; +} // namespace ink::runtime /** Instantiated story with global variable storage and access, used to instantiate new threads. * @ingroup unreal */ UCLASS() + class INKCPP_API AInkRuntime : public AActor { GENERATED_BODY() - -public: + +public: // Sets default values for this actor's properties AInkRuntime(); ~AInkRuntime(); @@ -53,14 +60,34 @@ class INKCPP_API AInkRuntime : public AActor */ UInkThread* StartExisting(UInkThread* thread, FString path = "", bool runImmediately = true); - UFUNCTION(BlueprintCallable, Category="Ink") + UFUNCTION(BlueprintCallable, Category = "Ink") /** creates a snapshot of the current runtime state. - * can be loladed with @ref #LoadSnapshot() + * can be loaded with @ref #LoadSnapshot() + * + * @attention this snapshot can only be loaded with the same story, for migratable snapshots + * please use UInkMigratableSnapshotAsync::UInkMigratableSnapshotAsync() + * + * make snapshot, if save? Return + * every time a thread chooses something + * yield this thread + * try to make snapshot, if save? fullfill promise and resume all threads * * @blueprint */ FInkSnapshot Snapshot(); + /** creates a snapshot the next time the story is in a stable state. + * for Blueprints please use snapshotAsync::UInkMigratableSnapshotAsync() + * This snapshot can be loaded with a new version of the same story. + * can be loaded with @ref #LoadSnapshot() + * + * @attention typical this snapshot will be created after the next choice is taken. + * To archive this each active runner will yield after the next choice and only continue after the + * snapshot is taken. + * + */ + TFuture MigratableSnapshot(); + UFUNCTION(BlueprintCallable, Category = "Ink") /** * Loads a snapshot file, therefore deletes globals and invalidate all current Threads @@ -71,7 +98,7 @@ class INKCPP_API AInkRuntime : public AActor void LoadSnapshot(const FInkSnapshot& snapshot); - UFUNCTION(BlueprintCallable, Category="Ink") + UFUNCTION(BlueprintCallable, Category = "Ink") /** Marks a thread as "exclusive". * As long as it is running, no other threads will update. * @see #PopExclusiveThread() @@ -80,27 +107,42 @@ class INKCPP_API AInkRuntime : public AActor */ void PushExclusiveThread(UInkThread* Thread); - UFUNCTION(BlueprintCallable, Category="Ink") + UFUNCTION(BlueprintCallable, Category = "Ink") /** Removes a thread from the exclusive stack. * @see #PushExclusiveThread() * * @blueprint */ void PopExclusiveThread(UInkThread* Thread); - - UFUNCTION(BlueprintCallable, Category="Ink") + + UFUNCTION(BlueprintCallable, Category = "Ink") /** register a "tag function" - * This function is executed if context or a tag in a special format appears + * This function is executed if context or a tag in a special format appears. + * @return handle — call Cancel() to remove this binding (or pass to Unregister() from Blueprint) * @see @ref TagFunction * * @blueprint */ - void RegisterTagFunction(FName functionName, const FTagFunctionDelegate & function); + FInkHandle RegisterTagFunction(FName functionName, const FTagFunctionDelegate& function); + + UFUNCTION(BlueprintCallable, Category = "Ink") + + /** Stop receiving variable-change notifications or unregister a tag function. + * Prefer calling @ref FInkHandle::Cancel() directly — that does not require the runtime. + * @param handle the handle returned by ObserverVariable / ObserverVariableEvent / + * ObserverVariableChange / RegisterTagFunction + * + * @blueprint + */ + void Unregister(const FInkHandle& handle) { handle.Cancel(); } /** @private for internal use */ void HandleTagFunction(UInkThread* Caller, const TArray& Params); - - UFUNCTION(BlueprintCallable, Category="Ink") + + /** @private for internal use */ + void RunnerEnterStableState(UInkThread* thread); + + UFUNCTION(BlueprintCallable, Category = "Ink") /** Access a variable from the ink runtime. * variables are shared between all threads in the same runtime. * @param name of variable in ink script @@ -108,8 +150,8 @@ class INKCPP_API AInkRuntime : public AActor * @blueprint */ FInkVar GetGlobalVariable(const FString& name); - - UFUNCTION(BlueprintCallable, Category="Ink") + + UFUNCTION(BlueprintCallable, Category = "Ink") /** Sets a global variable inside the ink runtime. * @param name of variable in ink script * @param value new value for the variable @@ -118,60 +160,80 @@ class INKCPP_API AInkRuntime : public AActor */ void SetGlobalVariable(const FString& name, const FInkVar& value); - UFUNCTION(BlueprintCallable, Category="Ink") - /** Gets a ping if variable changes + UFUNCTION(BlueprintCallable, Category = "Ink") + /** Gets a ping if variable changes. + * @return handle — call Cancel() to remove this binding (or pass to Unregister() from Blueprint) * @see #ObserverVariableEvent() #ObserverVariableChange() * * @blueprint */ - void ObserverVariable(const FString& variableName, const FVariableCallbackDelegate& callback); + FInkHandle + ObserverVariable(const FString& variableName, const FVariableCallbackDelegate& callback); - UFUNCTION(BlueprintCallable, Category="Ink") - /** On variable change provides new value + UFUNCTION(BlueprintCallable, Category = "Ink") + /** On variable change provides new value. + * @return handle — call Cancel() to remove this binding (or pass to Unregister() from Blueprint) * @see #ObserverVariable() #ObserverVariableChange() * * @blueprint */ - void ObserverVariableEvent(const FString& variableName, const FVariableCallbackDelegateNewValue& callback); + FInkHandle ObserverVariableEvent( + const FString& variableName, const FVariableCallbackDelegateNewValue& callback + ); - UFUNCTION(BlueprintCallable, Category="Ink") + UFUNCTION(BlueprintCallable, Category = "Ink") /** On variable change provides old and new value. - * @see #ObserverVariableEvent() #ObserverVariable() - * @attention if the variable set for the firs time, the old value has value type @ref + * @return handle — call Cancel() to remove this binding (or pass to Unregister() from Blueprint) + * @attention if the variable set for the first time, the old value has value type @ref * EInkVarType::None + * @see #ObserverVariableEvent() #ObserverVariable() * * @blueprint */ - void ObserverVariableChange(const FString& variableName, const FVariableCallbackDelegateNewOldValue& callback); + FInkHandle ObserverVariableChange( + const FString& variableName, const FVariableCallbackDelegateNewOldValue& callback + ); protected: /** Called when the game starts or when spawned */ virtual void BeginPlay() override; -public: + /** Called when the actor is being removed from play */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +public: // Called every frame /** @private */ virtual void Tick(float DeltaTime) override; // Story asset used in this level - UPROPERTY(EditAnywhere, Category="Ink") + UPROPERTY(EditAnywhere, Category = "Ink") /** @private */ class UInkAsset* InkAsset; private: - ink::runtime::story* mpRuntime; + ink::runtime::story* mpRuntime; ink::runtime::globals mpGlobals; UPROPERTY() TArray mThreads; - - TMap mGlobalTagFunctions; + + /** Token storage for tag function registrations. Maps function name → parallel arrays of + * tokens and delegates. Setting a token to false skips and lazily removes that entry. */ + TMap>> mTagFunctionTokens; + TMap> mTagFunctionDelegates; UPROPERTY() TArray mExclusiveStack; - - // UPROPERTY(EditDefaultsOnly, Category="Ink") - TOptional mSnapshot; + // snapshot generates data when re-constructing the globals to allow reconstructing the threads - ink::runtime::snapshot* mpSnapshot; + TOptional mSnapshot; + ink::runtime::snapshot* mpSnapshot = nullptr; + + /** Active observer tokens. When Cancel() is called on the handle the token is set to false, + * the lambda checks it before firing and skips. Tokens are cleaned up lazily. */ + TArray> mObserverTokens; + TSharedPtr> mStableSnapshot; + UPROPERTY() + TArray mYieldedThreadsForSnapshot; }; diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h b/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h index bb7d6d7c..3207ff66 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h @@ -6,6 +6,8 @@ */ #pragma once +#include "Kismet/BlueprintAsyncActionBase.h" + #include "InkSnapshot.generated.h" /** A serializable snapshot of a runtime state @@ -13,18 +15,70 @@ * @ingroup unreal */ USTRUCT(BlueprintType) -struct INKCPP_API FInkSnapshot -{ + +struct INKCPP_API FInkSnapshot { GENERATED_BODY() - FInkSnapshot() {} + + FInkSnapshot() + : Migratable(false) + { + } /** @private */ - FInkSnapshot(const char* snap_data, size_t snap_len) + FInkSnapshot(const char* snap_data, size_t snap_len, bool migratable) : data(reinterpret_cast(snap_data), snap_len) - {} + , Migratable(migratable) + { + } UPROPERTY(BlueprintReadWrite, SaveGame, Category = "ink|SaveGame") /** Raw data used to restore runtime state. * not needed if a USaveGame is used. */ TArray data; + + UPROPERTY(BlueprintReadOnly, SaveGame, Category = "ink|SaveGame") + /** Is true if the snapshot is migratable. + */ + bool Migratable; +}; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( + FInkMigratableSnapshotCompleted, const FInkSnapshot&, Snapshot +); + +/** A helper class to create migratable snapshots. + * creating an instance with ::GetMigratableSnapshot() will @ref UInkThread::Yield() "yield" each + * assoziated thread after the next choice until a migratable snapshot can be cerated. all threads + * will then be @ref UInkThread::Resume() "resumed". + * @attention if a thread is inside a tunnel it will still yield after a choice and will then stop + * at an point where it cannot create a valid migratable snapshot, fix still pending. + * @ingroup unreal + */ +UCLASS(BlueprintType) + +class INKCPP_API UInkMigratableSnapshotAsync : public UBlueprintAsyncActionBase +{ + GENERATED_BODY() + +public: + /** @private */ + UPROPERTY(BlueprintAssignable) + FInkMigratableSnapshotCompleted Completed; + + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true")) + /** Tries to create a migratable snapshot, on completion returns it. + * see @ref UInkMigratableSnapshotAsync for more details. + * + * @blueprint{ActionBase, Snapshot} + */ + static UInkMigratableSnapshotAsync* GetMigratableSnapshot(AInkRuntime* Runtime); + + /** @private */ + virtual void Activate() override; + +private: + UPROPERTY() + TObjectPtr Runtime; + + void HandleResult(const FInkSnapshot& Snapshot); }; diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkThread.h b/unreal/inkcpp/Source/inkcpp/Public/InkThread.h index b8abaf0b..53bad52b 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkThread.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkThread.h @@ -7,10 +7,13 @@ #pragma once #include "CoreMinimal.h" +#include "InkRuntime.h" #include "UObject/NoExportTypes.h" #include "InkVar.h" #include "InkDelegates.h" +#include "InkList.h" +#include "InkHandles.h" #include "ink/runner.h" @@ -26,6 +29,7 @@ class UInkChoice; * @ingroup unreal */ UCLASS(Blueprintable) + class INKCPP_API UInkThread : public UObject { GENERATED_BODY() @@ -146,37 +150,56 @@ class INKCPP_API UInkThread : public UObject // Registers a callback for a named "tag function" UFUNCTION(BlueprintCallable, Category = "Ink") - /** Register a callback for a named "tag function" - * @see @ref TagFunction + /** Register a callback for a named "tag function". + * @return handle — call Cancel() to remove this binding (or pass to Unregister() from Blueprint) * * @blueprint */ - void RegisterTagFunction(FName functionName, const FTagFunctionDelegate& function); + FInkHandle RegisterTagFunction(FName functionName, const FTagFunctionDelegate& function); UFUNCTION(BlueprintCallable, Category = "Ink") - /** register a external function. - * A function provides a return value + /** Register an external function that returns a value. + * @return handle — call Cancel() to remove this binding (or pass to Unregister() from Blueprint) * @see if you do not want to return something #RegisterExternalEvent() * * @blueprint */ - void RegisterExternalFunction( + FInkHandle RegisterExternalFunction( const FString& functionName, const FExternalFunctionDelegate& function, bool lookaheadSafe = false ); UFUNCTION(BlueprintCallable, Category = "Ink") - /** register external event. - * A event has the return type void. - * @see If you want to return a value use #RegisterExternalFunction() + /** Register an external event (void return). + * @return handle — call Cancel() to remove this binding (or pass to Unregister() from Blueprint) + * @see If you want to return a value use #RegisterExternalFunction() * * @blueprint */ - void RegisterExternalEvent( + FInkHandle RegisterExternalEvent( const FString& functionName, const FExternalFunctionVoidDelegate& function, bool lookaheadSafe = false ); + UFUNCTION(BlueprintCallable, Category = "Ink") + + /** Unregister a previously registered external function, event, or tag function. + * Prefer calling @ref FInkHandle::Cancel() directly — that does not require the thread. + * @param handle the handle returned by RegisterExternalFunction() / RegisterExternalEvent() / + * RegisterTagFunction() + * + * @blueprint + */ + void Unregister(const FInkHandle& handle) { handle.Cancel(); } + + UFUNCTION(BlueprintCallable, Category = "Ink") + /** Unregister all external functions and events bound to this thread. + * Useful when reusing a thread via StartExisting() to ensure no stale bindings remain. + * + * @blueprint + */ + void ClearExternalFunctions(); + UFUNCTION(BlueprintCallable, Category = "Ink") /** get knots assoziated with current knot. * knot tags are tags listed behind a knot `== knot name ==` before the first line of content @@ -193,6 +216,11 @@ class INKCPP_API UInkThread : public UObject */ const UTagList* GetGlobalTags(); + /** Get choices from the last OnChoice event. + * @return array of choices available at the current choice point, empty if not at a choice + */ + const TArray& GetCurrentChoices() const { return mCurrentChoices; } + protected: /** @private */ @@ -201,6 +229,11 @@ class INKCPP_API UInkThread : public UObject /** @private */ virtual void OnLineWritten_Implementation(const FString& line, UTagList* tags) {} + /** @private */ + virtual void OnKnotEntered_Implementation(const UTagList* global_tags, const UTagList* knot_tags) + { + } + /** @private */ virtual void OnTag_Implementation(const FString& line) {} @@ -221,31 +254,58 @@ class INKCPP_API UInkThread : public UObject void ExecuteTagMethod(const TArray& Params); + /** Register a UInkList that was created from runner-owned memory during this thread's + * execution. The thread will call Invalidate() on all registered lists before the next + * choose() call so that stale pointers are detected early. + * @private + */ + void RegisterLiveList(UInkList* list); + friend FInkVar::FInkVar(UInkList&), FInkVar::FInkVar(ink::runtime::value); + + /** Invalidate all UInkList objects registered since the last choose(). + * Called internally before mpRunner->choose(). + * @private + */ + void InvalidateLiveLists(); + private: ink::runtime::runner mpRunner; UPROPERTY() - UTagList* mpTags; + UTagList* mpTags; UPROPERTY() - UTagList* mkTags = nullptr; + UTagList* mkTags = nullptr; UPROPERTY() - UTagList* mgTags = nullptr; + UTagList* mgTags = nullptr; UPROPERTY() - TArray mCurrentChoices; /// @TODO: make accessible? + TArray mCurrentChoices; + + /** Lists wrapping runner-owned memory, registered during current execute cycle. */ + TArray> mLiveLists; + + /** Token storage for tag function registrations. Maps function name → parallel arrays of + * tokens and delegates. Each token is shared with the corresponding FInkHandle. + * Setting a token to false causes the entry to be skipped and lazily compacted. */ + TMap>> mTagFunctionTokens; + + /** Multicast delegates for tag functions, parallel to mTagFunctionTokens. */ + TMap> mTagFunctionDelegates; - TMap mTagFunctions; + /** Token storage for external function registrations, keyed by name hash. + * A matching FInkHandle invalidates the token to suppress the binding. */ + TMap> mExternalFunctionTokens; FString mStartPath; bool mbHasRun; - int mnChoiceToChoose; - int mnYieldCounter; - bool mbInChoice; - bool mbKill; - bool mbInitialized; + int mnChoiceToChoose; + int mnYieldCounter; + bool mbInChoice; + bool mbKill; + bool mbInitialized; ink::hash_t mCurrentKnot; UPROPERTY() diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkVar.h b/unreal/inkcpp/Source/inkcpp/Public/InkVar.h index 4449e9a8..c1893ed6 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkVar.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkVar.h @@ -3,16 +3,13 @@ * This file is part of inkCPP which is released under MIT license. * See file LICENSE.txt or go to * https://github.com/JBenda/inkcpp for full license details. - * - * Based on Copyright (c) 2020 David Colson - * UnrealInk @ https://github.com/DavidColson/UnrealInk */ #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" +#include "inkcpp.h" #include "UObject/TextProperty.h" -#include "Containers/Union.h" #include "Containers/StringConv.h" #include "InkList.h" @@ -23,18 +20,20 @@ * @ingroup unreal */ UENUM(BlueprintType) -enum class EInkVarType : uint8 -{ +enum class EInkVarType : uint8 { Float, ///< contains a value of type float - Int, ///< contains a value of type int or uint - UInt, ///< @todo currently not supported + Int, ///< contains a value of type int + UInt, ///< @todo currently not supported in Blueprints (converts to signed) Bool, ///< contains a boolean String, ///< contains a string value List, ///< contains a @ref UInkList None ///< contains no value }; -namespace ink::runtime { struct value; } +namespace ink::runtime +{ +struct value; +} // namespace ink::runtime /** A wrapper for passing around ink vars to and from ink itself. * To access the values see @ref UInkVarLibrary @@ -42,36 +41,59 @@ namespace ink::runtime { struct value; } * @ingroup unreal */ USTRUCT(BlueprintType) -struct INKCPP_API FInkVar -{ + +struct INKCPP_API FInkVar { GENERATED_BODY() - FInkVar() {} + FInkVar() + : VarType(EInkVarType::None) + , IntVal(0) + { + } /** @private */ - FInkVar(float val) { value.SetSubtype(val); } + FInkVar(float val) + : VarType(EInkVarType::Float) + , FloatVal(val) + { + } /** @private */ - FInkVar(int val) { value.SetSubtype(val); } + FInkVar(int val) + : VarType(EInkVarType::Int) + , IntVal(val) + { + } /** @private */ FInkVar(unsigned val) + : VarType(EInkVarType::UInt) + , UIntVal(val) { - UE_LOG(InkCpp, Warning, TEXT("Converting unsigned to signed int, since missing blueprint support for unsigned type")); - value.SetSubtype(val); - } // TODO: change if we find a way to support unsigned values in blueprints + UE_LOG( + InkCpp, Warning, + TEXT("Converting unsigned to signed int, since missing blueprint support for unsigned type") + ); + } /** @private */ - FInkVar(bool val) { value.SetSubtype(val); } + FInkVar(bool val) + : VarType(EInkVarType::Bool) + , BoolVal(val) + { + } /** @private */ - FInkVar(FString val) { - value.SetSubtype(val); + FInkVar(FString val) + : VarType(EInkVarType::String) + , IntVal(0) + , StringVal(MoveTemp(val)) + { BufferDecodedString(); } /** @private */ - FInkVar(UInkList& List) { value.SetSubtype(&List); } + FInkVar(UInkList& List); /** @private */ FInkVar(ink::runtime::value val); @@ -79,40 +101,56 @@ struct INKCPP_API FInkVar /** @private */ ink::runtime::value to_value() const; + /** Get the type contained in the value + * @retval EInkVarType::None if no value is contained (void) + * @private + */ + EInkVarType type() const { return VarType; } + + // ----------------------------------------------------------------------- + // Data — laid out as explicit named fields so the GC can see ListVal and + // StringVal, and there is no union obscuring UObject* from the reflector. + // ----------------------------------------------------------------------- - // allow changing via Editor, but not in control flow, it is just a wrapper type to create a new - // one UPROPERTY(EditAnywhere, Category = "Ink") /** @private */ - TUnion value; + UPROPERTY() + EInkVarType VarType = EInkVarType::None; - /** Keeps utf8 version of string alive to write it in runtime. + // Scalar storage — only one is valid at a time based on VarType. + // Not UPROPERTY because they hold primitive types the GC does not need. + /** @private */ float FloatVal = 0.f; + /** @private */ int32 IntVal = 0; + /** @private */ uint32 UIntVal = 0; + /** @private */ bool BoolVal = false; + + /** String storage — separate field so it is always properly constructed/destroyed. * @private */ - TArray utf8{}; + FString StringVal; - /** Get the type contained in the value - * @retval EInkVarType::None if no value is contained (void) + /** List storage — UPROPERTY so the GC can trace the UObject*. + * Only valid when VarType == EInkVarType::List. * @private */ - EInkVarType type() const { - uint8 id = value.GetCurrentSubtypeIndex(); - if(id >= static_cast(EInkVarType::None)) - { return EInkVarType::None; } - else - { return static_cast(id); } - } + UPROPERTY() + TObjectPtr ListVal = nullptr; + + /** Keeps utf8 version of the string alive to write it back to the ink runtime. + * @private + */ + TArray Utf8{}; + private: - void BufferDecodedString() { - FTCHARToUTF8 Convert(*value.GetSubtype()); - utf8.SetNum(Convert.Length() + 1); - UTF8CHAR* dst = utf8.GetData(); - [this,&dst](const UTF8CHAR* src){ - int i = 0; - while(*src) { - *dst++ = *src++; - } - *dst = static_cast(0); - }(reinterpret_cast(Convert.Get())); + void BufferDecodedString() + { + FTCHARToUTF8 Convert(*StringVal); + Utf8.SetNum(Convert.Length() + 1); + UTF8CHAR* dst = Utf8.GetData(); + const UTF8CHAR* src = reinterpret_cast(Convert.Get()); + while (*src) { + *dst++ = *src++; + } + *dst = static_cast(0); } }; @@ -120,6 +158,7 @@ struct INKCPP_API FInkVar * @ingroup unreal */ UCLASS() + class INKCPP_API UInkVarLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() @@ -133,96 +172,141 @@ class INKCPP_API UInkVarLibrary : public UBlueprintFunctionLibrary */ static EInkVarType InkVarType(const FInkVar& InkVar); - UFUNCTION(BlueprintPure, meta = (DisplayName = "String (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "String (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Access String value * * @blueprint */ static FString Conv_InkVarString(const FInkVar& InkVar); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Int (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") - /** Access Int/Uint value - * @todo support unsigned int + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Int (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) + /** Access Int value * * @blueprint */ static int Conv_InkVarInt(const FInkVar& InkVar); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Float (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Float (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Access Float Value * * @blueprint */ static float Conv_InkVarFloat(const FInkVar& InkVar); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Name (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Name (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Access String value as FName * * @blueprint */ static FName Conv_InkVarName(const FInkVar& InkVar); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Text (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Text (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Access String value as FText * * @blueprint */ static FText Conv_InkVarText(const FInkVar& InkVar); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Bool (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Bool (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Access bool value - * * @blueprint */ static bool Conv_InkVarBool(const FInkVar& InkVar); - UFUNCTION(BlueprintPure, meta = (DisplayName = "InkList (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "InkList (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Access @ref UInkList "List" value * * @blueprint */ static const UInkList* Conv_InkVarInkList(const FInkVar& InkVar); - - // UFUNCTION(BlueprintPure, meta = (DisplayName = "UInt (Ink Var)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") - // static unsigned Conv_InkVarUInt(const FInkVar& InkVar); - - UFUNCTION(BlueprintPure, meta = (DisplayName = "Ink Var (String)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Ink Var (String)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Convert string to @ref FInkVar * * @blueprint */ static FInkVar Conv_StringInkVar(const FString& String); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Ink Var (Int)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Ink Var (Int)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Convert int to @ref FInkVar - * @todo support unsigned values * * @blueprint */ static FInkVar Conv_IntInkVar(int Number); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Ink Var (Float)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Ink Var (Float)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Convert float to @ref FInkVar * * @blueprint */ static FInkVar Conv_FloatInkVar(float Number); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Ink Var (Text)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Ink Var (Text)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Convert FText to @ref FInkVar of type @ref EInkVarType::String "String" * * @blueprint */ static FInkVar Conv_TextInkVar(const FText& Text); - - UFUNCTION(BlueprintPure, meta = (DisplayName = "Ink Var (Name)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Ink Var (Name)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Convert FName to @ref FInkVar of type @ref EInkVarType::String "String" * * @blueprint */ static FInkVar Conv_NameInkVar(const FName& Name); - UFUNCTION(BlueprintPure, meta = (DisplayName = "Ink Var (Bool)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") + UFUNCTION( + BlueprintPure, + meta = (DisplayName = "Ink Var (Bool)", CompactNodeTitle = "->", BlueprintAutocast), + Category = "Ink" + ) /** Convert bool to @ref FInkVar * * @blueprint @@ -239,7 +323,4 @@ class INKCPP_API UInkVarLibrary : public UBlueprintFunctionLibrary * @blueprint */ static FInkVar Conv_ListInkVar(UInkList* List); - - // UFUNCTION(BlueprintPure, meta = (DisplayName = "Ink Var (UInt)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Ink") - // static FInkVar Conv_UIntInkVar(unsigned Number); }; diff --git a/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h b/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h index 76cf0751..91701a51 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h +++ b/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h @@ -17,10 +17,12 @@ * ```sh * /UNREAL_ENGINE/Build/BatchFiles/RunUAT.bat BuildPlugin * -plugin=/AN_TEMP_DIRECTORY/inkcpp/inkcpp.uplugin -package=/YOUR_UNREAL_PROJECT/Plugins/inkcpp - * -TargetPlatforms=Win64` - * ```
And either way activating the plugin. + * -TargetPlatforms=Win64 + * ``` + *
And either way activating the plugin. * - * The C++ API will be available soon([Issue](https://github.com/JBenda/inkcpp/issues/60)). + * The C++ API is available — include the plugin headers directly from your game module + * after adding `"inkcpp"` to your module's `PublicDependencyModuleNames` in Build.cs. * * + @ref ue_setup "General setup" * + @ref ue_components "UE5 Blueprints" @@ -184,39 +186,57 @@ * width="80%"/> * @endhtmlonly * + * @htmlonly * * A InkThread Yield Resume example Blueprint + * @endhtmlonly + * + * @htmlonly + * + * A InkThread Yield Resume example Blueprint + * @endhtmlonly * * @subsubsection ue_example_demo_DemoThread DemoThread * + * @htmlonly * * Example of the ussage of TagList::GetValue inside processing a new context line. + * @endhtmlonly * + * @htmlonly * * Example for choice handling. + * @endhtmlonly * * @subsection ue_example_minimal Minimal * + * @htmlonly * * Minmal InkRuntime Blueprint + * @endhtmlonly * + * @htmlonly * * Minimal InkThread Blueprint + * @endhtmlonly * */ diff --git a/unreal/inkcpp/Source/inkcpp/inkcpp.Build.cs b/unreal/inkcpp/Source/inkcpp/inkcpp.Build.cs index c9639147..0daecba8 100644 --- a/unreal/inkcpp/Source/inkcpp/inkcpp.Build.cs +++ b/unreal/inkcpp/Source/inkcpp/inkcpp.Build.cs @@ -14,21 +14,21 @@ public inkcpp(ReadOnlyTargetRules Target) : base(Target) PublicIncludePaths.AddRange( new string[] { - Path.Combine(ModuleDirectory, "../shared/Public") + Path.Combine(ModuleDirectory, "../shared/Public"), + Path.Combine(ModuleDirectory, "Public/ink"), } ); - - + + PrivateIncludePaths.AddRange( new string[] { Path.Combine(ModuleDirectory, "../shared/Private"), Path.Combine(ModuleDirectory, "Private/ink"), - Path.Combine(ModuleDirectory, "Public/ink"), Path.Combine(ModuleDirectory, "../ThirdParty/Private"), } ); - - + + PublicDependencyModuleNames.AddRange( new string[] { @@ -36,8 +36,8 @@ public inkcpp(ReadOnlyTargetRules Target) : base(Target) // ... add other public dependencies that you statically link with here ... } ); - - + + PrivateDependencyModuleNames.AddRange( new string[] { @@ -45,11 +45,11 @@ public inkcpp(ReadOnlyTargetRules Target) : base(Target) "Engine", "Slate", "SlateCore", - // ... add private dependencies that you statically link with here ... + // ... add private dependencies that you statically link with here ... } ); - - + + DynamicallyLoadedModuleNames.AddRange( new string[] {