diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9954c941..0be77c3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -116,13 +116,23 @@ jobs: if: ${{ matrix.unreal }} working-directory: ${{github.workspace}}/build shell: bash - run: cmake --install . --config $BUILD_TYPE --prefix comp_unreal --component unreal - - name: Upload UE + run: | + cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.5" + cmake --install . --config $BUILD_TYPE --prefix comp_unreal_5_5 --component unreal + cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.4" + cmake --install . --config $BUILD_TYPE --prefix comp_unreal_5_4 --component unreal + - name: Upload UE 5.5 + if: ${{ matrix.unreal }} + uses: actions/upload-artifact@v4 + with: + name: unreal_5_5 + path: build/comp_unreal_5_5/ + - name: Upload UE 5.4 if: ${{ matrix.unreal }} uses: actions/upload-artifact@v4 with: - name: unreal - path: build/comp_unreal/ + name: unreal_5_4 + path: build/comp_unreal_5_4/ # Make sure Inkproof has everything it needs to run our executable - name: Setup Ink Proof @@ -181,6 +191,13 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.x" + - name: install pyton libs + run: >- + python3 -m + pip install + pybind11-stubgen + pdoc + --user - name: Install Doxygen run: | sudo apt-get install graphviz -y @@ -198,11 +215,11 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}}/build - run: cmake $GITHUB_WORKSPACE -DINKCPP_PY=ON -DINKCPP_DOC_BlueprintUE=ON + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=Release -DINKCPP_PY=ON -DINKCPP_DOC_BlueprintUE=ON - name: Build working-directory: ${{github.workspace}}/build shell: bash - run: cmake --build . --target doc + run: cmake --build . --target doc --config Release - name: Upload uses: actions/upload-artifact@v4 with: @@ -226,6 +243,8 @@ jobs: python3 -m pip install build + pybind11-stubgen + pdoc pytest --user - name: Build python release @@ -247,7 +266,7 @@ jobs: - name: Test python release run: | python3 -m pip install dist/*.whl --user - python3 -m pytest + python3 -m pytest inkcpp_python/tests - name: Remove wheel run: | rm dist/*.whl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 625d5738..e73ea82c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,12 @@ jobs: run: | mkdir artifacts ID=$(gh run list -b master --limit 1 --json databaseId | jq '.[0].databaseId') - gh run download $ID -D artifacts -n linux-cl -n linux-lib -n linux-clib -n unreal -n macos-cl -n macos-lib -n macos-clib -n macos-arm-cl -n macos-arm-lib -n macos-arm-clib -n win64-cl -n win64-lib -n win64-clib -n python-package-distribution + gh run download $ID -D artifacts -n linux-cl -n linux-lib -n linux-clib -n unreal_5_5 -n unreal_5_4 -n macos-cl -n macos-lib -n macos-clib -n macos-arm-cl -n macos-arm-lib -n macos-arm-clib -n win64-cl -n win64-lib -n win64-clib -n python-package-distribution mv artifacts/python-package-distribution dist - name: Zip working-directory: ${{github.workspace}}/artifacts run: | - for f in linux-cl linux-lib linux-clib unreal macos-cl macos-lib macos-clib macos-arm-cl macos-arm-lib macos-arm-clib win64-cl win64-lib win64-clib; do zip -r ../$f.zip $f; done + for f in linux-cl linux-lib linux-clib unreal_5_5 unreal_5_4 macos-cl macos-lib macos-clib macos-arm-cl macos-arm-lib macos-arm-clib win64-cl win64-lib win64-clib; do zip -r ../$f.zip $f; done - name: List run: tree - name: Publish to PyPI @@ -45,5 +45,5 @@ jobs: --repo="$GITHUB_REPOSITORY" \ --title="${GITHUB_REPOSITORY#*/} ${tag#v}" \ --generate-notes \ - "$tag" "linux-cl.zip" "linux-lib.zip" "linux-clib.zip" "unreal.zip" "macos-cl.zip" "macos-lib.zip" "macos-clib.zip" "win64-cl.zip" "macos-arm-cl.zip" "macos-arm-lib.zip" "macos-arm-clib.zip" "win64-lib.zip" "win64-clib.zip" + "$tag" "linux-cl.zip" "linux-lib.zip" "linux-clib.zip" "unreal_5_5.zip" "unreal_5_4.zip" "macos-cl.zip" "macos-lib.zip" "macos-clib.zip" "win64-cl.zip" "macos-arm-cl.zip" "macos-arm-lib.zip" "macos-arm-clib.zip" "win64-lib.zip" "win64-clib.zip" diff --git a/.gitmodules b/.gitmodules index 27948af3..f1e39021 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,8 +2,8 @@ path = proofing/ink-proof url = https://github.com/chromy/ink-proof.git shallow = true -[submodule "inkcpp_py/pybind11"] - path = inkcpp_py/pybind11 +[submodule "inkcpp_python/pybind11"] + path = inkcpp_python/pybind11 url = https://github.com/pybind/pybind11.git branch = stable shallow = true diff --git a/CMakeLists.txt b/CMakeLists.txt index c984ddee..5612491c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ set(CMAKE_TLS_VERIFY true) enable_testing() # Project setup -project(inkcpp VERSION 0.1.6) +project(inkcpp VERSION 0.1.7) SET(CMAKE_CXX_STANDARD 20) SET(CMAKE_CXX_STANDARD_REQUIRED ON) SET(CMAKE_INSTALL_LIBRARY_DIR lib) @@ -57,7 +57,7 @@ endif() if (INKCPP_PY) add_compile_options(-fPIC) - add_subdirectory(inkcpp_py) + add_subdirectory(inkcpp_python) endif(INKCPP_PY) add_subdirectory(shared) add_subdirectory(inkcpp) @@ -154,10 +154,11 @@ if (DOXYGEN_FOUND) COMPONENTS Interpreter ) add_custom_target(inkcpp_py_doc - python -m pydoc -w inkcpp_py - COMMAND mv "./inkcpp_py.html" ${PY_HTML} + python -m pybind11_stubgen -o . inkcpp_py + COMMAND python -m pdoc -d google -o . inkcpp_py.pyi + COMMAND ${CMAKE_COMMAND} -E copy "./inkcpp_py.html" ${PY_HTML} DEPENDS inkcpp_py - WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/inkcpp_py" + WORKING_DIRECTORY $ COMMENT "Generates simple python documentation") add_dependencies(doc inkcpp_py_doc) else() diff --git a/README.md b/README.md index 4a4238c9..68ca27d6 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ Ink Proofing Test Results: https://jbenda.github.io/inkcpp/proof Doxygen Documentation: https://jbenda.github.io/inkcpp/html +CLib Documentation: https://jbenda.github.io/inkcpp/html/group__clib.html + +UE Documentation: https://jbenda.github.io/inkcpp/html/group__unreal.html + +Python Documentation: https://jbenda.github.io/inkcpp/html/inkcpp_py.html + ## Project Goals * Fast, simple, clean syntax * No heap allocations during execution (unless in emergencies) @@ -17,6 +23,12 @@ Doxygen Documentation: https://jbenda.github.io/inkcpp/html ## Current Status +supported languages ([latest release](https://github.com/JBenda/inkcpp/releases/latest)): + + C++ [doc](https://jbenda.github.io/inkcpp/html/index.html)[example](https://jbenda.github.io/inkcpp/html/index.html) + + C [doc](https://jbenda.github.io/inkcpp/html/group__clib.html)[example](https://jbenda.github.io/inkcpp/html/group__clib.html#example_c) + + UE Blueprints [doc](https://jbenda.github.io/inkcpp/html/group__unreal.html)[distribution](https://www.unrealengine.com/marketplace/product/inkcpp)[example](https://jbenda.github.io/inkcpp/html/group__unreal.html#ue_example) + + Python [doc](https://jbenda.github.io/inkcpp/html/inkcpp_py.html)[distribution](https://pypi.org/project/inkcpp-py/)[example](https://jbenda.github.io/inkcpp/html/index.html#py) + Run `inkcpp_cl.exe -p myfile.json` to execute a compiled Ink JSON file in play mode. It can also operate on `.ink` files but `inklecate.exe` must be in the same folder or in the PATH. `inklecate` can be downloaded from the [official release page](https://github.com/inkle/ink/releases) and will be downloaded from CMake at configure time (located at `build/unreal/inkcpp/Resources/inklecate`). Or do it automatically with the `INKCPP_INKLECATE=OS` CMake flag. (It will be downloaded to `/inklecate//` and will be installed with `cmake --install . --component cl`) @@ -39,9 +51,18 @@ Place the content of this file at your plugin folder of your UE project and at t A example project can be found [here](https://jbenda.github.io/inkcpp/unreal/InkCPP_DEMO.zip). And here the [Documentation](https://jbenda.github.io/inkcpp/html/group__unreal.html). -Code for the Unreal plugin is located in the `unreal` directory. In order to install it, run `cmake --install . --component unreal --prefix Path/To/Unreal/Plugins/` which will add an `inkcpp` folder there with the `.uplugin`, the code for the UClasses, and all the inkcpp source files required. `config.h` will automatically detect it is being built in an Unreal plugin environment and disable STL and enable Unreal extensions (FString support, Unreal asserts, CityHash, etc.). - -If you compile the UE Plugin by your self feel free to visit the [wiki page](https://github.com/JBenda/inkcpp/wiki/Unreal) for a more debug oriented build process. +Code for the Unreal plugin is located in the `unreal` directory. In order to install it, run +```sh +mkdir build +cd build +mkdir plugin +mkdir plugin-build +cmake -DINKCPP_UNREAL_TARGET_VERSION="5.5" . +cmake --install . --component unreal --prefix .\plugin # create source files for plugin +\PATH\TO\UNREAL_ENGINE\Build\BatchFiles\RunUAT.bat BuildPlugin -plugin=GIT_REPO\build\plugin\inkcpp\inkcpp.uplugin -package=GIT_REPO\build\plugin-build\inkcpp -TargetPlatforms=Win64 # compile plugin +move plugin-build\inkcpp UE_ENGINE\Engine\Plugins\inkcpp +``` +Adapt `TargetPlatforms` as nessesarry. You might also want to install the Plugin directly into a project or the in UE5.5 introduced external plugin directory. Just adapt the pathets accordendly. ## Use standalone @@ -165,7 +186,7 @@ python -m pip install dist/*.whl --user # if inklecate is not in the same directory / inside Path set INKLECATE enviroment variable export INKLECATE= # unix set INKLECTATE= # windows -python -m pytest +python -m pytest inkcpp_python/tests ``` Right now this only executes the internal unit tests which test the functions of particular classes. Soon it'll run more complex tests on .ink files using ink-proof. @@ -174,7 +195,7 @@ Right now this only executes the internal unit tests which test the functions of ## Python Bindings The easy way to start is installing it with pip: `pip install inkcpp_py`. -An example can be found at [example.py](./inkcpp_py/example.py). +An example can be found at [example.py](./inkcpp_python/example.py). To build it from source use: ```sh @@ -182,7 +203,7 @@ git clone --recurse-submodules https://github.com/JBenda/inkcpp.git pip install . ``` -The python bindnigs are defined in `inkcpp_py` subfolder. +The python bindnigs are defined in `inkcpp_python` subfolder. ## Dependencies The compiler depends on Nlohmann's JSON library and the C++ STL. diff --git a/inkcpp/include/story.h b/inkcpp/include/story.h index f1a20def..1cc1696f 100644 --- a/inkcpp/include/story.h +++ b/inkcpp/include/story.h @@ -157,7 +157,7 @@ class story * + INKCPP_TEST: (ON|OFF) weather or not execute tests * requires `inklecate` to be in the PATH or `INKCPP_INKLECATE=OS` or `=ALL` * + INKCPP_INKLECATE: (NONE|OS|ALL) download the current supported inklecate version from the - * official [release page](https://github.com/inkle/ink/releases/latest)
They are stored at + * official [release page](https://github.com/inkle/ink/releases/latest)
They are stored at * `/inklecate//` and will be automatcilly used for the tests * + NONE: disable this function * + OS: only the version supported for the OS @@ -212,8 +212,9 @@ class story * `pip install .` * * Here can you find an - * [example](https://raw.githubusercontent.com/JBenda/inkcpp/master/inkcpp_py/example.py) inclusive - * [story](https://raw.githubusercontent.com/JBenda/inkcpp/master/inkcpp_py/unreal_example.ink). + * [example](https://raw.githubusercontent.com/JBenda/inkcpp/master/inkcpp_python/example.py) + * inclusive + * [story](https://raw.githubusercontent.com/JBenda/inkcpp/master/inkcpp_python/unreal_example.ink). * * [Python module documentation](./inkcpp_py.html) */ diff --git a/inkcpp_py/pybind11 b/inkcpp_py/pybind11 deleted file mode 160000 index 8b03ffa7..00000000 --- a/inkcpp_py/pybind11 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8b03ffa7c06cd9c8a38297b1c8923695d1ff1b07 diff --git a/inkcpp_py/CMakeLists.txt b/inkcpp_python/CMakeLists.txt similarity index 100% rename from inkcpp_py/CMakeLists.txt rename to inkcpp_python/CMakeLists.txt diff --git a/inkcpp_py/example.py b/inkcpp_python/example.py similarity index 100% rename from inkcpp_py/example.py rename to inkcpp_python/example.py diff --git a/inkcpp_python/pybind11 b/inkcpp_python/pybind11 new file mode 160000 index 00000000..0c69e1eb --- /dev/null +++ b/inkcpp_python/pybind11 @@ -0,0 +1 @@ +Subproject commit 0c69e1eb2177fa8f8580632c7b1f97fdb606ce8f diff --git a/inkcpp_py/src/module.cpp b/inkcpp_python/src/module.cpp similarity index 50% rename from inkcpp_py/src/module.cpp rename to inkcpp_python/src/module.cpp index 4d5fbaa3..624fce54 100644 --- a/inkcpp_py/src/module.cpp +++ b/inkcpp_python/src/module.cpp @@ -28,7 +28,7 @@ using globals_ptr = ink::runtime::globals; using story = ink::runtime::story; using choice = ink::runtime::choice; using value = ink::runtime::value; -using list = ink::runtime::list_interface; +using ilist = ink::runtime::list_interface; using snapshot = ink::runtime::snapshot; PYBIND11_DECLARE_HOLDER_TYPE(T, ink::runtime::story_ptr); @@ -46,7 +46,7 @@ struct StringValueWrap : public value { std::string str; }; -std::string list_to_str(const list& list) +std::string list_to_str(const ilist& list) { std::stringstream out; out << "["; @@ -65,100 +65,75 @@ std::string list_to_str(const list& list) PYBIND11_MODULE(inkcpp_py, m) { + py::options options; + options.disable_enum_members_docstring(); m.doc() = "Python bindings for InkCPP https://github.com/JBenda/inkcpp"; // optional module docstring - py::class_(m, "Story") - .def("from_file", &story::from_file, "Creates a new story from a .bin file") - .def("new_globals", &story::new_globals, "creates new globals store for the current story") - .def("new_runner", [](story& self) { return self.new_runner(); }) - .def("new_runner", &story::new_runner, "creates a new runner for the current story") - .def("new_globals_from_snapshot", &story::new_globals_from_snapshot) - .def("new_runner_from_snapshot", &story::new_runner_from_snapshot) - .def( - "new_runner_from_snapshot", [](story& self, const snapshot& snap, globals_ptr store - ) { return self.new_runner_from_snapshot(snap, store); } - ) - .def("new_runner_from_snapshot", [](story& self, const snapshot& snap) { - return self.new_runner_from_snapshot(snap); - }); - py::class_(m, "Globals") - .def( - "create_snapshot", &globals::create_snapshot, - "Creates a snapshot from the current state for later usage" - ) - .def( - "observe", - [](globals& self, const char* name, - std::function)> f) { - self.observe(name, f); - }, - "Get a call with the new and old value (this could be None) each time the variable " - "changes" - ) - .def( - "__getattr__", - [](const globals& self, const std::string& key) { - auto res = self.get(key.c_str()); - if (! res.has_value()) { - throw py::key_error(std::string("No global variable with name '") + key + "' found"); - } - return res.value(); - }, - "Access global varible if exists, if not throws an key_error" - ) - .def("__setattr__", [](globals& self, const std::string& key, const value& val) { - if (! self.set(key.c_str(), val)) { - throw py::key_error( - std::string("No global variable with name '") + key - + "' found or you are trying to override a non string variable with a string" - ); - } - }); - - py::class_> py_list( - m, "List", + py::class_> py_list( + m, "IList", "Allows reading and editing inkcpp lists. !Only valid until next choose ore getline a runner " "referncing the corresponding global" ); - py_list.def("add", &list::add, "Add flag to list") - .def("remove", &list::remove, "Remove flag from list") - .def("contains", &list::contains, "Check if list contains the given flag") + py::class_( + py_list, "Flag", "A list flag containing the name of the flag and the corresponding list" + ) + .def_readonly("name", &ilist::iterator::Flag::flag_name, "The flag") + .def_readonly( + "list_name", &ilist::iterator::Flag::list_name, "Name of the corresponding list" + ); + py_list.def("add", &ilist::add, "Add flag to list.", py::arg("flag").none(false)) + .def("remove", &ilist::remove, "Remove flag from list", py::arg("flag").none(false)) + .def( + "contains", &ilist::contains, "Check if list contains the given flag", + py::arg("flag").none(false) + ) .def( "flags_from", - [](const list& self, const char* list_name) { + [](const ilist& self, const char* list_name) { return py::make_iterator(self.begin(list_name), self.end()); }, - "Rerutrns all flags contained in list from a list", py::keep_alive<0, 1>() + R"(Rerutrns all flags contained in this list from a list of name list_name. + +Use iter(List) to iterate over all flags.)", + py::keep_alive<0, 1>(), py::arg("list_name").none(false) ) .def( - "__iter__", [](const list& self) { return py::make_iterator(self.begin(), self.end()); }, + "__iter__", [](const ilist& self) { return py::make_iterator(self.begin(), self.end()); }, py::keep_alive<0, 1>() ) .def("__str__", &list_to_str); - py::class_( - py_list, "Flag", "A list flag containing the name of the flag and the corresponding list" - ) - .def_readonly("name", &list::iterator::Flag::flag_name, "The flag") - .def_readonly( - "list_name", &list::iterator::Flag::list_name, "Name of the corresponding list" - ); - py::class_ py_value(m, "Value", "A Value of a Ink Variable"); + py::enum_(py_value, "Type") + .value("Bool", value::Type::Bool) + .value("Uint32", value::Type::Uint32) + .value("Int32", value::Type::Int32) + .value("String", value::Type::String) + .value("Float", value::Type::Float) + .value("List", value::Type::List) + .export_values(); py_value.def_readonly("type", &value::type, "Type contained in value"); - py_value.def(py::init<>()); - py_value.def(py::init()); - py_value.def("__init__", [](value& self, uint32_t v, value::Type type) { - if (type != value::Type::Uint32) { - throw py::key_error("only use this signture if you want to explicit pass a uint"); - } - self = value(v); - }); - py_value.def(py::init()); - py_value.def(py::init()); - py_value.def(py::init()); - py_value.def(py::init([](const std::string& str) { return new StringValueWrap(str); })); + // py_value.def(py::init<>()); + py_value.def(py::init(), py::arg("value").none(false)); + py_value.def( + "__init__", + [](value& self, uint32_t v, value::Type type) { + if (type != value::Type::Uint32) { + throw py::key_error("only use this signture if you want to explicit pass a uint"); + } + self = value(v); + }, + "Used to explicit set a Uint32 value. Type must be inkcpp_py.Value.Type.Uint32!", + py::arg("value").none(false), py::arg("type").none(false) + ); + py_value.def(py::init(), py::arg("value").none(false)); + py_value.def(py::init(), py::arg("value").none(false)); + py_value.def(py::init(), py::arg("value").none(false)); + py_value.def( + py::init([](const std::string& str) { return new StringValueWrap(str); }), + py::arg("value").none(false) + ); py_value.def( "as_list", [](const value& self) { @@ -167,8 +142,55 @@ PYBIND11_MODULE(inkcpp_py, m) } return self.get(); }, + R"(If it contains a inkcpp_py.Value.Type.List, return it. Else throws AttributeError.)", py::return_value_policy::reference ); + py_value.def( + "as_int", + [](const value& self) { + if (self.type == value::Type::Int32) { + return static_cast(self.get()); + } else if (self.type == value::Type::Uint32) { + return static_cast(self.get()); + } + throw py::attribute_error("Try to access a int of a non int32 nor uint32 value."); + }, + "If value contains a inkcpp_py.Value.Type.Int32 or inkcpp_py.Value.Type.Uint32 return the " + "int value. " + "Else throws AttributeError" + ); + py_value.def( + "as_bool", + [](const value& self) { + if (self.type != value::Type::Bool) { + throw py::attribute_error("Try to access a bool of a non bool value."); + } + return self.get(); + }, + "If value contains a inkcpp_py.Value.Type.Bool, return it. Else throws a AttributeError." + ); + py_value.def( + "as_str", + [](const value& self) { + if (self.type != value::Type::String) { + throw py::attribute_error("Try to access a string of a non string value."); + } + return self.get(); + }, + R"(If value contains a inkcpp_py.Value.Type.String, return it. Else throws an AttributeError. + +If you want convert it to a string use: `str(value)`.)" + ); + py_value.def( + "as_float", + [](const value& self) { + if (self.type != value::Type::Float) { + throw py::attribute_error("Try to access a float of a non float value."); + } + return self.get(); + }, + "If value contains a inkcpp_py.Value.Type.Float, return it. Else throws an AttributeError." + ); py_value.def("__str__", [](const value& self) { switch (self.type) { case value::Type::Bool: return std::string(self.get() ? "true" : "false"); @@ -183,34 +205,125 @@ PYBIND11_MODULE(inkcpp_py, m) throw py::attribute_error("value is in an invalid state"); }); - py::enum_(py_value, "Type") - .value("Bool", value::Type::Bool) - .value("Uint32", value::Type::Uint32) - .value("Int32", value::Type::Int32) - .value("String", value::Type::String) - .value("Float", value::Type::Float) - .value("List", value::Type::List) - .export_values(); - py::class_(m, "Runner") + py::class_( + m, "Snapshot", "Globals and all assoziatet runner stored for later restoration" + ) + .def("num_runners", &snapshot::num_runners, "Number of different runners stored in snapshot") .def( - "create_snapshot", &runner::create_snapshot, + "write_to_file", &snapshot::write_to_file, "Store snapshot in file.", + py::arg("filename").none(false) + ) + .def_static( + "from_file", &snapshot::from_file, "Load snapshot from file", + py::arg("filename").none(false) + ); + m.def( + "compile_json", + [](const char* input_file_name, const char* output_file_name) { + ink::compiler::compilation_results results; + ink::compiler::run(input_file_name, output_file_name, &results); + if (! results.errors.empty()) { + std::string str; + for (auto& error : results.errors) { + str += error; + str += '\n'; + } + throw py::value_error(str); + } + }, + py::arg("input_file_name").none(false), py::arg("output_file_name").none(false), + "Converts a story.json file to a story.bin file used by inkcpp" + ); + py::class_(m, "Choice") + .def("text", &choice::text, "Get choice printable content") + .def("has_tags", &choice::has_tags, "if choices is tagged?") + .def("num_tags", &choice::num_tags, "Number of tags assigned to choice") + .def( + "get_tag", &choice::get_tag, "Get tag at index", py::arg("index").none(false), + py::return_value_policy::reference_internal + ) + .def( + "tags", + [](const choice& self) { + std::vector tags(self.num_tags()); + for (size_t i = 0; i < self.num_tags(); ++i) { + tags[i] = self.get_tag(i); + } + return tags; + }, + "Get all current assinged tags" + ); + + py::class_( + m, "Globals", "Global variable store. Use `globals[var_name]` to read/write them." + ) + .def( + "create_snapshot", &globals::create_snapshot, "Creates a snapshot from the current state for later usage" ) + .def( + "observe", + [](globals& self, const char* name, + std::function)> f) { + self.observe(name, f); + }, + R"( +Start observing a variable. + +Args: + name: name of the (global) variable to observe + callback: called when varibale changes with `(new_value, old_value)`. + `old_value` will be `None` when variable is initelized + )", + py::arg("name").none(false), py::arg("callback").none(false) + ) + .def( + "__getattr__", + [](const globals& self, const std::string& key) { + auto res = self.get(key.c_str()); + if (! res.has_value()) { + throw py::key_error(std::string("No global variable with name '") + key + "' found"); + } + return res.value(); + }, + "Access global varible if exists, if not throws an KeyError" + ) + .def("__setattr__", [](globals& self, const std::string& key, const value& val) { + if (! self.set(key.c_str(), val)) { + throw py::key_error( + std::string("No global variable with name '") + key + + "' found or you are trying to override a non string variable with a string" + ); + } + }); + py::class_(m, "Runner", "Runtime logic for a story.") + .def( + "create_snapshot", &runner::create_snapshot, + R"(Creates a snapshot from the current state for later usage. + +This snapshot will also contain the global state. +To reload: + +>>> snapshot = old_runner.create_snapshot() +>>> story = Story.from_file("story.bin") +>>> runner = story.new_runner_from_snapshot(snapshot) + )" + ) .def("can_continue", &runner::can_continue, "check if there is content left in story") .def( "getline", static_cast(&runner::getline), - "Get content of one output line" + "Get content of the next output line" ) .def( "getall", static_cast(&runner::getall), - "execute getline and append until can_continue is false" + "execute getline and append until inkcp_py.Runner.can_continue is false" ) - .def("has_tags", &runner::has_tags, "Where there tags since last getline?") + .def("has_tags", &runner::has_tags, "Where there tags since last choice") .def("num_tags", &runner::num_tags, "Number of tags currently stored") .def( - "get_tag", &runner::get_tag, "Get Tag currently stored at position i", - py::return_value_policy::reference_internal + "get_tag", &runner::get_tag, "Get Tag currently stored at index", + py::arg("index").none(false), py::return_value_policy::reference_internal ) .def( "tags", @@ -228,8 +341,11 @@ PYBIND11_MODULE(inkcpp_py, m) "Check if there is at least one open choice at the moment." ) .def( - "get_choice", &runner::get_choice, "Get current choice at index", - py::return_value_policy::reference_internal + "get_choice", &runner::get_choice, R"( +Get current choice at index. + +iter(inkcpp_py.Runner) returns a iterator over all current choices.)", + py::arg("index").none(false), py::return_value_policy::reference_internal ) .def("num_choices", &runner::num_choices, "Number of current open choices") .def( @@ -237,7 +353,10 @@ PYBIND11_MODULE(inkcpp_py, m) [](const runner& self) { return py::make_iterator(self.begin(), self.end()); }, py::keep_alive<0, 1>() ) - .def("choose", &runner::choose, "Select a choice to continue") + .def( + "choose", &runner::choose, "Select choice at index and continue.", + py::arg("index").none(false) + ) .def( "bind_void", [](runner& self, const char* function_name, std::function)> f, @@ -251,8 +370,8 @@ PYBIND11_MODULE(inkcpp_py, m) lookaheadSafe ); }, - py::arg("function_name"), py::arg("function"), py::arg_v("lookaheadSafe", false), - "Bind function with void result" + py::arg("function_name").none(false), py::arg("function").none(false), + py::arg_v("lookaheadSafe", false).none(false), "Bind function with void result" ) .def( "bind", @@ -263,55 +382,65 @@ PYBIND11_MODULE(inkcpp_py, m) return f(args); }); }, - py::arg("function_name"), py::arg("function"), py::arg_v("lookaheadSafe", false), - "Bind a function with return value" + py::arg("function_name").none(false), py::arg("function").none(false), + py::arg_v("lookaheadSafe", false).none(false), "Bind a function with return value" ) .def( "move_to", [](runner& self, const char* path) -> bool { return self.move_to(ink::hash_string(path)); }, - "Moves execution pointer to start of container desrcipet by the path" + "Moves execution pointer to start of container desrcipet by the path", + py::arg("path").none(false) ); - py::class_(m, "Choice") - .def("text", &choice::text, "Get choice printable content") - .def("has_tags", &choice::has_tags, "if choices is tagged?") - .def("num_tags", &choice::num_tags, "Number of tags assigned to choice") + py::class_(m, "Story") + .def_static( + "from_file", &story::from_file, R"( +Creates a new story from a .bin file. + +Returns: + inkcpp_py.Story: a new story +)", + py::arg("filename").none(false) + ) + .def("new_globals", &story::new_globals, R"( +Creates new globals store for the current story. +)") .def( - "get_tag", &choice::get_tag, "Get tag at index", - py::return_value_policy::reference_internal + "new_runner", &story::new_runner, R"( +Ceates a new runner. + +Args: + globals: pass a global to use, else use a new inetrnal one. + +Returns: + inkcpp_py.Runner: a newly created runner, initelized at start of story + )", + py::arg("globals").none(true) = py::none() ) .def( - "tags", - [](const choice& self) { - std::vector tags(self.num_tags()); - for (size_t i = 0; i < self.num_tags(); ++i) { - tags[i] = self.get_tag(i); - } - return tags; - }, - "Get all current assinged tags" + "new_globals_from_snapshot", &story::new_globals_from_snapshot, R"( +Loads a global store from a snapshot. + +Returns: + inkcpp_py.Globals: a new global store with the same state then stored in the snapshot. + )", + py::arg("snapshot").none(false) + ) + .def( + "new_runner_from_snapshot", &story::new_runner_from_snapshot, R"( +Reconstructs a runner from a snapshot. + +Args: + snapshot: snapshot to load runner from. + globals: store used by this runner, else load the store from the file and use it. + (created with inkcpp_py.Story.new_runner_from_snapshot) + runner_id: if multiple runners are stored in the snapshot, id of runner to reconstruct. (ids start at 0 and are dense) + +Returns: + inkcpp_py.Runner: at same state as before +)", + py::arg("snapshot").none(false), py::arg("globals").none(true) = py::none(), + py::arg("runner_id").none(false) = 0 ); - py::class_( - m, "Snapshot", "Globals and all assoziatet runner stored for later restoration" - ) - .def("num_runners", &snapshot::num_runners, "Number of different runners stored in snapshot") - .def("write_to_file", &snapshot::write_to_file, "Store snapshot in file") - .def("from_file", &snapshot::from_file, "Load snapshot from file"); - m.def( - "compile_json", - [](const char* input_file_name, const char* output_filen_ame) { - ink::compiler::compilation_results results; - ink::compiler::run(input_file_name, output_filen_ame, &results); - if (! results.errors.empty()) { - std::string str; - for (auto& error : results.errors) { - str += error; - str += '\n'; - } - throw py::value_error(str); - } - }, - "Converts a story.json file to a story.bin file used by inkcpp" - ); } diff --git a/inkcpp_py/tests/conftest.py b/inkcpp_python/tests/conftest.py similarity index 97% rename from inkcpp_py/tests/conftest.py rename to inkcpp_python/tests/conftest.py index 14fce796..9548fdaa 100644 --- a/inkcpp_py/tests/conftest.py +++ b/inkcpp_python/tests/conftest.py @@ -32,6 +32,7 @@ def story_path(tmpdir_factory): @pytest.fixture(scope='session', autouse=True) def assets(story_path, inklecate_cmd): + print(ink.__dict__, file=sys.stderr) res = {} for (name, files) in story_path.items(): if not os.path.exists(files[0]): diff --git a/inkcpp_py/tests/test_ExternalFunctions.py b/inkcpp_python/tests/test_ExternalFunctions.py similarity index 100% rename from inkcpp_py/tests/test_ExternalFunctions.py rename to inkcpp_python/tests/test_ExternalFunctions.py diff --git a/inkcpp_py/tests/test_Globals.py b/inkcpp_python/tests/test_Globals.py similarity index 100% rename from inkcpp_py/tests/test_Globals.py rename to inkcpp_python/tests/test_Globals.py diff --git a/inkcpp_py/tests/test_Lists.py b/inkcpp_python/tests/test_Lists.py similarity index 100% rename from inkcpp_py/tests/test_Lists.py rename to inkcpp_python/tests/test_Lists.py diff --git a/inkcpp_py/tests/test_Observer.py b/inkcpp_python/tests/test_Observer.py similarity index 100% rename from inkcpp_py/tests/test_Observer.py rename to inkcpp_python/tests/test_Observer.py diff --git a/inkcpp_py/tests/test_Snapshot.py b/inkcpp_python/tests/test_Snapshot.py similarity index 100% rename from inkcpp_py/tests/test_Snapshot.py rename to inkcpp_python/tests/test_Snapshot.py diff --git a/inkcpp_py/unreal_example.ink b/inkcpp_python/unreal_example.ink similarity index 100% rename from inkcpp_py/unreal_example.ink rename to inkcpp_python/unreal_example.ink diff --git a/inkcpp_test/CMakeLists.txt b/inkcpp_test/CMakeLists.txt index 4568eee7..cb7ca896 100644 --- a/inkcpp_test/CMakeLists.txt +++ b/inkcpp_test/CMakeLists.txt @@ -52,7 +52,7 @@ if((inkcpp_inklecate_upper STREQUAL "ALL") OR (inkcpp_inklecate_upper STREQUAL " set(INKLECATE_CMD "${inklecate_linux_SOURCE_DIR}/inklecate") else() message(FATAL_ERROR "inklecate download is not provided, please check if the download failed. - You may set INCPP_INKLECATE=NONE and provide inkcleate via your PATH or the INKLECATE enviroment variable. + You may set INKCPP_INKLECATE=NONE and provide inkcleate via your PATH or the INKLECATE enviroment variable. You can also disable tests altogether by setting INKCPP_TEST=OFF") endif(inklecate_linux_POPULATED) elseif(APPLE) @@ -60,7 +60,7 @@ if((inkcpp_inklecate_upper STREQUAL "ALL") OR (inkcpp_inklecate_upper STREQUAL " set(INKLECATE_CMD "${inklecate_mac_SOURCE_DIR}/inklecate") else() message(FATAL_ERROR "inklecate download is not provided, please check if the download failed. - You may set INCPP_INKLECATE=NONE and provide inkcleate via your PATH or the INKLECATE enviroment variable. + You may set INKCPP_INKLECATE=NONE and provide inkcleate via your PATH or the INKLECATE enviroment variable. You can also disable tests altogether by setting INKCPP_TEST=OFF") endif(inklecate_mac_POPULATED) elseif(MSYS OR MINGW OR WIN32 OR CYGWIN) @@ -68,7 +68,7 @@ if((inkcpp_inklecate_upper STREQUAL "ALL") OR (inkcpp_inklecate_upper STREQUAL " set(INKLECATE_CMD "${inklecate_windows_SOURCE_DIR}/inklecate") else() message(FATAL_ERROR "inklecate download is not provided, please check if the download failed. - You may set INCPP_INKLECATE=NONE and provide inkcleate via your PATH or the INKLECATE enviroment variable. + You may set INKCPP_INKLECATE=NONE and provide inkcleate via your PATH or the INKLECATE enviroment variable. You can also disable tests altogether by setting INKCPP_TEST=OFF") endif(inklecate_windows_POPULATED) else() diff --git a/setup.py b/setup.py index fc8002e0..8b26d584 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ from setuptools.command.build_ext import build_ext -# echo "[\n$(find shared/** inkcpp/** inkcpp_compiler/** inkcpp_py/** -not -path '*/.*' | tr '\n' ' ' | sed -e 's/[^ ]\+/"\0"/g' -e 's/[[:blank:]]*$//' -e 's/ /,\n/g')\n]" +# echo "[\n$(find shared/** inkcpp/** inkcpp_compiler/** inkcpp_python/** -not -path '*/.*' | tr '\n' ' ' | sed -e 's/[^ ]\+/"\0"/g' -e 's/[[:blank:]]*$//' -e 's/ /,\n/g')\n]" # + "CMakeLists.txt" # Convert distutils Windows platform specifiers to CMake -A arguments @@ -37,7 +37,7 @@ def src_files(dir): def glob_src_files(): files = [] - dirs = ['./inkcpp', './inkcpp_compiler', './inkcpp_py', './shared'] + dirs = ['./inkcpp', './inkcpp_compiler', './inkcpp_python', './shared'] for dir in dirs: files += src_files(dir) return files @@ -46,7 +46,7 @@ def glob_src_files(): class CMakeExtension(Extension): def __init__(self, name: str, sourcedir: str = "") -> None: src_files = glob_src_files() - # src_files = json.load(open('inkcpp_py/sources.json')) + # src_files = json.load(open('inkcpp_python/sources.json')) src_files += ["CMakeLists.txt", "Config.cmake.in"] super().__init__(name, sources=src_files) self.sourcedir = os.fspath(Path(sourcedir).resolve()) @@ -153,7 +153,7 @@ def build_extension(self, ext: CMakeExtension) -> None: setup( name="inkcpp-py", - version="0.1.6", + version="0.1.7", author="Julian Benda", author_email="julian.benda@ovgu.de", description="Python bindings for InkCPP a Inkle runtime written in C++", diff --git a/unreal/CMakeLists.txt b/unreal/CMakeLists.txt index 9805e548..cae2cd17 100644 --- a/unreal/CMakeLists.txt +++ b/unreal/CMakeLists.txt @@ -1,4 +1,9 @@ # download inklecate for unreal plugin +set(INKCPP_UNREAL_TARGET_VERSION "5.5" CACHE STRING "Unreal engine version the plugin should target (e.g: 5.5)") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/inkcpp.uplugin.in" + "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/inkcpp.uplugin" +) install(CODE " include(FetchContent) FetchContent_Populate(inklecate_mac diff --git a/unreal/inkcpp/Source/inkcpp/Private/InkAsset.cpp b/unreal/inkcpp/Source/inkcpp/Private/InkAsset.cpp index f79c0d69..b7eb87d9 100644 --- a/unreal/inkcpp/Source/inkcpp/Private/InkAsset.cpp +++ b/unreal/inkcpp/Source/inkcpp/Private/InkAsset.cpp @@ -35,13 +35,15 @@ void UInkAsset::PostInitProperties() Super::PostInitProperties(); } -void UInkAsset::GetAssetRegistryTags(TArray& OutTags) const +void UInkAsset::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const { if (AssetImportData) { - OutTags.Add(FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden)); + Context.AddTag(FAssetRegistryTag( + SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden + )); } - Super::GetAssetRegistryTags(OutTags); + Super::GetAssetRegistryTags(Context); } #endif diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkAsset.h b/unreal/inkcpp/Source/inkcpp/Public/InkAsset.h index 4101c293..038b33c1 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkAsset.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkAsset.h @@ -44,7 +44,7 @@ class INKCPP_API UInkAsset : public UObject /** @private */ virtual void PostInitProperties() override; /** @private */ - virtual void GetAssetRegistryTags(TArray& OutTags) const override; + virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override; // End UObject #endif }; diff --git a/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h b/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h index 467fa66d..0202eda2 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h +++ b/unreal/inkcpp/Source/inkcpp/Public/inkcpp.h @@ -10,10 +10,15 @@ /** * @defgroup unreal Unreal Blueprints * Blueprint Classes usable in Unreal. An example can be found - * [here](unreal/InkCPP_DEMO.zip), do not forgett to install the plugin via the + * [here](../unreal/InkCPP_DEMO.zip), do not forgett to install the plugin via the * [marketplace](https://www.unrealengine.com/marketplace/product/inkcpp) or unzipping the - * `unreal.zip` from the [release page](https://github.com/JBenda/inkcpp/releases/latest) to - * `/YOUR_UNREAL_PROJECT/Plugins/`.
And eitherway activating the plugin. + * `unreal_X_x.zip` from the [release page](https://github.com/JBenda/inkcpp/releases/latest) to + * `/AN_TEMP_DIRECTORY/` and build it with: + * ```sh + * /UNREAL_ENGINE/Build/BatchFiles/RunUAT.bat BuildPlugin + * -plugin=/AN_TEMP_DIRECTORY/inkcpp/inkcpp.uplugin -package=/YOUR_UNREAL_PROJECT/Plugins/inkcpp + * -TargetPlatforms=Win64` + * ```
And eitherway activating the plugin. * * The C++ API will be available soon([Issue](https://github.com/JBenda/inkcpp/issues/60)). * @@ -104,7 +109,8 @@ * * To setup the [example project](../unreal/InkCPP_DEMO.zip) install the Plugin via the [UE * marketplace](https://www.unrealengine.com/product/inkcpp) place unpack - * the `unreal.zip` from the [release page](https://github.com/JBenda/inkcpp/releases/latest) inside + * the `unreal_X_x.zip` from the [release page](https://github.com/JBenda/inkcpp/releases/latest) + * inside * `/PATH/InkCPP_DEMO/Plugins/`. * * Next open the project via the `InkCPP_DEMO/InkCPP_DEMO.uproject` flie. diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp index 19b657de..6b0621c3 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp @@ -24,20 +24,26 @@ #include UInkAssetFactory::UInkAssetFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer), FReimportHandler(), object_ptr(this) + : Super(ObjectInitializer) + , object_ptr(this) { // Add ink format - Formats.Add(FString(TEXT("json;")) + NSLOCTEXT("UInkAssetFactory", "FormatInkJSON", "Ink JSON File").ToString()); - Formats.Add(FString(TEXT("ink;")) + NSLOCTEXT("UInkAssetFactory", "FormatInk", "Ink File").ToString()); + Formats.Add( + FString(TEXT("json;")) + + NSLOCTEXT("UInkAssetFactory", "FormatInkJSON", "Ink JSON File").ToString() + ); + Formats.Add( + FString(TEXT("ink;")) + NSLOCTEXT("UInkAssetFactory", "FormatInk", "Ink File").ToString() + ); // Set class - SupportedClass = UInkAsset::StaticClass(); - bCreateNew = false; - bEditorImport = true; + SupportedClass = UInkAsset::StaticClass(); + bCreateNew = false; + bAutomatedReimport = true; + bForceShowDialog = true; + bEditorImport = true; - // Fuck data tables TODO - some criteria? - ImportPriority = 99999; - FReimportManager::Instance()->RegisterHandler(*this); + ImportPriority = 20; } UInkAssetFactory::~UInkAssetFactory() { FReimportManager::Instance()->UnregisterHandler(*this); } @@ -80,58 +86,71 @@ void TraversImports( } } -UObject* UInkAssetFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, FFeedbackContext* Warn, bool& bOutOperationCanceled) +UObject* UInkAssetFactory::FactoryCreateFile( + UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& Filename, + const TCHAR* Parms, FFeedbackContext* Warn, bool& bOutOperationCanceled +) { - std::stringstream output; - std::stringstream cmd{}; + std::stringstream output; + std::stringstream cmd{}; const std::string inklecate_cmd = get_inklecate_cmd(); static const std::string ink_suffix{".ink"}; - try - { + try { using path = std::filesystem::path; std::string cFilename = TCHAR_TO_ANSI(*Filename); path story_path(cFilename, path::format::generic_format); story_path.make_preferred(); bool use_temp_file = false; if (cFilename.size() > ink_suffix.size() - && std::equal(ink_suffix.rbegin(), ink_suffix.rend(), cFilename.rbegin())) - { + && std::equal(ink_suffix.rbegin(), ink_suffix.rend(), cFilename.rbegin())) { use_temp_file = true; - if(inklecate_cmd.size() == 0) { - UE_LOG(InkCpp, Warning, TEXT("Inklecate provided with the plugin, please import ink.json files")); + if (inklecate_cmd.size() == 0) { + UE_LOG( + InkCpp, Warning, + TEXT("Inklecate provided with the plugin, please import ink.json files") + ); return nullptr; } - path path_bin(TCHAR_TO_ANSI(*IPluginManager::Get().FindPlugin(TEXT("InkCPP"))->GetBaseDir()), path::format::generic_format); + path path_bin( + TCHAR_TO_ANSI(*IPluginManager::Get().FindPlugin(TEXT("InkCPP"))->GetBaseDir()), + path::format::generic_format + ); path_bin.make_preferred(); path_bin /= path(inklecate_cmd, path::format::generic_format).make_preferred(); const char* filename = std::tmpnam(nullptr); - if(filename == nullptr) { + if (filename == nullptr) { UE_LOG(InkCpp, Error, TEXT("Failed to create temporary file")); return nullptr; } cFilename = filename; path json_path(cFilename, path::format::generic_format); json_path.make_preferred(); - cmd - // if std::system start with a quote, the pair of qoute is removed, which leads to errors with pathes with spaces - // but if the quote is not the first symbol it works fine (a"b" is glued to ab from bash) - << path_bin.string()[0] << "\"" << (path_bin.string().c_str() + 1) << "\"" - << " -o \"" << json_path.string() << "\" " - << '"' << story_path.string() << "\" 2>&1"; + cmd + // if std::system start with a quote, the pair of qoute is removed, which leads to errors + // with pathes with spaces but if the quote is not the first symbol it works fine (a"b" is + // glued to ab from bash) + << path_bin.string()[0] << "\"" << (path_bin.string().c_str() + 1) << "\"" + << " -o \"" << json_path.string() << "\" " << '"' << story_path.string() << "\" 2>&1"; auto cmd_str = cmd.str(); - int result = std::system(cmd_str.c_str()); + int result = std::system(cmd_str.c_str()); if (result != 0) { - UE_LOG(InkCpp, Warning, TEXT("Inklecate failed with exit code %i, executed was: '%s'"), result, *FString(cmd_str.c_str())); + UE_LOG( + InkCpp, Warning, TEXT("Inklecate failed with exit code %i, executed was: '%s'"), result, + *FString(cmd_str.c_str()) + ); return nullptr; } } ink::compiler::run(cFilename.c_str(), output); - if(use_temp_file) { + if (use_temp_file) { std::filesystem::remove(cFilename); } // Create ink asset UInkAsset* asset = NewObject(InParent, InClass, InName, Flags); + if (! asset->AssetImportData) { + asset->AssetImportData = NewObject(asset, UAssetImportData::StaticClass()); + } // Load it up std::string data = output.str(); @@ -147,9 +166,7 @@ UObject* UInkAssetFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, // Return return asset; - } - catch (...) - { + } catch (...) { // some kind of error? return nullptr; } @@ -159,77 +176,72 @@ UObject* UInkAssetFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, bool UInkAssetFactory::FactoryCanImport(const FString& Filename) { - return true; // Fuck you Unreal + const FString Extension = FPaths::GetExtension(Filename); + return Extension == TEXT("json") || Extension == TEXT("ink"); } bool UInkAssetFactory::CanReimport(UObject* Obj, TArray& OutFilenames) { // Can reimport story assets UInkAsset* InkAsset = Cast(Obj); - if (InkAsset != nullptr && InkAsset->AssetImportData) - { - UE_LOG(InkCpp, Warning, TEXT("Can Reimport")); + if (InkAsset != nullptr && InkAsset->AssetImportData) { InkAsset->AssetImportData->ExtractFilenames(OutFilenames); return true; } - UE_LOG(InkCpp, Warning, TEXT("Failed to reimport")); return false; } void UInkAssetFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) { - UE_LOG(InkCpp, Warning, TEXT("SetReimportPaths")); - UInkAsset* InkAsset = Cast(Obj); - if (InkAsset != nullptr && NewReimportPaths.Num() > 0) { - InkAsset->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); + UE_LOG(InkCpp, Display, TEXT("SetReimportPaths")); + UInkAsset* obj = Cast(Obj); + if (obj && NewReimportPaths.Num() > 0) { + for (size_t i = 0; i < NewReimportPaths.Num(); ++i) { + obj->AssetImportData->UpdateFilenameOnly(NewReimportPaths[i], i); + } } } -int32 UInkAssetFactory::GetPriority() const -{ - return ImportPriority; -} +int32 UInkAssetFactory::GetPriority() const { return ImportPriority; } TObjectPtr* UInkAssetFactory::GetFactoryObject() const { return const_cast*>(&object_ptr); } -EReimportResult::Type UInkAssetFactory::Reimport(UObject* Obj, int SourceID) +EReimportResult::Type UInkAssetFactory::Reimport(UObject* Obj) { - UE_LOG(InkCpp, Warning, TEXT("Reimport started")); + UE_LOG(InkCpp, Display, TEXT("Reimport started")); UInkAsset* InkAsset = Cast(Obj); - if (!InkAsset) + if (! InkAsset || ! InkAsset->AssetImportData) { return EReimportResult::Failed; + } const FString Filename = InkAsset->AssetImportData->GetFirstFilename(); - if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE) - { + if (! Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE) { return EReimportResult::Failed; } // Run the import again - EReimportResult::Type Result = EReimportResult::Failed; - bool OutCanceled = false; - - if (ImportObject(InkAsset->GetClass(), InkAsset->GetOuter(), *InkAsset->GetName(), RF_Public | RF_Standalone, Filename, nullptr, OutCanceled) != nullptr) - { + EReimportResult::Type Result = EReimportResult::Failed; + bool OutCanceled = false; + + if (ImportObject( + InkAsset->GetClass(), InkAsset->GetOuter(), *InkAsset->GetName(), + RF_Public | RF_Standalone, Filename, nullptr, OutCanceled + ) + != nullptr) { /// TODO: add aditional pathes InkAsset->AssetImportData->Update(Filename); // Try to find the outer package so we can dirty it up - if (InkAsset->GetOuter()) - { + if (InkAsset->GetOuter()) { InkAsset->GetOuter()->MarkPackageDirty(); - } - else - { + } else { InkAsset->MarkPackageDirty(); } Result = EReimportResult::Succeeded; - } - else - { + } else { Result = EReimportResult::Failed; } diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.h b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.h index 6b143b6c..2753b735 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.h +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.h @@ -31,9 +31,7 @@ class UInkAssetFactory : public UFactory, public FReimportHandler // Begin FReimportHandler virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override; virtual TObjectPtr* GetFactoryObject() const override; - virtual EReimportResult::Type Reimport(UObject* Obj, int SourceID) override; - - virtual EReimportResult::Type Reimport(UObject* Obj) override { return Reimport(Obj, 0); } + virtual EReimportResult::Type Reimport(UObject* Obj) override; virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override; virtual int32 GetPriority() const override; diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/inkcpp_editor.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/inkcpp_editor.cpp index 251f3e61..04d607cb 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/inkcpp_editor.cpp +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/inkcpp_editor.cpp @@ -8,6 +8,191 @@ #include "CoreMinimal.h" #include "Modules/ModuleManager.h" +#include +#include "EditorFramework/AssetImportData.h" +#include "InkAsset.h" -IMPLEMENT_MODULE(FDefaultGameModuleImpl, inkcpp_editor) -DEFINE_LOG_CATEGORY(InkCpp); \ No newline at end of file +#define LOCTEXT_NAMESPACE "AssetTypeActions" + +class FInkAssetActions : public IAssetTypeActions +{ +public: + // IAssetTypeActions interface + virtual FText GetName() const override { return LOCTEXT("FInkAssetActions", "InkAsset"); } + + virtual FColor GetTypeColor() const override { return FColor::Red; } + + virtual UClass* GetSupportedClass() const override { return UInkAsset::StaticClass(); } + + virtual uint32 GetCategories() override { return EAssetTypeCategories::Media; } + + virtual bool IsImportedAsset() const override { return true; } + + virtual void GetResolvedSourceFilePaths( + const TArray& TypeAssets, TArray& OutSourceFilePaths + ) const override + { + for (auto& Asset : TypeAssets) { + const auto InkAsset = Cast(Asset); + if (InkAsset->AssetImportData) { + InkAsset->AssetImportData->ExtractFilenames(OutSourceFilePaths); + } + } + } + + // Inherited via IAssetTypeActions + void OpenAssetEditor( + const TArray& InObjects, TSharedPtr EditWithinLevelEditor + ) override + { + } + + void OpenAssetEditor( + const TArray& InObjects, const EAssetTypeActivationOpenedMethod OpenedMethod, + TSharedPtr EditWithinLevelEditor + ) override + { + } + + bool AssetsActivatedOverride( + const TArray& InObjects, EAssetTypeActivationMethod::Type ActivationType + ) override + { + return false; + } + + bool CanRename(const FAssetData& InAsset, FText* OutErrorMsg) const override { return true; } + + bool CanDuplicate(const FAssetData& InAsset, FText* OutErrorMsg) const override { return false; } + + TArray GetValidAssetsForPreviewOrEdit( + TArrayView InAssetDatas, bool bIsPreview + ) override + { + return TArray(); + } + + bool CanFilter() override { return false; } + + FName GetFilterName() const override { return FName(); } + + bool CanLocalize() const override { return false; } + + bool CanMerge() const override { return false; } + + void Merge(UObject* InObject) override {} + + void Merge( + UObject* BaseAsset, UObject* RemoteAsset, UObject* LocalAsset, + const FOnMergeResolved& ResolutionCallback + ) override + { + } + + FString GetObjectDisplayName(UObject* Object) const override { return FString("InkAsset"); } + + const TArray& GetSubMenus() const override + { + // TODO: insert return statement here + static TArray SubMenus; + return SubMenus; + } + + bool ShouldForceWorldCentric() override { return false; } + + void PerformAssetDiff( + UObject* OldAsset, UObject* NewAsset, const FRevisionInfo& OldRevision, + const FRevisionInfo& NewRevision + ) const override + { + } + + UThumbnailInfo* GetThumbnailInfo(UObject* Asset) const override { return nullptr; } + + EThumbnailPrimType GetDefaultThumbnailPrimitiveType(UObject* Asset) const override + { + return EThumbnailPrimType(); + } + + TSharedPtr GetThumbnailOverlay(const FAssetData& AssetData) const override + { + return TSharedPtr(); + } + + FText GetAssetDescription(const FAssetData& AssetData) const override + { + return LOCTEXT("InkAssetDescription", "In Inkle story compiled and ready to use with InkCPP"); + } + + void GetSourceFileLabels(const TArray& TypeAssets, TArray& OutSourceFileLabels) + const override + { + for (const UObject* obj : TypeAssets) { + const UInkAsset* InkAsset = Cast(obj); + if (InkAsset != nullptr && InkAsset->AssetImportData) { + InkAsset->AssetImportData->ExtractDisplayLabels(OutSourceFileLabels); + } + } + } + + void BuildBackendFilter(FARFilter& InFilter) override {} + + FText GetDisplayNameFromAssetData(const FAssetData& AssetData) const override { return FText(); } + + FTopLevelAssetPath GetClassPathName() const override + { + return GetSupportedClass()->GetClassPathName(); + } + + bool SupportsOpenedMethod(const EAssetTypeActivationOpenedMethod OpenedMethod) const override + { + return false; + } + + const FSlateBrush* + GetThumbnailBrush(const FAssetData& InAssetData, const FName InClassName) const override + { + return nullptr; + } + + const FSlateBrush* + GetIconBrush(const FAssetData& InAssetData, const FName InClassName) const override + { + return nullptr; + } + + // End of IAssetTypeActions interface +}; + +class FInkAssetModuleImpl : public FDefaultModuleImpl +{ +public: + virtual void StartupModule() override + { + // Register asset types + IAssetTools& AssetTools + = FModuleManager::LoadModuleChecked("AssetTools").Get(); + + SpriteSheetImportAssetTypeActions = MakeShareable(new FInkAssetActions); + AssetTools.RegisterAssetTypeActions(SpriteSheetImportAssetTypeActions.ToSharedRef()); + } + + virtual void ShutdownModule() override + { + if (FModuleManager::Get().IsModuleLoaded("AssetTools")) { + IAssetTools& AssetTools + = FModuleManager::GetModuleChecked("AssetTools").Get(); + if (SpriteSheetImportAssetTypeActions.IsValid()) { + AssetTools.UnregisterAssetTypeActions(SpriteSheetImportAssetTypeActions.ToSharedRef()); + } + } + } + +private: + TSharedPtr SpriteSheetImportAssetTypeActions; +}; + +////////////////////////////////////////////////////////////////////////// + +IMPLEMENT_MODULE(FInkAssetModuleImpl, inkcpp_editor); +DEFINE_LOG_CATEGORY(InkCpp); diff --git a/unreal/inkcpp/inkcpp.uplugin b/unreal/inkcpp/inkcpp.uplugin.in similarity index 89% rename from unreal/inkcpp/inkcpp.uplugin rename to unreal/inkcpp/inkcpp.uplugin.in index 0f9e8793..95b5fbbe 100644 --- a/unreal/inkcpp/inkcpp.uplugin +++ b/unreal/inkcpp/inkcpp.uplugin.in @@ -1,8 +1,8 @@ { "FileVersion": 1, - "EngineVersion": "5.4", + "EngineVersion": "${INKCPP_UNREAL_TARGET_VERSION}", "Version": 1, - "VersionName": "0.1.6", + "VersionName": "${CMAKE_PROJECT_VERSION}", "FriendlyName": "inkcpp", "Description": "inkcpp integration", "Category": "Narrative",