diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 7adab0a..3f16f98 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -6,24 +6,23 @@ on:
pull_request:
branches: [ "master" ]
-# https://devblogs.microsoft.com/cppblog/vcpkg-integration-with-the-github-dependency-graph/
-permissions:
- contents: write
-
env:
- # https://devblogs.microsoft.com/cppblog/vcpkg-integration-with-the-github-dependency-graph/
- GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- VCPKG_ROOT: "C:/my-vcpkg"
VCPKG_DEFAULT_BINARY_CACHE: "C:/vcpkg-binary-cache"
- VCPKG_FEATURE_FLAGS: dependencygraph
- BUILD_TYPE: RelWithDebInfo
jobs:
build:
runs-on: windows-2022
+ strategy:
+ matrix:
+ arch: [ x64, x86 ]
steps:
- uses: actions/checkout@v4
+ - name: Setup Developer Command Prompt
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: ${{ matrix.arch }}
+
- name: Get project vcpkg baseline
shell: pwsh
run: |
@@ -33,14 +32,14 @@ jobs:
- name: Cache vcpkg
uses: actions/cache@v4
with:
- key: vcpkg-${{ hashFiles('vcpkg.json') }}
+ key: vcpkg-${{ matrix.arch }}-${{ hashFiles('vcpkg.json') }}
path: |
${{env.VCPKG_DEFAULT_BINARY_CACHE}}
- name: Setup vcpkg
run: |
- New-Item -ItemType Directory -Path ${{env.VCPKG_ROOT}}
- Set-Location -Path ${{env.VCPKG_ROOT}}
+ New-Item -ItemType Directory -Path C:/my-vcpkg
+ Set-Location -Path C:/my-vcpkg
git init
git remote add --no-tags origin https://github.com/microsoft/vcpkg.git
git fetch --depth 1 --no-write-fetch-head origin ${{env.VCPKG_BASELINE}}
@@ -48,25 +47,49 @@ jobs:
git checkout
./bootstrap-vcpkg.bat
New-Item -ItemType Directory -Path ${{env.VCPKG_DEFAULT_BINARY_CACHE}} -Force
+ echo "VCPKG_ROOT=C:/my-vcpkg" >> $env:GITHUB_ENV
- name: Configure CMake
- run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} --toolchain ${{env.VCPKG_ROOT}}/scripts/buildsystems/vcpkg.cmake
+ run: cmake --preset ${{ matrix.arch }}-release
- name: Build
- run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
+ run: cmake --build ${{github.workspace}}/build/${{ matrix.arch }}-release
- name: Pack
run: |
- cd ${{github.workspace}}/build
- cpack --config CPackConfig.cmake -C ${{env.BUILD_TYPE}}
+ cd ${{github.workspace}}/build/${{ matrix.arch }}-release
+ cpack --config CPackConfig.cmake -C RelWithDebInfo
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: observer-modules-${{ matrix.arch }}
+ path: ${{github.workspace}}/build/${{ matrix.arch }}-release/*.zip
- - name: Release nightly build
- uses: andelf/nightly-release@main
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
+ release:
+ needs: build
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ steps:
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
with:
- name: 'Nightly Release $$'
- body: 'This is an automated nightly build. Download the *-dll.zip files if you need Observer modules. Download the *-pdb.zip files if you need debug symbols.'
- tag_name: nightly
+ merge-multiple: true
+ path: ./artifacts
+
+ - name: Generate release tag
+ id: tag
+ run: echo "tag=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: release-${{ steps.tag.outputs.tag }}
+ name: 'Release ${{ steps.tag.outputs.tag }}'
+ body: |
+ Automated release from master branch.
+
+ Download the *-dll.zip files if you need Observer modules.
+ Download the *-pdb.zip files if you need debug symbols.
+ files: ./artifacts/*.zip
prerelease: false
- files: |
- ./build/*.zip
diff --git a/.idea/cmake.xml b/.idea/cmake.xml
deleted file mode 100644
index 090753e..0000000
--- a/.idea/cmake.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index cfd78c7..7fbe760 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -3,23 +3,33 @@
andelf
auriemma
+ birkenfeld
bstatic
+ catchorg
debugfarhome
dependencygraph
dest
funcs
+ ilammy
+ lazyhamster
+ luxrck
makemoduleversion
+ mateidavid
nomoreitems
popd
pushd
+ refaim's
ren'
renpy
rgssad
rpatool
rpgmaker
+ shizmob
+ softprops
strbuf
thirdparty
userabort
+ vcvars
wstring
xxhash
zanzapak
diff --git a/.idea/runConfigurations/Far_Manager__Debug_.xml b/.idea/runConfigurations/Far_Manager__Debug_.xml
index 84791f8..3fe8131 100644
--- a/.idea/runConfigurations/Far_Manager__Debug_.xml
+++ b/.idea/runConfigurations/Far_Manager__Debug_.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/.idea/runConfigurations/Far_Manager__Release_.xml b/.idea/runConfigurations/Far_Manager__Release_.xml
index ec1a695..3da7b08 100644
--- a/.idea/runConfigurations/Far_Manager__Release_.xml
+++ b/.idea/runConfigurations/Far_Manager__Release_.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/.idea/runConfigurations/Run_Tests.xml b/.idea/runConfigurations/Run_Tests.xml
index da68c74..b5dd467 100644
--- a/.idea/runConfigurations/Run_Tests.xml
+++ b/.idea/runConfigurations/Run_Tests.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7284bc3..7674aa1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,21 +1,29 @@
cmake_minimum_required(VERSION 3.31)
+if (DEFINED ENV{VCPKG_ROOT})
+ set(VCPKG_ROOT "$ENV{VCPKG_ROOT}")
+elseif (DEFINED ENV{USERPROFILE})
+ set(VCPKG_ROOT "$ENV{USERPROFILE}/.vcpkg-clion/vcpkg")
+endif ()
+if (NOT EXISTS ${VCPKG_ROOT})
+ message(FATAL_ERROR "VCPKG_ROOT is not defined. Please set it to the path of your vcpkg installation.")
+endif ()
+file(TO_CMAKE_PATH ${VCPKG_ROOT} VCPKG_ROOT)
+set(CMAKE_TOOLCHAIN_FILE "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
+
set(VCPKG_CRT_LINKAGE static)
set(VCPKG_LIBRARY_LINKAGE static)
-set(VCPKG_TARGET_TRIPLET x64-windows-static)
+set(VCPKG_TARGET_TRIPLET ${OBSERVER_ARCHITECTURE}-windows-static)
project(observer_modules LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
-
add_compile_options("/W3" "/analyze")
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>")
-set(CMAKE_VERBOSE_MAKEFILE ON)
set(ZLIB_USE_STATIC_LIBS ON)
find_package(ZLIB REQUIRED)
find_path(ZSTR_INCLUDE_DIRS "zstr.hpp")
-find_package(Python3 COMPONENTS Development REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(xxHash CONFIG REQUIRED)
@@ -27,13 +35,10 @@ add_library(renpy SHARED
src/dll.cpp
src/archive.cpp
${RENPY_DIR}/renpy.cpp
- ${RENPY_DIR}/python/context.cpp
- ${RENPY_DIR}/python/dict.cpp
- ${RENPY_DIR}/python/list.cpp
- ${RENPY_DIR}/python/tuple.cpp)
+ ${RENPY_DIR}/pickle.cpp
+)
target_link_libraries(renpy PRIVATE ZLIB::ZLIB)
target_include_directories(renpy PRIVATE ${ZSTR_INCLUDE_DIRS})
-target_link_libraries(renpy PRIVATE Python3::Python)
add_library(rpgmaker SHARED src/dll.cpp src/archive.cpp src/modules/rpgmaker/rpgmaker.cpp)
@@ -50,7 +55,8 @@ add_executable(tests
src/tests/framework/observer.cpp
src/tests/framework/testcase.cpp
src/tests/renpy.cpp
- src/tests/zanzarah.cpp)
+ src/tests/zanzarah.cpp
+)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
target_link_libraries(tests PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(tests PRIVATE xxHash::xxhash)
@@ -75,8 +81,8 @@ foreach (MODULE IN LISTS RELEASED_MODULES)
install(FILES "$" DESTINATION . COMPONENT ${MODULE}_pdb)
string(TOUPPER ${MODULE} MODULE_UPPER)
- set(CPACK_ARCHIVE_${MODULE_UPPER}_FILE_NAME "${MODULE}-${TODAY}-amd64-dll")
- set(CPACK_ARCHIVE_${MODULE_UPPER}_PDB_FILE_NAME "${MODULE}-${TODAY}-amd64-pdb")
+ set(CPACK_ARCHIVE_${MODULE_UPPER}_FILE_NAME "${MODULE}-${TODAY}-${OBSERVER_ARCHITECTURE}-dll")
+ set(CPACK_ARCHIVE_${MODULE_UPPER}_PDB_FILE_NAME "${MODULE}-${TODAY}-${OBSERVER_ARCHITECTURE}-pdb")
endforeach ()
list(TRANSFORM RELEASED_MODULES APPEND "_pdb" OUTPUT_VARIABLE ALL_COMPONENTS)
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 0000000..cdb22cd
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,67 @@
+{
+ "version": 3,
+ "configurePresets": [
+ {
+ "hidden": true,
+ "name": "default",
+ "generator": "Ninja",
+ "vendor": {
+ "jetbrains.com/clion": {
+ "toolchain": "Visual Studio"
+ }
+ }
+ },
+ {
+ "name": "x64-debug",
+ "inherits": "default",
+ "binaryDir": "${sourceDir}/build/x64-debug",
+ "architecture": {
+ "value": "x64",
+ "strategy": "external"
+ },
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug",
+ "OBSERVER_ARCHITECTURE": "x64"
+ }
+ },
+ {
+ "name": "x64-release",
+ "inherits": "default",
+ "binaryDir": "${sourceDir}/build/x64-release",
+ "architecture": {
+ "value": "x64",
+ "strategy": "external"
+ },
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "RelWithDebInfo",
+ "OBSERVER_ARCHITECTURE": "x64"
+ }
+ },
+ {
+ "name": "x86-debug",
+ "inherits": "default",
+ "binaryDir": "${sourceDir}/build/x86-debug",
+ "architecture": {
+ "value": "Win32",
+ "strategy": "external"
+ },
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug",
+ "OBSERVER_ARCHITECTURE": "x86"
+ }
+ },
+ {
+ "name": "x86-release",
+ "inherits": "default",
+ "binaryDir": "${sourceDir}/build/x86-release",
+ "architecture": {
+ "value": "Win32",
+ "strategy": "external"
+ },
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "RelWithDebInfo",
+ "OBSERVER_ARCHITECTURE": "x86"
+ }
+ }
+ ]
+}
diff --git a/README.md b/README.md
index 89b49ce..fe931d5 100644
--- a/README.md
+++ b/README.md
@@ -56,23 +56,23 @@ specific files as needed without having to unpack the entire archive.
## Third-party components
-| Project | License |
-|-----------------------------------------------------|-------------------------------------|
-| [json](https://github.com/nlohmann/json) | [MIT](licenses/json.txt) |
-| [Catch2](https://github.com/catchorg/Catch2) | [BSL-1.0](licenses/Boost.txt) |
-| [Observer](https://github.com/lazyhamster/Observer) | [LGPL-3.0](licenses/Observer.txt) |
-| [Python](https://www.python.org) | [PSF-2.0](licenses/Python.txt) |
-| [xxHash](https://github.com/Cyan4973/xxHash) | [BSD-2-Clause](licenses/xxHash.txt) |
-| [zlib](https://zlib.net) | [zlib](licenses/zlib.txt) |
-| [zstr](https://github.com/mateidavid/zstr) | [MIT](licenses/zstr.txt) |
+| Project | License |
+|-----------------------------------------------------------------|-------------------------------------|
+| [nlohmann/json](https://github.com/nlohmann/json) | [MIT](licenses/json.txt) |
+| [catchorg/Catch2](https://github.com/catchorg/Catch2) | [BSL-1.0](licenses/Catch2.txt) |
+| [lazyhamster/Observer](https://github.com/lazyhamster/Observer) | [LGPL-3.0](licenses/Observer.txt) |
+| [Cyan4973/xxHash](https://github.com/Cyan4973/xxHash) | [BSD-2-Clause](licenses/xxHash.txt) |
+| [zlib](https://zlib.net) | [zlib](licenses/zlib.txt) |
+| [mateidavid/zstr](https://github.com/mateidavid/zstr) | [MIT](licenses/zstr.txt) |
## Sources of inspiration
-| Project | License |
-|------------------------------------------------------------------|----------------------------------|
-| [rgssad](https://github.com/luxrck/rgssad) | [MIT](licenses/rgssad.txt) |
-| [rpatool](https://github.com/Shizmob/rpatool) | [WTFPL](licenses/rpatool.txt) |
-| [zanzapak](https://aluigi.altervista.org/papers.htm#others-file) | [GPL-3.0](licenses/zanzapak.txt) |
+| Project | License |
+|-----------------------------------------------------------------------|----------------------------------|
+| [luxrck/rgssad](https://github.com/luxrck/rgssad) | [MIT](licenses/rgssad.txt) |
+| [birkenfeld/serde-pickle](https://github.com/birkenfeld/serde-pickle) | [MIT](licenses/serde-pickle.txt) |
+| [Shizmob/rpatool](https://github.com/Shizmob/rpatool) | [WTFPL](licenses/rpatool.txt) |
+| [zanzapak](https://aluigi.altervista.org/papers.htm#others-file) | [GPL-3.0](licenses/zanzapak.txt) |
## Building from Source
diff --git a/licenses/Python.txt b/licenses/Python.txt
deleted file mode 100644
index 1038ea3..0000000
--- a/licenses/Python.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and
- the Individual or Organization ("Licensee") accessing and otherwise using this
- software ("Python") in source or binary form and its associated documentation.
-
-2. Subject to the terms and conditions of this License Agreement, PSF hereby
- grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
- analyze, test, perform and/or display publicly, prepare derivative works,
- distribute, and otherwise use Python alone or in any derivative
- version, provided, however, that PSF's License Agreement and PSF's notice of
- copyright, i.e., "Copyright © 2001-2024 Python Software Foundation; All Rights
- Reserved" are retained in Python alone or in any derivative version
- prepared by Licensee.
-
-3. In the event Licensee prepares a derivative work that is based on or
- incorporates Python or any part thereof, and wants to make the
- derivative work available to others as provided herein, then Licensee hereby
- agrees to include in any such work a brief summary of the changes made to Python.
-
-4. PSF is making Python available to Licensee on an "AS IS" basis.
- PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF
- EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR
- WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE
- USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
-
-5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
- FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF
- MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE
- THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
-
-6. This License Agreement will automatically terminate upon a material breach of
- its terms and conditions.
-
-7. Nothing in this License Agreement shall be deemed to create any relationship
- of agency, partnership, or joint venture between PSF and Licensee. This License
- Agreement does not grant permission to use PSF trademarks or trade name in a
- trademark sense to endorse or promote products or services of Licensee, or any
- third party.
-
-8. By copying, installing or otherwise using Python, Licensee agrees
- to be bound by the terms and conditions of this License Agreement.
diff --git a/licenses/serde-pickle.txt b/licenses/serde-pickle.txt
new file mode 100644
index 0000000..39d4bdb
--- /dev/null
+++ b/licenses/serde-pickle.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2014 The Rust Project Developers
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/src/modules/renpy/pickle.cpp b/src/modules/renpy/pickle.cpp
new file mode 100644
index 0000000..f3893bf
--- /dev/null
+++ b/src/modules/renpy/pickle.cpp
@@ -0,0 +1,573 @@
+#include "pickle.h"
+
+namespace pickle
+{
+ // Pickle opcodes (subset needed for basic functionality)
+ namespace opcodes
+ {
+ constexpr uint8_t MARK = '(';
+ constexpr uint8_t STOP = '.';
+ constexpr uint8_t POP = '0';
+ constexpr uint8_t POP_MARK = '1';
+ constexpr uint8_t DUP = '2';
+ constexpr uint8_t FLOAT = 'F';
+ constexpr uint8_t INT = 'I';
+ constexpr uint8_t BININT = 'J';
+ constexpr uint8_t BININT1 = 'K';
+ constexpr uint8_t BININT2 = 'M';
+ constexpr uint8_t NONE = 'N';
+ constexpr uint8_t PERSID = 'P';
+ constexpr uint8_t BINPERSID = 'Q';
+ constexpr uint8_t REDUCE = 'R';
+ constexpr uint8_t STRING = 'S';
+ constexpr uint8_t BINSTRING = 'T';
+ constexpr uint8_t SHORT_BINSTRING = 'U';
+ constexpr uint8_t UNICODE = 'V';
+ constexpr uint8_t BINUNICODE = 'X';
+ constexpr uint8_t APPEND = 'a';
+ constexpr uint8_t BUILD = 'b';
+ constexpr uint8_t GLOBAL = 'c';
+ constexpr uint8_t DICT = 'd';
+ constexpr uint8_t EMPTY_DICT = '}';
+ constexpr uint8_t APPENDS = 'e';
+ constexpr uint8_t GET = 'g';
+ constexpr uint8_t BINGET = 'h';
+ constexpr uint8_t INST = 'i';
+ constexpr uint8_t LONG_BINGET = 'j';
+ constexpr uint8_t LIST = 'l';
+ constexpr uint8_t EMPTY_LIST = ']';
+ constexpr uint8_t OBJ = 'o';
+ constexpr uint8_t PUT = 'p';
+ constexpr uint8_t BINPUT = 'q';
+ constexpr uint8_t LONG_BINPUT = 'r';
+ constexpr uint8_t SETITEM = 's';
+ constexpr uint8_t TUPLE = 't';
+ constexpr uint8_t EMPTY_TUPLE = ')';
+ constexpr uint8_t SETITEMS = 'u';
+ constexpr uint8_t BINFLOAT = 'G';
+
+ // Protocol 2
+ constexpr uint8_t PROTO = 0x80;
+ constexpr uint8_t NEWOBJ = 0x81;
+ constexpr uint8_t EXT1 = 0x82;
+ constexpr uint8_t EXT2 = 0x83;
+ constexpr uint8_t EXT4 = 0x84;
+ constexpr uint8_t TUPLE1 = 0x85;
+ constexpr uint8_t TUPLE2 = 0x86;
+ constexpr uint8_t TUPLE3 = 0x87;
+ constexpr uint8_t NEWTRUE = 0x88;
+ constexpr uint8_t NEWFALSE = 0x89;
+ constexpr uint8_t LONG1 = 0x8a;
+ constexpr uint8_t LONG4 = 0x8b;
+
+ // Protocol 3
+ constexpr uint8_t BINBYTES = 'B';
+ constexpr uint8_t SHORT_BINBYTES = 'C';
+
+ // Protocol 4
+ constexpr uint8_t SHORT_BINUNICODE = 0x8c;
+ constexpr uint8_t BINUNICODE8 = 0x8d;
+ constexpr uint8_t BINBYTES8 = 0x8e;
+ constexpr uint8_t EMPTY_SET = 0x8f;
+ constexpr uint8_t ADDITEMS = 0x90;
+ constexpr uint8_t FROZENSET = 0x91;
+ constexpr uint8_t NEWOBJ_EX = 0x92;
+ constexpr uint8_t STACK_GLOBAL = 0x93;
+ constexpr uint8_t MEMOIZE = 0x94;
+ constexpr uint8_t FRAME = 0x95;
+ }
+
+ uint8_t parser::read_byte()
+ {
+ if (pos_ >= data_.size()) {
+ throw std::runtime_error("Unexpected end of pickle data");
+ }
+ return static_cast(data_[pos_++]);
+ }
+
+ uint16_t parser::read_uint16_le()
+ {
+ if (pos_ + 2 > data_.size()) {
+ throw std::runtime_error("Unexpected end of pickle data");
+ }
+ const uint16_t result = static_cast(data_[pos_]) |
+ static_cast(data_[pos_ + 1]) << 8;
+ pos_ += 2;
+ return result;
+ }
+
+ uint32_t parser::read_uint32_le()
+ {
+ if (pos_ + 4 > data_.size()) {
+ throw std::runtime_error("Unexpected end of pickle data");
+ }
+ const uint32_t result = static_cast(data_[pos_]) |
+ static_cast(data_[pos_ + 1]) << 8 |
+ static_cast(data_[pos_ + 2]) << 16 |
+ static_cast(data_[pos_ + 3]) << 24;
+ pos_ += 4;
+ return result;
+ }
+
+ uint64_t parser::read_uint64_le()
+ {
+ if (pos_ + 8 > data_.size()) {
+ throw std::runtime_error("Unexpected end of pickle data");
+ }
+ const uint64_t result = static_cast(data_[pos_]) |
+ static_cast(data_[pos_ + 1]) << 8 |
+ static_cast(data_[pos_ + 2]) << 16 |
+ static_cast(data_[pos_ + 3]) << 24 |
+ static_cast(data_[pos_ + 4]) << 32 |
+ static_cast(data_[pos_ + 5]) << 40 |
+ static_cast(data_[pos_ + 6]) << 48 |
+ static_cast(data_[pos_ + 7]) << 56;
+ pos_ += 8;
+ return result;
+ }
+
+ std::string parser::read_string(const size_t length)
+ {
+ if (pos_ + length > data_.size()) {
+ throw std::runtime_error("Unexpected end of pickle data");
+ }
+ std::string result(reinterpret_cast(data_.data() + pos_), length);
+ pos_ += length;
+ return result;
+ }
+
+ std::string parser::read_line()
+ {
+ const size_t start = pos_;
+ while (pos_ < data_.size() && data_[pos_] != std::byte{'\n'}) {
+ pos_++;
+ }
+ if (pos_ >= data_.size()) {
+ throw std::runtime_error("Unexpected end of pickle data");
+ }
+ std::string result(reinterpret_cast(data_.data() + start), pos_ - start);
+ pos_++; // skip newline
+ return result;
+ }
+
+ void parser::push_mark()
+ {
+ mark_stack_.push_back(stack_.size());
+ }
+
+ list parser::pop_to_mark()
+ {
+ if (mark_stack_.empty()) {
+ throw std::runtime_error("No mark on stack");
+ }
+ const size_t mark_pos = mark_stack_.back();
+ mark_stack_.pop_back();
+
+ list result;
+ result.reserve(stack_.size() - mark_pos);
+ for (size_t i = mark_pos; i < stack_.size(); ++i) {
+ result.push_back(std::move(stack_[i]));
+ }
+ stack_.resize(mark_pos);
+ return result;
+ }
+
+ value_ptr parser::parse_value()
+ {
+ switch (uint8_t opcode = read_byte()) {
+ case opcodes::MARK:
+ push_mark();
+ break;
+
+ case opcodes::STOP:
+ if (stack_.size() != 1) {
+ throw std::runtime_error("Invalid stack size at end of pickle");
+ }
+ return std::move(stack_[0]);
+
+ case opcodes::NONE:
+ stack_.push_back(value::none());
+ break;
+
+ case opcodes::NEWTRUE:
+ stack_.push_back(value::boolean(true));
+ break;
+
+ case opcodes::NEWFALSE:
+ stack_.push_back(value::boolean(false));
+ break;
+
+ case opcodes::BININT:
+ stack_.push_back(value::int64(static_cast(read_uint32_le())));
+ break;
+
+ case opcodes::BININT1:
+ stack_.push_back(value::int64(read_byte()));
+ break;
+
+ case opcodes::BININT2:
+ stack_.push_back(value::int64(read_uint16_le()));
+ break;
+
+ case opcodes::INT:
+ {
+ std::string int_str = read_line();
+ if (int_str.back() == 'L') {
+ int_str.pop_back(); // Remove trailing L
+ }
+ int64_t val = std::stoll(int_str);
+ stack_.push_back(value::int64(val));
+ break;
+ }
+
+ case opcodes::BINFLOAT:
+ {
+ uint64_t bits = read_uint64_le();
+ double val;
+ std::memcpy(&val, &bits, sizeof(double));
+ stack_.push_back(value::float64(val));
+ break;
+ }
+
+ case opcodes::SHORT_BINSTRING:
+ {
+ uint8_t length = read_byte();
+ stack_.push_back(value::string(read_string(length)));
+ break;
+ }
+
+ case opcodes::BINSTRING:
+ {
+ uint32_t length = read_uint32_le();
+ stack_.push_back(value::string(read_string(length)));
+ break;
+ }
+
+ case opcodes::SHORT_BINUNICODE:
+ {
+ uint8_t length = read_byte();
+ stack_.push_back(value::string(read_string(length)));
+ break;
+ }
+
+ case opcodes::BINUNICODE:
+ {
+ uint32_t length = read_uint32_le();
+ stack_.push_back(value::string(read_string(length)));
+ break;
+ }
+
+ case opcodes::SHORT_BINBYTES:
+ {
+ uint8_t length = read_byte();
+ stack_.push_back(value::bytes(read_string(length)));
+ break;
+ }
+
+ case opcodes::BINBYTES:
+ {
+ uint32_t length = read_uint32_le();
+ stack_.push_back(value::bytes(read_string(length)));
+ break;
+ }
+
+ case opcodes::EMPTY_LIST:
+ stack_.push_back(value::list({}));
+ break;
+
+ case opcodes::APPEND:
+ {
+ if (stack_.size() < 2) {
+ throw std::runtime_error("Not enough items on stack for APPEND");
+ }
+ auto item = std::move(stack_.back());
+ stack_.pop_back();
+ auto &list_val = stack_.back();
+ if (list_val->get_type() != value::type::list) {
+ throw std::runtime_error("APPEND target is not a list");
+ }
+ auto &list_data = const_cast(list_val->as_list());
+ list_data.push_back(std::move(item));
+ break;
+ }
+
+ case opcodes::APPENDS:
+ {
+ auto items = pop_to_mark();
+ if (stack_.empty()) {
+ throw std::runtime_error("No list on stack for APPENDS");
+ }
+ auto &list_val = stack_.back();
+ if (list_val->get_type() != value::type::list) {
+ throw std::runtime_error("APPENDS target is not a list");
+ }
+ auto &list_data = const_cast(list_val->as_list());
+ for (auto &item: items) {
+ list_data.push_back(std::move(item));
+ }
+ break;
+ }
+
+ case opcodes::LIST:
+ {
+ auto items = pop_to_mark();
+ stack_.push_back(value::list(std::move(items)));
+ break;
+ }
+
+ case opcodes::EMPTY_TUPLE:
+ stack_.push_back(value::tuple({}));
+ break;
+
+ case opcodes::TUPLE:
+ {
+ auto items = pop_to_mark();
+ stack_.push_back(value::tuple(std::move(items)));
+ break;
+ }
+
+ case opcodes::TUPLE1:
+ {
+ if (stack_.empty()) {
+ throw std::runtime_error("Not enough items on stack for TUPLE1");
+ }
+ auto item = std::move(stack_.back());
+ stack_.pop_back();
+ list tuple_items;
+ tuple_items.push_back(std::move(item));
+ stack_.push_back(value::tuple(std::move(tuple_items)));
+ break;
+ }
+
+ case opcodes::TUPLE2:
+ {
+ if (stack_.size() < 2) {
+ throw std::runtime_error("Not enough items on stack for TUPLE2");
+ }
+ auto item2 = std::move(stack_.back());
+ stack_.pop_back();
+ auto item1 = std::move(stack_.back());
+ stack_.pop_back();
+ list tuple_items;
+ tuple_items.push_back(std::move(item1));
+ tuple_items.push_back(std::move(item2));
+ stack_.push_back(value::tuple(std::move(tuple_items)));
+ break;
+ }
+
+ case opcodes::TUPLE3:
+ {
+ if (stack_.size() < 3) {
+ throw std::runtime_error("Not enough items on stack for TUPLE3");
+ }
+ auto item3 = std::move(stack_.back());
+ stack_.pop_back();
+ auto item2 = std::move(stack_.back());
+ stack_.pop_back();
+ auto item1 = std::move(stack_.back());
+ stack_.pop_back();
+ list tuple_items;
+ tuple_items.push_back(std::move(item1));
+ tuple_items.push_back(std::move(item2));
+ tuple_items.push_back(std::move(item3));
+ stack_.push_back(value::tuple(std::move(tuple_items)));
+ break;
+ }
+
+ case opcodes::EMPTY_DICT:
+ stack_.push_back(value::dict({}));
+ break;
+
+ case opcodes::DICT:
+ {
+ auto items = pop_to_mark();
+ if (items.size() % 2 != 0) {
+ throw std::runtime_error("Odd number of items for DICT");
+ }
+ dict dict_data;
+ for (size_t i = 0; i < items.size(); i += 2) {
+ if (items[i]->get_type() != value::type::string) {
+ throw std::runtime_error("Dict key must be string");
+ }
+ std::string key = items[i]->as_string();
+ dict_data[std::move(key)] = std::move(items[i + 1]);
+ }
+ stack_.push_back(value::dict(std::move(dict_data)));
+ break;
+ }
+
+ case opcodes::SETITEM:
+ {
+ if (stack_.size() < 3) {
+ throw std::runtime_error("Not enough items on stack for SETITEM");
+ }
+ auto val = std::move(stack_.back());
+ stack_.pop_back();
+ auto key = std::move(stack_.back());
+ stack_.pop_back();
+ auto &dict_val = stack_.back();
+
+ if (dict_val->get_type() != value::type::dict) {
+ throw std::runtime_error("SETITEM target is not a dict");
+ }
+ if (key->get_type() != value::type::string) {
+ throw std::runtime_error("Dict key must be string");
+ }
+
+ auto &dict_data = const_cast(dict_val->as_dict());
+ dict_data[key->as_string()] = std::move(val);
+ break;
+ }
+
+ case opcodes::SETITEMS:
+ {
+ auto items = pop_to_mark();
+ if (items.size() % 2 != 0) {
+ throw std::runtime_error("Odd number of items for SETITEMS");
+ }
+ if (stack_.empty()) {
+ throw std::runtime_error("No dict on stack for SETITEMS");
+ }
+ auto &dict_val = stack_.back();
+ if (dict_val->get_type() != value::type::dict) {
+ throw std::runtime_error("SETITEMS target is not a dict");
+ }
+
+ auto &dict_data = const_cast(dict_val->as_dict());
+ for (size_t i = 0; i < items.size(); i += 2) {
+ if (items[i]->get_type() != value::type::string) {
+ throw std::runtime_error("Dict key must be string");
+ }
+ std::string key = items[i]->as_string();
+ dict_data[std::move(key)] = std::move(items[i + 1]);
+ }
+ break;
+ }
+
+ case opcodes::BINPUT:
+ {
+ uint8_t memo_id = read_byte();
+ if (stack_.empty()) {
+ throw std::runtime_error("No item on stack for BINPUT");
+ }
+ // For simplicity, we create a copy for memo storage
+ // In a full implementation, you'd want to share the object
+ memo_[memo_id] = nullptr; // Placeholder for now
+ break;
+ }
+
+ case opcodes::LONG_BINPUT:
+ {
+ uint32_t memo_id = read_uint32_le();
+ if (stack_.empty()) {
+ throw std::runtime_error("No item on stack for LONG_BINPUT");
+ }
+ // For simplicity, we create a copy for memo storage
+ // In a full implementation, you'd want to share the object
+ memo_[memo_id] = nullptr; // Placeholder for now
+ break;
+ }
+
+ case opcodes::BINGET:
+ {
+ uint8_t memo_id = read_byte();
+ if (auto it = memo_.find(memo_id); it == memo_.end()) {
+ throw std::runtime_error("Memo key not found");
+ }
+ // For now, just push a placeholder
+ stack_.push_back(value::none());
+ break;
+ }
+
+ case opcodes::LONG_BINGET:
+ {
+ uint32_t memo_id = read_uint32_le();
+ if (auto it = memo_.find(memo_id); it == memo_.end()) {
+ throw std::runtime_error("Memo key not found");
+ }
+ // For now, just push a placeholder
+ stack_.push_back(value::none());
+ break;
+ }
+
+ case opcodes::PROTO:
+ {
+ uint8_t proto = read_byte();
+ // Just ignore protocol version for now
+ break;
+ }
+
+ case opcodes::FRAME:
+ {
+ uint64_t frame_size = read_uint64_le();
+ // Just ignore frame size for now
+ break;
+ }
+
+ case opcodes::LONG1:
+ {
+ uint8_t length = read_byte();
+ if (length == 0) {
+ stack_.push_back(value::int64(0));
+ } else {
+ std::string bytes_data = read_string(length);
+ int64_t result = 0;
+
+ // Convert little-endian bytes to integer
+ for (int i = length - 1; i >= 0; --i) {
+ result = result << 8 | static_cast(bytes_data[i]);
+ }
+
+ // Handle two's complement for negative numbers
+ if (length > 0 && static_cast(bytes_data[length - 1]) & 0x80) {
+ // Extend sign bit
+ for (int i = length; i < 8; ++i) {
+ result |= 0xFFLL << i * 8;
+ }
+ }
+
+ stack_.push_back(value::int64(result));
+ }
+ break;
+ }
+
+ case opcodes::MEMOIZE:
+ {
+ if (stack_.empty()) {
+ throw std::runtime_error("No item on stack for MEMOIZE");
+ }
+ // Store the top item in memo with auto-incrementing ID
+ auto memo_id = static_cast(memo_.size());
+ memo_[memo_id] = nullptr; // Placeholder for now
+ break;
+ }
+
+ default:
+ throw std::runtime_error("Unsupported pickle opcode: " + std::to_string(opcode));
+ }
+
+ return nullptr; // Continue parsing
+ }
+
+ value_ptr parser::parse()
+ {
+ while (pos_ < data_.size()) {
+ if (auto result = parse_value()) {
+ return result;
+ }
+ }
+ throw std::runtime_error("Unexpected end of pickle data");
+ }
+
+ value_ptr loads(const std::span data)
+ {
+ parser p(data);
+ return p.parse();
+ }
+
+ value_ptr loads(const std::string &data)
+ {
+ const auto byte_span = std::span(reinterpret_cast(data.data()), data.size());
+ return loads(byte_span);
+ }
+}
diff --git a/src/modules/renpy/pickle.h b/src/modules/renpy/pickle.h
new file mode 100644
index 0000000..eacb388
--- /dev/null
+++ b/src/modules/renpy/pickle.h
@@ -0,0 +1,210 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace pickle
+{
+ class value;
+
+ using value_ptr = std::unique_ptr;
+ using list = std::vector;
+ using dict = std::unordered_map;
+
+ class value
+ {
+ public:
+ enum class type
+ {
+ none,
+ bool_,
+ int64,
+ float64,
+ bytes,
+ string,
+ list,
+ dict,
+ tuple
+ };
+
+ private:
+ type type_;
+ std::variant<
+ std::monostate, // none
+ bool, // bool_
+ int64_t, // int64
+ double, // float64
+ std::string, // bytes/string
+ list, // list/tuple
+ dict // dict
+ > data_;
+
+ public:
+ explicit value(const type t) : type_(t)
+ {
+ }
+
+ static value_ptr none()
+ {
+ return std::make_unique(type::none);
+ }
+
+ static value_ptr boolean(bool val)
+ {
+ auto v = std::make_unique(type::bool_);
+ v->data_ = val;
+ return v;
+ }
+
+ static value_ptr int64(int64_t val)
+ {
+ auto v = std::make_unique(type::int64);
+ v->data_ = val;
+ return v;
+ }
+
+ static value_ptr float64(double val)
+ {
+ auto v = std::make_unique(type::float64);
+ v->data_ = val;
+ return v;
+ }
+
+ static value_ptr bytes(std::string val)
+ {
+ auto v = std::make_unique(type::bytes);
+ v->data_ = std::move(val);
+ return v;
+ }
+
+ static value_ptr string(std::string val)
+ {
+ auto v = std::make_unique(type::string);
+ v->data_ = std::move(val);
+ return v;
+ }
+
+ static value_ptr list(pickle::list val)
+ {
+ auto v = std::make_unique(type::list);
+ v->data_ = std::move(val);
+ return v;
+ }
+
+ static value_ptr tuple(pickle::list val)
+ {
+ auto v = std::make_unique(type::tuple);
+ v->data_ = std::move(val);
+ return v;
+ }
+
+ static value_ptr dict(pickle::dict val)
+ {
+ auto v = std::make_unique(type::dict);
+ v->data_ = std::move(val);
+ return v;
+ }
+
+ type get_type() const { return type_; }
+
+ bool as_bool() const
+ {
+ if (type_ != type::bool_) {
+ throw std::runtime_error("Value is not a bool");
+ }
+ return std::get(data_);
+ }
+
+ int64_t as_int64() const
+ {
+ if (type_ != type::int64) {
+ throw std::runtime_error("Value is not an int64");
+ }
+ return std::get(data_);
+ }
+
+ double as_float64() const
+ {
+ if (type_ != type::float64) {
+ throw std::runtime_error("Value is not a float64");
+ }
+ return std::get(data_);
+ }
+
+ const std::string &as_string() const
+ {
+ if (type_ != type::string && type_ != type::bytes) {
+ throw std::runtime_error("Value is not a string");
+ }
+ return std::get(data_);
+ }
+
+ const pickle::list &as_list() const
+ {
+ if (type_ != type::list) {
+ throw std::runtime_error("Value is not a list");
+ }
+ return std::get(data_);
+ }
+
+ const pickle::list &as_tuple() const
+ {
+ if (type_ != type::tuple) {
+ throw std::runtime_error("Value is not a tuple");
+ }
+ return std::get(data_);
+ }
+
+ const pickle::dict &as_dict() const
+ {
+ if (type_ != type::dict) {
+ throw std::runtime_error("Value is not a dict");
+ }
+ return std::get(data_);
+ }
+ };
+
+ class parser
+ {
+ private:
+ std::span data_;
+ size_t pos_ = 0;
+ std::vector stack_;
+ std::vector mark_stack_;
+ std::unordered_map memo_;
+
+ uint8_t read_byte();
+
+ uint16_t read_uint16_le();
+
+ uint32_t read_uint32_le();
+
+ uint64_t read_uint64_le();
+
+ std::string read_string(size_t length);
+
+ std::string read_line();
+
+ void push_mark();
+
+ list pop_to_mark();
+
+ value_ptr parse_value();
+
+ public:
+ explicit parser(const std::span data) : data_(data)
+ {
+ }
+
+ value_ptr parse();
+ };
+
+ value_ptr loads(std::span data);
+
+ value_ptr loads(const std::string &data);
+}
diff --git a/src/modules/renpy/python/_ref.h b/src/modules/renpy/python/_ref.h
deleted file mode 100644
index 343acdf..0000000
--- a/src/modules/renpy/python/_ref.h
+++ /dev/null
@@ -1,83 +0,0 @@
-#pragma once
-
-#include "python.h"
-
-#include
-#include
-
-namespace python
-{
- class ref : public object
- {
- public:
- explicit ref(PyObject *object)
- {
- if (object == nullptr) {
- throw std::invalid_argument("object is null");
- }
- object_ = object;
- }
-
- ~ref() override = default;
-
- [[nodiscard]] PyObject *get() const noexcept
- {
- return object_;
- }
-
- protected:
- PyObject *object_;
- };
-
- class weak_ref final : public ref
- {
- public:
- weak_ref() = delete;
-
- weak_ref(const weak_ref &) = delete;
-
- weak_ref(weak_ref &&) = delete;
-
- weak_ref &operator=(const weak_ref &) = delete;
-
- weak_ref &operator=(weak_ref &&) = delete;
-
- explicit weak_ref(PyObject *object) : ref(object)
- {
- }
-
- ~weak_ref() override
- {
- object_ = nullptr;
- }
- };
-
- class strong_ref final : public ref
- {
- public:
- strong_ref() = delete;
-
- strong_ref(const strong_ref &) = delete;
-
- strong_ref(strong_ref &&) = delete;
-
- strong_ref &operator=(const strong_ref &) = delete;
-
- strong_ref &operator=(strong_ref &&) = delete;
-
- explicit strong_ref(PyObject *object) : ref(object)
- {
- }
-
- ~strong_ref() override
- {
- Py_DECREF(object_);
- object_ = nullptr;
- }
- };
-
- inline PyObject *rawptr(const object &object)
- {
- return dynamic_cast(object).get();
- }
-}
diff --git a/src/modules/renpy/python/context.cpp b/src/modules/renpy/python/context.cpp
deleted file mode 100644
index e3ce7ac..0000000
--- a/src/modules/renpy/python/context.cpp
+++ /dev/null
@@ -1,77 +0,0 @@
-#include "python.h"
-#include "_ref.h"
-
-#include
-#include
-#include
-
-#include
-
-namespace python
-{
- context::context() noexcept
- {
- Py_Initialize();
- }
-
- context::~context()
- {
- Py_Finalize();
- }
-
- // ReSharper disable once CppMemberFunctionMayBeStatic
- std::unique_ptr