Skip to content

Commit fc6ce10

Browse files
authored
Release bink 0.7.0
Release bink 0.7.0
2 parents ea34df6 + 2a47bc1 commit fc6ce10

19 files changed

Lines changed: 434 additions & 336 deletions

bink/__init__.py

Lines changed: 9 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,11 @@
1-
"""Load the Bink C library."""
2-
import os
3-
import platform
4-
import ctypes.util
1+
"""Python bindings for the Blade Ink C API."""
52

6-
def _load_library():
7-
# First, look for a bundled bink shared object on the lib folder.
8-
system = platform.system()
9-
library_name = None
10-
if system == 'Windows':
11-
library_name = 'bink.dll'
12-
elif system == 'Darwin':
13-
library_name = 'libbink.dylib'
14-
else:
15-
library_name = 'libbink.so'
3+
from ._ffi import (BINK_ERROR_ERROR, BINK_ERROR_WARNING, BINK_FAIL,
4+
BINK_FAIL_INVALID_ARGUMENT, BINK_FAIL_INVALID_UTF8,
5+
BINK_FAIL_NUL_BYTE, BINK_FAIL_NULL_POINTER,
6+
BINK_FAIL_PANIC, BINK_OK, BINK_VALUE_BOOL,
7+
BINK_VALUE_DIVERT_TARGET, BINK_VALUE_FLOAT, BINK_VALUE_INT,
8+
BINK_VALUE_LIST, BINK_VALUE_STRING,
9+
BINK_VALUE_VARIABLE_POINTER, LIB)
1610

17-
arch = "x86_64/"
18-
19-
if platform.machine() == "arm64":
20-
arch = "arm64/"
21-
22-
_filename = os.path.join(os.path.dirname(__file__), 'native/' + arch + library_name)
23-
24-
# If no bundled shared object is found, look for a system-wide installed one.
25-
if not os.path.exists(_filename):
26-
# on windows all ctypes does when checking for the library
27-
# is to append .dll to the end and look for an exact match
28-
# within any entry in PATH.
29-
_filename = ctypes.util.find_library('bink')
30-
31-
if _filename is None:
32-
if platform.system() == 'Windows':
33-
# Check current working directory for dll as ctypes fails to do so
34-
_filename = os.path.join(os.path.realpath('.'), "bink.dll")
35-
else:
36-
_filename = library_name
37-
38-
try:
39-
#print("lib filename: ", _filename)
40-
lib = ctypes.CDLL(_filename)
41-
except (OSError, TypeError) as exc:
42-
lib = None
43-
raise RuntimeError('bink library not found') from exc
44-
return lib
45-
46-
LIB = _load_library()
47-
48-
LIB.bink_story_new.argtypes = [
49-
ctypes.POINTER(
50-
ctypes.c_void_p), ctypes.c_char_p, ctypes.POINTER(
51-
ctypes.c_char_p)]
52-
LIB.bink_story_new.restype = ctypes.c_int
53-
54-
LIB.bink_story_can_continue.argtypes = [
55-
ctypes.c_void_p, ctypes.POINTER(ctypes.c_bool)]
56-
LIB.bink_story_can_continue.restype = ctypes.c_int
57-
58-
BINK_OK = 0
59-
BINK_FAIL = 1
60-
BINK_FAIL_NULL_POINTER = 2
11+
__all__ = [name for name in globals() if name.startswith("BINK_")] + ["LIB"]

bink/_ffi.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Low-level ctypes declarations shared by the public wrappers."""
2+
import ctypes
3+
import ctypes.util
4+
import os
5+
import platform
6+
7+
BINK_OK, BINK_FAIL, BINK_FAIL_NULL_POINTER, BINK_FAIL_INVALID_UTF8, BINK_FAIL_NUL_BYTE, BINK_FAIL_PANIC, BINK_FAIL_INVALID_ARGUMENT = range(7)
8+
BINK_ERROR_WARNING, BINK_ERROR_ERROR = range(2)
9+
BINK_VALUE_BOOL, BINK_VALUE_INT, BINK_VALUE_FLOAT, BINK_VALUE_STRING, BINK_VALUE_LIST, BINK_VALUE_DIVERT_TARGET, BINK_VALUE_VARIABLE_POINTER = range(7)
10+
11+
12+
def _load_library():
13+
names = {"Windows": "bink.dll", "Darwin": "libbink.dylib"}
14+
library_name = names.get(platform.system(), "libbink.so")
15+
arch = "arm64" if platform.machine() in ("arm64", "aarch64") else "x86_64"
16+
filename = os.path.join(os.path.dirname(__file__), "native", arch, library_name)
17+
if not os.path.exists(filename):
18+
filename = ctypes.util.find_library("bink") or library_name
19+
try:
20+
return ctypes.CDLL(filename)
21+
except (OSError, TypeError) as exc:
22+
raise RuntimeError("bink library not found") from exc
23+
24+
25+
LIB = _load_library()
26+
P = ctypes.c_void_p
27+
CP = ctypes.POINTER(ctypes.c_char_p)
28+
SZ = ctypes.c_size_t
29+
30+
31+
def _declare(name, args, restype=ctypes.c_int):
32+
fn = getattr(LIB, name)
33+
fn.argtypes, fn.restype = args, restype
34+
35+
36+
for _name, _args in {
37+
"bink_story_new": [ctypes.POINTER(P), ctypes.c_char_p, CP],
38+
"bink_story_can_continue": [P, ctypes.POINTER(ctypes.c_bool), CP],
39+
"bink_story_cont": [P, CP, CP], "bink_story_continue_maximally": [P, CP, CP],
40+
"bink_story_continue_async": [P, ctypes.c_float, ctypes.POINTER(ctypes.c_bool), CP],
41+
"bink_story_get_current_text": [P, CP, CP],
42+
"bink_story_get_current_choices": [P, ctypes.POINTER(P), ctypes.POINTER(SZ), CP],
43+
"bink_story_choose_choice_index": [P, SZ, CP],
44+
"bink_story_get_current_tags": [P, ctypes.POINTER(P), ctypes.POINTER(SZ), CP],
45+
"bink_story_get_global_tags": [P, ctypes.POINTER(P), ctypes.POINTER(SZ), CP],
46+
"bink_story_get_tags_for_content_at_path": [P, ctypes.c_char_p, ctypes.POINTER(P), ctypes.POINTER(SZ), CP],
47+
"bink_story_choose_path_string": [P, ctypes.c_char_p, CP],
48+
"bink_story_choose_path_string_with_args": [P, ctypes.c_char_p, ctypes.c_bool, P, CP],
49+
"bink_story_evaluate_function": [P, ctypes.c_char_p, P, ctypes.POINTER(P), CP, CP],
50+
"bink_story_load_state": [P, ctypes.c_char_p, CP], "bink_story_save_state": [P, CP, CP],
51+
"bink_story_reset_state": [P, CP],
52+
"bink_story_get_visit_count_at_path_string": [P, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int32), CP],
53+
"bink_story_get_current_path": [P, CP, CP], "bink_story_build_string_of_hierarchy": [P, CP, CP],
54+
"bink_choices_get_text": [P, SZ, CP, CP], "bink_choices_get_tags": [P, SZ, ctypes.POINTER(P), ctypes.POINTER(SZ), CP],
55+
"bink_tags_get": [P, SZ, CP, CP],
56+
"bink_value_new_bool": [ctypes.c_bool, ctypes.POINTER(P), CP], "bink_value_new_int": [ctypes.c_int32, ctypes.POINTER(P), CP],
57+
"bink_value_new_float": [ctypes.c_float, ctypes.POINTER(P), CP], "bink_value_new_string": [ctypes.c_char_p, ctypes.POINTER(P), CP],
58+
"bink_value_get_bool": [P, ctypes.POINTER(ctypes.c_bool), CP], "bink_value_get_int": [P, ctypes.POINTER(ctypes.c_int32), CP],
59+
"bink_value_get_float": [P, ctypes.POINTER(ctypes.c_float), CP], "bink_value_get_string": [P, CP, CP],
60+
"bink_value_get_kind": [P, ctypes.POINTER(ctypes.c_int), CP],
61+
"bink_value_array_new": [ctypes.POINTER(P), CP], "bink_value_array_push": [P, P, CP],
62+
"bink_list_new": [ctypes.POINTER(P), CP], "bink_story_list_new_from_origin": [P, ctypes.c_char_p, ctypes.POINTER(P), CP],
63+
"bink_story_list_new_from_item": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], "bink_list_add_item": [P, ctypes.c_char_p, ctypes.c_int32, CP],
64+
"bink_list_get_count": [P, ctypes.POINTER(SZ), CP], "bink_list_get_item": [P, SZ, CP, ctypes.POINTER(ctypes.c_int32), CP],
65+
"bink_list_get_origin_count": [P, ctypes.POINTER(SZ), CP], "bink_list_get_origin": [P, SZ, CP, CP],
66+
"bink_value_new_list": [P, ctypes.POINTER(P), CP], "bink_value_get_list": [P, ctypes.POINTER(P), CP],
67+
"bink_var_get": [P, ctypes.c_char_p, ctypes.POINTER(P), CP], "bink_var_set": [P, ctypes.c_char_p, P, CP],
68+
"bink_story_switch_flow": [P, ctypes.c_char_p, CP], "bink_story_remove_flow": [P, ctypes.c_char_p, CP],
69+
"bink_story_switch_to_default_flow": [P, CP], "bink_story_set_allow_external_function_fallbacks": [P, ctypes.c_bool, CP],
70+
"bink_unbind_external_function": [P, ctypes.c_char_p, CP],
71+
"bink_fun_args_count": [P, ctypes.POINTER(SZ), CP], "bink_fun_args_get": [P, SZ, ctypes.POINTER(P), CP],
72+
}.items():
73+
_declare(_name, _args)
74+
75+
for _name, _args in {"bink_story_free": [P], "bink_choices_free": [P], "bink_tags_free": [P], "bink_value_free": [P], "bink_value_array_free": [P], "bink_list_free": [P], "bink_cstring_free": [ctypes.c_char_p]}.items():
76+
_declare(_name, _args, None)
77+
78+
79+
def check(result, error):
80+
if result == BINK_OK:
81+
return
82+
message = error.value.decode("utf-8") if error.value else "Blade Ink FFI error"
83+
if error.value:
84+
LIB.bink_cstring_free(error)
85+
raise RuntimeError(message)
86+
87+
88+
def call(name, *args):
89+
error = ctypes.c_char_p()
90+
check(getattr(LIB, name)(*args, ctypes.byref(error)), error)
91+
92+
93+
def take_string(value):
94+
try:
95+
return value.value.decode("utf-8") if value.value else ""
96+
finally:
97+
if value.value:
98+
LIB.bink_cstring_free(value)

bink/choices.py

Lines changed: 29 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,36 @@
1-
# pylint: disable=E1101, R0903
2-
"""Handle Ink Choices."""
1+
"""Choice collections returned by a story."""
32
import ctypes
4-
from bink import LIB, BINK_OK
53

6-
7-
class ChoicesIterator:
8-
"""Iterator for choices."""
9-
def __init__(self, choices):
10-
self._choices = choices
11-
self._index = 0
12-
13-
def __next__(self):
14-
if self._index >= len(self._choices):
15-
raise StopIteration
16-
17-
self._index += 1
18-
return self._choices[self._index - 1]
4+
from ._ffi import LIB, call, take_string
5+
from .tags import Tags
196

207

218
class Choices:
22-
"""List of story choices."""
23-
def __init__(self, choices, c_len: int):
24-
self._choices = choices
25-
self._len = c_len
26-
27-
def __len__(self) -> int:
28-
"""Returns the number of choices."""
29-
return self._len
30-
31-
def __bool__(self) -> bool:
32-
return self._len != 0
33-
34-
def __iter__(self):
35-
return ChoicesIterator(self)
36-
37-
def __getitem__(self, idx: int) -> str:
38-
"""Returns the choice text"""
39-
40-
if not isinstance(idx, int):
41-
raise TypeError
42-
43-
if idx < 0 or idx >= self._len:
44-
raise IndexError
45-
46-
return self.get_text(idx)
47-
48-
def get_text(self, idx) -> str:
49-
"""Returns the choice text."""
50-
text = ctypes.c_char_p()
51-
ret = LIB.bink_choices_get_text(self._choices, idx, ctypes.byref(text))
52-
53-
if ret != BINK_OK:
54-
raise RuntimeError(
55-
"Error getting choice text, index out of bounds?")
56-
57-
result = text.value.decode('utf-8')
58-
LIB.bink_cstring_free(text)
59-
60-
return result
9+
"""An owned sequence of choice text, with access to each choice's tags."""
10+
def __init__(self, pointer, length):
11+
self._choices, self._len = pointer, length
12+
13+
def __len__(self): return self._len
14+
def __bool__(self): return bool(self._len)
15+
def __iter__(self): return (self[index] for index in range(self._len))
16+
17+
def _index(self, index):
18+
if not isinstance(index, int): raise TypeError("choice index must be an integer")
19+
if index < 0: index += self._len
20+
if not 0 <= index < self._len: raise IndexError("choice index out of range")
21+
return index
22+
23+
def __getitem__(self, index):
24+
value = ctypes.c_char_p()
25+
call("bink_choices_get_text", self._choices, self._index(index), ctypes.byref(value))
26+
return take_string(value)
27+
28+
def get_tags(self, index):
29+
pointer, length = ctypes.c_void_p(), ctypes.c_size_t()
30+
call("bink_choices_get_tags", self._choices, self._index(index), ctypes.byref(pointer), ctypes.byref(length))
31+
return Tags(pointer, length.value)
6132

6233
def __del__(self):
63-
LIB.bink_choices_free(self._choices)
34+
if getattr(self, "_choices", None):
35+
LIB.bink_choices_free(self._choices)
36+
self._choices = None

bink/native/arm64/libbink.dylib

145 KB
Binary file not shown.

bink/native/arm64/libbink.so

146 KB
Binary file not shown.

bink/native/x86_64/bink.dll

132 KB
Binary file not shown.

bink/native/x86_64/libbink.dylib

141 KB
Binary file not shown.

bink/native/x86_64/libbink.so

149 KB
Binary file not shown.

0 commit comments

Comments
 (0)