diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..184c4eb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + ".", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/bink/choices.py b/bink/choices.py index cf1d13b..c2a0fe7 100644 --- a/bink/choices.py +++ b/bink/choices.py @@ -40,7 +40,7 @@ def __getitem__(self, idx: int) -> str: if not isinstance(idx, int): raise TypeError - if idx < 0 or idx > self._len: + if idx < 0 or idx >= self._len: raise IndexError return self.get_text(idx) diff --git a/bink/native/arm64/libbink.dylib b/bink/native/arm64/libbink.dylib index 1fba51e..5a46ec4 100755 Binary files a/bink/native/arm64/libbink.dylib and b/bink/native/arm64/libbink.dylib differ diff --git a/bink/native/arm64/libbink.so b/bink/native/arm64/libbink.so index b0e72c9..35218a4 100755 Binary files a/bink/native/arm64/libbink.so and b/bink/native/arm64/libbink.so differ diff --git a/bink/native/x86_64/bink.dll b/bink/native/x86_64/bink.dll index 72c56fe..4255b09 100644 Binary files a/bink/native/x86_64/bink.dll and b/bink/native/x86_64/bink.dll differ diff --git a/bink/native/x86_64/libbink.dylib b/bink/native/x86_64/libbink.dylib index d71f499..e67afc3 100755 Binary files a/bink/native/x86_64/libbink.dylib and b/bink/native/x86_64/libbink.dylib differ diff --git a/bink/native/x86_64/libbink.so b/bink/native/x86_64/libbink.so index 7947d62..bad4b7c 100755 Binary files a/bink/native/x86_64/libbink.so and b/bink/native/x86_64/libbink.so differ diff --git a/bink/story.py b/bink/story.py index 434e374..d0803d4 100644 --- a/bink/story.py +++ b/bink/story.py @@ -127,9 +127,8 @@ def get_current_tags(self) -> Tags: def choose_path_string(self, path: str): err_msg = ctypes.c_char_p() - story = ctypes.c_void_p() - ret = LIB.bink_story_new( - ctypes.byref(story), + ret = LIB.bink_story_choose_path_string( + self._story, path.encode('utf-8'), ctypes.byref(err_msg)) if ret != BINK_OK: @@ -137,6 +136,40 @@ def choose_path_string(self, path: str): LIB.bink_cstring_free(err_msg) raise RuntimeError(err) + def save_state(self) -> str: + """Saves the current state of the story and returns it as a string. + The returned state can be loaded later using load_state().""" + err_msg = ctypes.c_char_p() + save_string = ctypes.c_char_p() + ret = LIB.bink_story_save_state( + self._story, + ctypes.byref(save_string), + ctypes.byref(err_msg)) + + if ret != BINK_OK: + err = err_msg.value.decode('utf-8') + LIB.bink_cstring_free(err_msg) + raise RuntimeError(err) + + result = save_string.value.decode('utf-8') + LIB.bink_cstring_free(save_string) + + return result + + def load_state(self, save_state: str): + """Loads a previously saved state into the story. + This allows resuming the story from a saved point.""" + err_msg = ctypes.c_char_p() + ret = LIB.bink_story_load_state( + self._story, + save_state.encode('utf-8'), + ctypes.byref(err_msg)) + + if ret != BINK_OK: + err = err_msg.value.decode('utf-8') + LIB.bink_cstring_free(err_msg) + raise RuntimeError(err) + def __del__(self): LIB.bink_story_free(self._story) diff --git a/bink/tags.py b/bink/tags.py index aadccbf..5712979 100644 --- a/bink/tags.py +++ b/bink/tags.py @@ -21,7 +21,7 @@ def __next__(self): class Tags: """Contains a list of tags.""" - def __init__(self, tags, c_len): + def __init__(self, tags, c_len: int): self._tags = tags self._len = c_len @@ -41,7 +41,7 @@ def __getitem__(self, idx: int) -> str: if not isinstance(idx, int): raise TypeError - if idx < 0 or idx > self._len: + if idx < 0 or idx >= self._len: raise IndexError return self.get(idx) @@ -49,7 +49,7 @@ def __getitem__(self, idx: int) -> str: def get(self, idx) -> str: """Returns the tag text.""" tag = ctypes.c_char_p() - ret = LIB.bink_choices_get_text(self._tags, idx, ctypes.byref(tag)) + ret = LIB.bink_tags_get(self._tags, idx, ctypes.byref(tag)) if ret != BINK_OK: raise RuntimeError("Error getting tag, index out of bounds?") @@ -60,4 +60,4 @@ def get(self, idx) -> str: return result def __del__(self): - LIB.bink_choices_free(self._tags) + LIB.bink_tags_free(self._tags) diff --git a/inkfiles/runtime/load-save.ink b/inkfiles/runtime/load-save.ink new file mode 100644 index 0000000..ccd6921 --- /dev/null +++ b/inkfiles/runtime/load-save.ink @@ -0,0 +1,28 @@ +-> back_in_london + +=== back_in_london === + +We arrived into London at 9.45pm exactly. + +* "There is not a moment to lose!"[] I declared. + -> hurry_outside + +* "Monsieur, let us savour this moment!"[] I declared. + My master clouted me firmly around the head and dragged me out of the door. + -> dragged_outside + +* [We hurried home] -> hurry_outside + + +=== hurry_outside === +We hurried home to Savile Row -> as_fast_as_we_could + + +=== dragged_outside === +He insisted that we hurried home to Savile Row +-> as_fast_as_we_could + + +=== as_fast_as_we_could === +<> as fast as we could. +-> END diff --git a/inkfiles/runtime/load-save.ink.json b/inkfiles/runtime/load-save.ink.json new file mode 100644 index 0000000..a5b5767 --- /dev/null +++ b/inkfiles/runtime/load-save.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[{"->":"back_in_london"},["done",{"#n":"g-0"}],null],"done",{"back_in_london":[["^We arrived into London at 9.45pm exactly.","\n",["ev",{"^->":"back_in_london.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"There is not a moment to lose!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"back_in_london.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Monsieur, let us savour this moment!\"",{"->":"$r","var":true},null]}],"ev","str","^We hurried home","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"back_in_london.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ I declared.","\n",{"->":"hurry_outside"},{"#f":5}],"c-1":["ev",{"^->":"back_in_london.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I declared.","\n","^My master clouted me firmly around the head and dragged me out of the door.","\n",{"->":"dragged_outside"},{"#f":5}],"c-2":["^ ",{"->":"hurry_outside"},"\n",{"#f":5}]}],null],"hurry_outside":["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",null],"dragged_outside":["^He insisted that we hurried home to Savile Row","\n",{"->":"as_fast_as_we_could"},null],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/inkfiles/tags.ink b/inkfiles/tags.ink new file mode 100644 index 0000000..834e45f --- /dev/null +++ b/inkfiles/tags.ink @@ -0,0 +1,16 @@ +VAR x = 2 +# author: Joe +# title: My Great Story +This is the content + +== knot == +# knot tag +Knot content +# end of knot tag +-> END + += stitch +# stitch tag +Stitch content +# this tag is below some content so isn't included in the static tags for the stitch +-> END \ No newline at end of file diff --git a/inkfiles/tags.ink.json b/inkfiles/tags.ink.json new file mode 100644 index 0000000..5b5fabf --- /dev/null +++ b/inkfiles/tags.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["#","^author: Joe","/#","#","^title: My Great Story","/#","^This is the content","\n",["done",{"#n":"g-0"}],null],"done",{"knot":["#","^knot tag","/#","^Knot content","\n","#","^end of knot tag","/#","end",{"stitch":["#","^stitch tag","/#","^Stitch content","\n","#","^this tag is below some content so isn't included in the static tags for the stitch","/#","end",null]}],"global decl":["ev",2,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3a6bd66 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,19 @@ +[metadata] +name = bink +version = 0.5.0 +author = Rafael Garcia +description = Runtime for Ink, a scripting language for writing interactive narrative +long_description = file: README.rst +license = Apache 2.0 +classifiers = + Programming Language :: Python :: 3 + +[options] +zip_safe = False +packages = find: + +[options.packages.find] +exclude = + tests* + dist* + build* diff --git a/setup.py b/setup.py index 61f5fd9..ba04d92 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,20 @@ """Package definition.""" -import setuptools, sys -from setuptools import find_packages, setup + + +import sys +from setuptools import setup, Distribution from wheel.bdist_wheel import bdist_wheel -from os import path -from io import open -class BinaryDistribution (setuptools.Distribution): + +class BinaryDistribution (Distribution): def has_ext_modules(self): return True + class BdistWheel(bdist_wheel): def get_tag(self): - return ('py3', 'none') + bdist_wheel.get_tag(self)[2:] + return ('py3', 'none') + super().get_tag()[2:] + def get_package_data(): plat_name_idx = None @@ -32,35 +35,23 @@ def get_package_data(): lib = 'bink.dll' else: raise RuntimeError('Unsupported platform: ' + plat_name) - + arch = "x86_64/" if "arm64" in plat_name or "aarch64" in plat_name: arch = "arm64/" lib = arch + lib - + return ['native/' + lib] # if it is not present, return ['native/*'] return ['native/*'] -description = open( - path.join(path.abspath(path.dirname(__file__)), 'README.rst'), - encoding='utf-8').read() setup( - name='bink', - packages=find_packages(exclude=['tests']), - version='0.3.1', - description='Runtime for Ink, a scripting language for writing interactive narrative', - long_description_content_type='text/x-rst', - long_description=description, - author='Rafael Garcia', - license='Apache 2.0', package_data={'bink': get_package_data()}, distclass = BinaryDistribution, cmdclass = { 'bdist_wheel': BdistWheel, }, - zip_safe=False # native libraries are included in the package ) diff --git a/tests/test_story.py b/tests/test_story.py index f0f8abe..861b331 100644 --- a/tests/test_story.py +++ b/tests/test_story.py @@ -8,6 +8,47 @@ def test_oneline(self): self.assertTrue(story.can_continue()) self.assertEqual(story.cont(), "Line.\n") + def test_load_save(self): + """Test save_state and load_state functionality.""" + # Create a story and get initial text + story = story_from_file("inkfiles/runtime/load-save.ink.json") + + # Continue to get all initial text + lines = story.continue_maximally() + + # Check first line + self.assertEqual(lines, "We arrived into London at 9.45pm exactly.\n") + + # Save the game state + save_string = story.save_state() + + print(f"Save state: {save_string}") + + # Free the current story and create a new one + del story + + story = story_from_file("inkfiles/runtime/load-save.ink.json") + + # Load the saved state + story.load_state(save_string) + + # Choose first choice + story.choose_choice_index(0) + + # Continue to get the next text + lines = story.continue_maximally() + + # The text should contain both lines we expect + self.assertIn("\"There is not a moment to lose!\" I declared.", lines) + self.assertIn("We hurried home to Savile Row as fast as we could.", lines) + + # Check that we are at the end + self.assertFalse(story.can_continue()) + + # Check that there are no more choices + choices = story.get_current_choices() + self.assertEqual(len(choices), 0) + def test_the_intercept(self): story = story_from_file("inkfiles/TheIntercept.ink.json") self.assertTrue(story.can_continue()) @@ -25,7 +66,7 @@ def test_the_intercept(self): if choices: for i, text in enumerate(choices): print(f"{i + 1}. {text}") - + # Always choose the first option story.choose_choice_index(0) else: diff --git a/tests/test_tags.py b/tests/test_tags.py new file mode 100644 index 0000000..1d107b2 --- /dev/null +++ b/tests/test_tags.py @@ -0,0 +1,27 @@ +from bink.story import story_from_file +import unittest + +class TagsInkTestCase(unittest.TestCase): + def test_tags(self): + story = story_from_file("inkfiles/tags.ink.json") + self.assertTrue(story.can_continue()) + + self.assertEqual("This is the content\n", story.cont()) + + current_tags = story.get_current_tags() + self.assertEqual(2, len(current_tags)) + self.assertEqual("author: Joe", current_tags[0]) + self.assertEqual("title: My Great Story", current_tags[1]) + + story.choose_path_string("knot") + self.assertEqual("Knot content\n", story.cont()) + current_tags = story.get_current_tags() + self.assertEqual(1, len(current_tags)) + self.assertEqual("knot tag", current_tags[0]) + + self.assertEqual("", story.cont()) + current_tags = story.get_current_tags() + self.assertEqual("end of knot tag", current_tags[0]) + +if __name__ == '__main__': + unittest.main() diff --git a/update-natives.sh b/update-natives.sh index b1de43c..0707bf9 100755 --- a/update-natives.sh +++ b/update-natives.sh @@ -12,6 +12,8 @@ fi mkdir -p build cd build +echo "Downloading Version $VERSION..." + curl -kOL https://github.com/bladecoder/blade-ink-ffi/releases/download/${VERSION}/libbink-${VERSION}-aarch64-apple-darwin.tar.gz curl -kOL https://github.com/bladecoder/blade-ink-ffi/releases/download/${VERSION}/libbink-${VERSION}-aarch64-unknown-linux-gnu.tar.gz curl -kOL https://github.com/bladecoder/blade-ink-ffi/releases/download/${VERSION}/libbink-${VERSION}-x86_64-apple-darwin.tar.gz