diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 6e691de18..c34e99bba 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Create & Deploy Docs - uses: DenverCoder1/doxygen-github-pages-action@v1.3.1 + uses: DenverCoder1/doxygen-github-pages-action@v2.0.0 with: github_token: ${{secrets.GITHUB_TOKEN}} branch: docs diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 000000000..9dd7538cd --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,102 @@ +name: Build Wheels +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + release: + type: boolean + required: true + default: false + description: 'Push the wheels to PyPI' + release: + types: [published] +jobs: + build_sdist: + name: Build SDist + runs-on: ubuntu-latest + defaults: + run: + working-directory: '${{github.workspace}}/lang/python' + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build SDist + run: | + pipx run build --sdist + + - name: Check Metadata + run: | + pipx run twine check dist/* + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: dist-sdist + path: ${{github.workspace}}/lang/python/dist/*.tar.gz + +# Linux build fails because the compiler's not new enough +# macOS build fails because it keeps trying to build a universal binary +# +# build_wheels: +# strategy: +# fail-fast: false +# matrix: +# os: [windows-latest, ubuntu-latest, macos-latest] +# name: Build Wheels For ${{matrix.os}} +# runs-on: ${{matrix.os}} +# steps: +# - name: Checkout Repository +# uses: actions/checkout@v4 +# with: +# submodules: true +# +# - name: Build Wheels +# uses: pypa/cibuildwheel@v2.19 +# with: +# package-dir: '${{github.workspace}}/lang/python' +# output-dir: '${{github.workspace}}/lang/python/wheelhouse' +# +# - name: Verify Clean Directory +# shell: bash +# run: | +# git diff --exit-code +# +# - name: Upload Wheels +# uses: actions/upload-artifact@v4 +# with: +# path: ${{github.workspace}}/lang/python/wheelhouse/*.whl +# name: dist-${{matrix.os}} + + merge_wheels: + name: Merge Wheels + runs-on: ubuntu-latest + needs: + - build_sdist +# - build_wheels + steps: + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: dist + pattern: dist-* + + upload_release: + name: Upload a Release + if: (github.event_name == 'release' && github.event.action == 'published') || (github.event_name == 'workflow_dispatch' && inputs.release) + needs: [merge_wheels] + runs-on: ubuntu-latest + steps: + - uses: actions/setup-python@v5 + - uses: actions/download-artifact@v4 + with: + path: dist + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{secrets.PYPI_TOKEN}} diff --git a/.gitignore b/.gitignore index 0c0169e33..148b32c84 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,11 @@ out/ docs/html/ test/res/ test/Helpers.h + +# Python +.mypy_cache/ +__pycache__/ +*.pyd +*.pyi +*.typed +*.whl diff --git a/CMakeLists.txt b/CMakeLists.txt index 0106c49b1..ddb12ba6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.25 FATAL_ERROR) # Set defaults before project call if(PROJECT_IS_TOP_LEVEL) - set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") + set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE INTERNAL "" FORCE) endif() @@ -101,15 +101,16 @@ endif() # Python bindings, part 1 if(SOURCEPP_BUILD_PYTHON_WRAPPERS) set(SOURCEPP_PYTHON_NAME "${PROJECT_NAME}_python") - find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) + find_package(Python REQUIRED + COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule) FetchContent_Declare( - pybind11 - GIT_REPOSITORY "https://github.com/pybind/pybind11.git" - GIT_TAG "v2.13.6") - FetchContent_MakeAvailable(pybind11) + nanobind + GIT_REPOSITORY "https://github.com/wjakob/nanobind.git" + GIT_TAG "origin/master") + FetchContent_MakeAvailable(nanobind) set(${SOURCEPP_PYTHON_NAME}_SOURCES "") set(${SOURCEPP_PYTHON_NAME}_DEFINES "") - list(APPEND ${SOURCEPP_PYTHON_NAME}_DEPS pybind11::headers) endif() @@ -143,7 +144,6 @@ endif() # Benchmarks if(SOURCEPP_BUILD_BENCHMARKS) set(SOURCEPP_BENCH_NAME "${PROJECT_NAME}_bench") - include(FetchContent) FetchContent_Declare( benchmark GIT_REPOSITORY https://github.com/google/benchmark.git @@ -167,7 +167,7 @@ add_sourcepp_library(steampp C PYTHON ) # sourcepp::steampp add_sourcepp_library(toolpp ) # sourcepp::toolpp add_sourcepp_library(vcryptpp C CSHARP PYTHON ) # sourcepp::vcryptpp add_sourcepp_library(vpkpp C CSHARP NO_TEST ) # sourcepp::vpkpp -add_sourcepp_library(vtfpp BENCH) # sourcepp::vtfpp +add_sourcepp_library(vtfpp PYTHON BENCH) # sourcepp::vtfpp # Tests, part 2 @@ -182,16 +182,52 @@ endif() # Python bindings, part 2 if(SOURCEPP_BUILD_PYTHON_WRAPPERS) - python_add_library(${SOURCEPP_PYTHON_NAME} MODULE "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/src/sourcepp.cpp" ${${SOURCEPP_PYTHON_NAME}_SOURCES} WITH_SOABI) - set_target_properties(${SOURCEPP_PYTHON_NAME} PROPERTIES PREFIX "_" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/dist/sourcepp") + nanobind_add_module(${SOURCEPP_PYTHON_NAME} NB_STATIC STABLE_ABI LTO + "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/src/sourcepp.cpp" + ${${SOURCEPP_PYTHON_NAME}_SOURCES}) + set_target_properties(${SOURCEPP_PYTHON_NAME} PROPERTIES + OUTPUT_NAME "_${PROJECT_NAME}_impl" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/src/sourcepp" + LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/src/sourcepp" + LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/src/sourcepp") target_compile_definitions(${SOURCEPP_PYTHON_NAME} PRIVATE ${${SOURCEPP_PYTHON_NAME}_DEFINES}) target_link_libraries(${SOURCEPP_PYTHON_NAME} PRIVATE ${${SOURCEPP_PYTHON_NAME}_DEPS}) + + add_custom_target(${SOURCEPP_PYTHON_NAME}_all) + add_dependencies(${SOURCEPP_PYTHON_NAME}_all ${SOURCEPP_PYTHON_NAME}) + + # We need to manually write out each module :( + set(${SOURCEPP_PYTHON_NAME}_MODULES + "sourcepp" + "sourcepp._sourcepp_impl" + "sourcepp._sourcepp_impl.gamepp" + "sourcepp._sourcepp_impl.sourcepp" + "sourcepp._sourcepp_impl.sourcepp.math" + "sourcepp._sourcepp_impl.steampp" + "sourcepp._sourcepp_impl.vcryptpp" + "sourcepp._sourcepp_impl.vcryptpp.VFONT" + "sourcepp._sourcepp_impl.vcryptpp.VICE" + "sourcepp._sourcepp_impl.vtfpp" + "sourcepp._sourcepp_impl.vtfpp.ImageFormatDetails" + "sourcepp._sourcepp_impl.vtfpp.ImageDimensions" + "sourcepp._sourcepp_impl.vtfpp.ImageConversion") + foreach(MODULE ${${SOURCEPP_PYTHON_NAME}_MODULES}) + string(REPLACE "." "/" MODULE_DIR "${MODULE}") + string(REPLACE "." "_" MODULE_NAME_NORMALIZED "${MODULE}") + set(MODULE_NAME_NORMALIZED "${MODULE_NAME_NORMALIZED}_stub") + nanobind_add_stub("${SOURCEPP_PYTHON_NAME}_stub_${MODULE_NAME_NORMALIZED}" + DEPENDS ${SOURCEPP_PYTHON_NAME} + MODULE "${MODULE}" + OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/src/${MODULE_DIR}.pyi" + PYTHON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/lang/python/src") + add_dependencies(${SOURCEPP_PYTHON_NAME}_all ${SOURCEPP_PYTHON_NAME}_stub_${MODULE_NAME_NORMALIZED}) + endforeach() endif() # Print options print_options(OPTIONS USE_BSPPP USE_DMXPP USE_GAMEPP USE_KVPP USE_MDLPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VPKPP USE_VTFPP - BUILD_BENCHMARKS BUILD_C_WRAPPERS BUILD_PYTHON_WRAPPERS BUILD_WITH_OPENCL BUILD_WITH_TBB BUILD_WITH_THREADS BUILD_TESTS BUILD_WIN7_COMPAT + BUILD_BENCHMARKS BUILD_C_WRAPPERS BUILD_CSHARP_WRAPPERS BUILD_PYTHON_WRAPPERS BUILD_WITH_OPENCL BUILD_WITH_TBB BUILD_WITH_THREADS BUILD_TESTS BUILD_WIN7_COMPAT LINK_STATIC_MSVC_RUNTIME VPKPP_SUPPORT_VPK_V54) diff --git a/README.md b/README.md index 1544e8e6a..6fc49a144 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one - bsppp + bsppp* BSP v17-27 ✅ ✅ @@ -30,7 +30,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one - dmxpp + dmxpp* DMX Binary v1-5 ✅ ❌ @@ -53,15 +53,15 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one kvpp - KeyValues v1* + KeyValues Text v1 ✅ ✅ - mdlpp - MDL v44-49 + mdlpp* + MDL v44-49 ✅ ❌ @@ -217,7 +217,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one BMP ✅ ✅ - + Python @@ -290,9 +290,16 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one -(\*) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [VDF](https://developer.valvesoftware.com/wiki/VDF), [VMT](https://developer.valvesoftware.com/wiki/VMT), and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). +(\*) These libraries are incomplete and still in development. Their interfaces are unstable and will likely change in the future. +Libraries not starred should be considered stable, and their existing interfaces will not change much if at all. Note that wrappers +only exist for stable libraries. -(†) The MDL parser is not complete. It is usable in its current state, but it does not currently parse more complex components like animations. This parser is still in development. +(†) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [VMT](https://developer.valvesoftware.com/wiki/VMT) and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). + +## Wrappers + +Wrappers for libraries considered complete exist for C, C#, and/or Python, depending on the library. The Python wrappers can be +found on PyPI in the `sourcepp` package. ## Special Thanks diff --git a/THIRDPARTY_LEGAL_NOTICES.txt b/THIRDPARTY_LEGAL_NOTICES.txt index 44cfc395a..a6d198797 100644 --- a/THIRDPARTY_LEGAL_NOTICES.txt +++ b/THIRDPARTY_LEGAL_NOTICES.txt @@ -222,6 +222,37 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution. +--------------- nanobind --------------- + +Copyright (c) 2022 Wenzel Jakob . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + --------------- stb --------------- Copyright (c) 2017 Sean Barrett diff --git a/docs/index.md b/docs/index.md index 01fe38c94..1217602e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,14 +22,14 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one Wrappers - bsppp + bsppp* BSP v17-27 ✅ ✅ - dmxpp + dmxpp* DMX Binary v1-5 ✅ ❌ @@ -49,14 +49,14 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one kvpp - KeyValues v1* + KeyValues Text v1 ✅ ✅ - mdlpp - MDL v44-49 + mdlpp* + MDL v44-49 ✅ ❌ @@ -190,7 +190,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one BMP ✅ ✅ - + Python EXR v1 @@ -253,9 +253,16 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one \endhtmlonly -(\*) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [VDF](https://developer.valvesoftware.com/wiki/VDF), [VMT](https://developer.valvesoftware.com/wiki/VMT), and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). +(\*) These libraries are incomplete and still in development. Their interfaces are unstable and will likely change in the future. +Libraries not starred should be considered stable, and their existing interfaces will not change much if at all. Note that wrappers +only exist for stable libraries. -(†) The MDL parser is not complete. It is usable in its current state, but it does not currently parse more complex components like animations. This parser is still in development. +(†) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [VMT](https://developer.valvesoftware.com/wiki/VMT) and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). + +## Wrappers + +Wrappers for libraries considered complete exist for C, C#, and/or Python, depending on the library. The Python wrappers can be +found on PyPI in the `sourcepp` package. ## Special Thanks diff --git a/ext/_ext.cmake b/ext/_ext.cmake index 4c8f80d55..530c62412 100644 --- a/ext/_ext.cmake +++ b/ext/_ext.cmake @@ -42,11 +42,11 @@ endif() # minizip-ng (guard this behind vpkpp because this is a big dependency) -if(SOURCEPP_USE_VPKPP AND NOT TARGET MINIZIP::minizip) +if((SOURCEPP_USE_VPKPP OR SOURCEPP_USE_VTFPP) AND NOT TARGET MINIZIP::minizip) set(MZ_COMPAT OFF CACHE INTERNAL "") # todo: guard liblzma/xz force-enable behind BSP compression option set(MZ_LZMA ON CACHE INTERNAL "" FORCE) - if(SOURCEPP_VPKPP_SUPPORT_VPK_V54) + if(SOURCEPP_USE_VTFPP OR SOURCEPP_VPKPP_SUPPORT_VPK_V54) set(MZ_ZSTD ON CACHE INTERNAL "" FORCE) endif() set(MZ_FETCH_LIBS ON CACHE INTERNAL "" FORCE) diff --git a/ext/compressonator/CMakeLists.txt b/ext/compressonator/CMakeLists.txt index a4757f151..22cc64f15 100644 --- a/ext/compressonator/CMakeLists.txt +++ b/ext/compressonator/CMakeLists.txt @@ -1,4 +1,6 @@ -set(COMPRESSONATOR_DIR ${CMAKE_CURRENT_LIST_DIR} CACHE INTERNAL "") +include_guard(GLOBAL) + +set(COMPRESSONATOR_DIR "${CMAKE_CURRENT_LIST_DIR}" CACHE INTERNAL "") function(target_link_compressonator TARGET) if(WIN32) @@ -20,8 +22,8 @@ function(target_link_compressonator TARGET) endif() elseif(APPLE) target_link_libraries(${TARGET} PRIVATE - "${COMPRESSONATOR_DIR}/lib/macOS/libCompressonator$<$:d>.a" - "${COMPRESSONATOR_DIR}/lib/macOS/libCMP_Core$<$:d>.a") + "${COMPRESSONATOR_DIR}/lib/macOS_arm64/libCompressonator$<$:d>.a" + "${COMPRESSONATOR_DIR}/lib/macOS_arm64/libCMP_Core$<$:d>.a") elseif(UNIX) target_link_libraries(${TARGET} PRIVATE "${COMPRESSONATOR_DIR}/lib/linux_x86_64/libCompressonator$<$:d>.a" diff --git a/ext/compressonator/lib/macOS/libCMP_Core.a b/ext/compressonator/lib/macOS_arm64/libCMP_Core.a similarity index 98% rename from ext/compressonator/lib/macOS/libCMP_Core.a rename to ext/compressonator/lib/macOS_arm64/libCMP_Core.a index b334e5f1f..69947a390 100644 Binary files a/ext/compressonator/lib/macOS/libCMP_Core.a and b/ext/compressonator/lib/macOS_arm64/libCMP_Core.a differ diff --git a/ext/compressonator/lib/macOS/libCMP_Cored.a b/ext/compressonator/lib/macOS_arm64/libCMP_Cored.a similarity index 51% rename from ext/compressonator/lib/macOS/libCMP_Cored.a rename to ext/compressonator/lib/macOS_arm64/libCMP_Cored.a index 523764d44..68c2458ee 100644 Binary files a/ext/compressonator/lib/macOS/libCMP_Cored.a and b/ext/compressonator/lib/macOS_arm64/libCMP_Cored.a differ diff --git a/ext/compressonator/lib/macOS/libCompressonator.a b/ext/compressonator/lib/macOS_arm64/libCompressonator.a similarity index 89% rename from ext/compressonator/lib/macOS/libCompressonator.a rename to ext/compressonator/lib/macOS_arm64/libCompressonator.a index 6ac269e9e..2fa0c0702 100644 Binary files a/ext/compressonator/lib/macOS/libCompressonator.a and b/ext/compressonator/lib/macOS_arm64/libCompressonator.a differ diff --git a/ext/compressonator/lib/macOS/libCompressonatord.a b/ext/compressonator/lib/macOS_arm64/libCompressonatord.a similarity index 61% rename from ext/compressonator/lib/macOS/libCompressonatord.a rename to ext/compressonator/lib/macOS_arm64/libCompressonatord.a index 5c5e41bf9..203957a5b 100644 Binary files a/ext/compressonator/lib/macOS/libCompressonatord.a and b/ext/compressonator/lib/macOS_arm64/libCompressonatord.a differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MD.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MD.lib index 0efe277be..9efa4b405 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MD.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MD.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX.lib index b578fb151..4fc8a1a28 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512.lib index c08519b47..25f22e752 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512d.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512d.lib index 97202b42f..e10065294 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512d.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVX512d.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVXd.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVXd.lib index 99a1b33cc..482bcc8e5 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVXd.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_AVXd.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSE.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSE.lib index 014aebdd5..de5062d49 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSE.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSE.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSEd.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSEd.lib index 61d410a9a..f3c42d72f 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSEd.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MD_SSEd.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MDd.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MDd.lib index 95c3ef771..0d6f7aa66 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MDd.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MDd.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MT.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MT.lib index 62660ca70..7a56db967 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MT.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MT.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX.lib index 87defa8f6..f0b4cfc27 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512.lib index 7a8681eff..3150b8ed9 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512d.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512d.lib index 41ef859f2..af3fbcc5e 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512d.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVX512d.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVXd.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVXd.lib index 9db8d8d7c..932ae1f1b 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVXd.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_AVXd.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSE.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSE.lib index 2bf68a0ee..05f719b66 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSE.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSE.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSEd.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSEd.lib index 538cee480..b720f1c7a 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSEd.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MT_SSEd.lib differ diff --git a/ext/compressonator/lib/win_x86_64/CMP_Core_MTd.lib b/ext/compressonator/lib/win_x86_64/CMP_Core_MTd.lib index 1a21c9d03..5c19763da 100644 Binary files a/ext/compressonator/lib/win_x86_64/CMP_Core_MTd.lib and b/ext/compressonator/lib/win_x86_64/CMP_Core_MTd.lib differ diff --git a/ext/compressonator/lib/win_x86_64/Compressonator_MD.lib b/ext/compressonator/lib/win_x86_64/Compressonator_MD.lib index 1c3f6b54c..f6cb96cda 100644 Binary files a/ext/compressonator/lib/win_x86_64/Compressonator_MD.lib and b/ext/compressonator/lib/win_x86_64/Compressonator_MD.lib differ diff --git a/ext/compressonator/lib/win_x86_64/Compressonator_MDd.lib b/ext/compressonator/lib/win_x86_64/Compressonator_MDd.lib index 46aef8db7..79d3aad1c 100644 Binary files a/ext/compressonator/lib/win_x86_64/Compressonator_MDd.lib and b/ext/compressonator/lib/win_x86_64/Compressonator_MDd.lib differ diff --git a/ext/compressonator/lib/win_x86_64/Compressonator_MT.lib b/ext/compressonator/lib/win_x86_64/Compressonator_MT.lib index 49197349a..9f7df3aae 100644 Binary files a/ext/compressonator/lib/win_x86_64/Compressonator_MT.lib and b/ext/compressonator/lib/win_x86_64/Compressonator_MT.lib differ diff --git a/ext/compressonator/lib/win_x86_64/Compressonator_MTd.lib b/ext/compressonator/lib/win_x86_64/Compressonator_MTd.lib index d863c69f8..2b3d81cc3 100644 Binary files a/ext/compressonator/lib/win_x86_64/Compressonator_MTd.lib and b/ext/compressonator/lib/win_x86_64/Compressonator_MTd.lib differ diff --git a/include/vtfpp/PPL.h b/include/vtfpp/PPL.h index 6fa1a1541..18a8329c4 100644 --- a/include/vtfpp/PPL.h +++ b/include/vtfpp/PPL.h @@ -53,7 +53,7 @@ class PPL { bool setImage(std::span imageData, ImageFormat format_, uint32_t width, uint32_t height, uint32_t resizedWidth, uint32_t resizedHeight, uint32_t lod = 0, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR); - bool setImage(const std::string& imagePath, uint32_t lod); + bool setImage(const std::string& imagePath, uint32_t lod = 0); bool setImage(const std::string& imagePath, uint32_t resizedWidth, uint32_t resizedHeight, uint32_t lod = 0, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR); diff --git a/include/vtfpp/VTF.h b/include/vtfpp/VTF.h index a8d9bf556..54eeddb89 100644 --- a/include/vtfpp/VTF.h +++ b/include/vtfpp/VTF.h @@ -18,6 +18,11 @@ namespace vtfpp { constexpr uint32_t VTF_SIGNATURE = sourcepp::parser::binary::makeFourCC("VTF\0"); +enum class CompressionMethod : int16_t { + DEFLATE = 8, + ZSTD = 93, +}; + struct Resource { enum Type : uint32_t { TYPE_UNKNOWN = 0, // Unknown @@ -30,12 +35,7 @@ struct Resource { TYPE_KEYVALUES_DATA = sourcepp::parser::binary::makeFourCC("KVD\0"), TYPE_AUX_COMPRESSION = sourcepp::parser::binary::makeFourCC("AXC\0"), }; - static constexpr std::array TYPE_ARRAY_ORDER{ - // These don't really matter - Resource::TYPE_CRC, Resource::TYPE_EXTENDED_FLAGS, Resource::TYPE_LOD_CONTROL_INFO, Resource::TYPE_KEYVALUES_DATA, Resource::TYPE_PARTICLE_SHEET_DATA, - // These matter - Resource::TYPE_THUMBNAIL_DATA, Resource::TYPE_AUX_COMPRESSION, Resource::TYPE_IMAGE_DATA, - }; + static const std::array& getOrder(); enum Flags : uint8_t { FLAG_NONE = 0, @@ -71,8 +71,16 @@ struct Resource { return std::get(this->convertData()); } - [[nodiscard]] int32_t getDataAsAuxCompressionLevel() const { - return static_cast(std::get>(this->convertData())[1]); + [[nodiscard]] int16_t getDataAsAuxCompressionLevel() const { + return static_cast(std::get>(this->convertData())[1] & 0xffff); + } + + [[nodiscard]] CompressionMethod getDataAsAuxCompressionMethod() const { + auto method = static_cast((std::get>(this->convertData())[1] & 0xffff0000) >> 16); + if (method <= 0) { + return CompressionMethod::DEFLATE; + } + return static_cast(method); } [[nodiscard]] uint32_t getDataAsAuxCompressionLength(uint8_t mip, uint8_t mipCount, uint16_t frame, uint16_t frameCount, uint16_t face, uint16_t faceCount) const { @@ -159,7 +167,8 @@ class VTF { bool createMips = true; bool createThumbnail = true; bool createReflectivity = true; - uint8_t compressionLevel = 6; + int16_t compressionLevel = -1; + CompressionMethod compressionMethod = CompressionMethod::ZSTD; float bumpMapScale = 1.f; }; @@ -217,6 +226,10 @@ class VTF { void setImageResizeMethods(ImageConversion::ResizeMethod imageWidthResizeMethod_, ImageConversion::ResizeMethod imageHeightResizeMethod_); + void setImageWidthResizeMethod(ImageConversion::ResizeMethod imageWidthResizeMethod_); + + void setImageHeightResizeMethod(ImageConversion::ResizeMethod imageHeightResizeMethod_); + [[nodiscard]] uint16_t getWidth(uint8_t mip = 0) const; [[nodiscard]] uint16_t getHeight(uint8_t mip = 0) const; @@ -299,13 +312,17 @@ class VTF { void removeExtendedFlagsResource(); - void setKeyValuesData(const std::string& value); + void setKeyValuesDataResource(const std::string& value); + + void removeKeyValuesDataResource(); - void removeKeyValuesData(); + [[nodiscard]] int16_t getCompressionLevel() const; - [[nodiscard]] uint8_t getCompressionLevel() const; + void setCompressionLevel(int16_t newCompressionLevel); - void setCompressionLevel(uint8_t newCompressionLevel); + [[nodiscard]] CompressionMethod getCompressionMethod() const; + + void setCompressionMethod(CompressionMethod newCompressionMethod); [[nodiscard]] bool hasImageData() const; @@ -333,6 +350,8 @@ class VTF { [[nodiscard]] std::vector getThumbnailDataAsRGBA8888() const; + void setThumbnail(std::span imageData_, ImageFormat format_, uint16_t width_, uint16_t height_); + void computeThumbnail(ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR); void removeThumbnail(); @@ -396,7 +415,8 @@ class VTF { //uint8_t _padding3[4]; // These aren't in the header, these are for VTF modification - uint8_t compressionLevel = 0; + int16_t compressionLevel = 0; + CompressionMethod compressionMethod = CompressionMethod::ZSTD; ImageConversion::ResizeMethod imageWidthResizeMethod = ImageConversion::ResizeMethod::POWER_OF_TWO_BIGGER; ImageConversion::ResizeMethod imageHeightResizeMethod = ImageConversion::ResizeMethod::POWER_OF_TWO_BIGGER; }; diff --git a/lang/python/.gitignore b/lang/python/.gitignore index 0c986d628..d88da90dd 100644 --- a/lang/python/.gitignore +++ b/lang/python/.gitignore @@ -1,4 +1,8 @@ -# Build Files -__pycache__/ -dist/sourcepp/* -!dist/sourcepp/__init__.py +# Keep most Python-specific stuff in the root .gitignore +# because scikit-build-core will use it to ignore everything +# you want to ship!!!!!!!! This took like an hour to figure +# out, thanks folks :D + +# Anyway + +wheelhouse/ diff --git a/lang/python/CMakeLists.txt b/lang/python/CMakeLists.txt new file mode 100644 index 000000000..069cb3677 --- /dev/null +++ b/lang/python/CMakeLists.txt @@ -0,0 +1,39 @@ +# Load this to build the sourcepp Python package + +cmake_minimum_required(VERSION 3.25 FATAL_ERROR) +set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE INTERNAL "" FORCE) +project(sourcepp_python) + +if (NOT SKBUILD) + message(WARNING "\ +This CMake file is meant to be executed using 'scikit-build-core'. +Running it directly will almost certainly not produce the desired +result. If you are a user trying to install this package, use the +command below, which will install all necessary build dependencies, +compile the package in an isolated environment, and then install it. +===================================================================== + $ pip install . +===================================================================== +If you are a software developer, and this is your own package, then +it is usually much more efficient to install the build dependencies +in your environment once and use the following command that avoids +a costly creation of a new virtual environment at every compilation: +===================================================================== + $ pip install nanobind scikit-build-core[pyproject] + $ pip install --no-build-isolation -ve . +===================================================================== +You may optionally add -Ceditable.rebuild=true to auto-rebuild when +the package is imported. Otherwise, you need to rerun the above +after editing C++ files.") +endif() + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# As weird as this looks, this is necessary for sdist wheel +include(FetchContent) +FetchContent_Declare( + sourcepp + GIT_REPOSITORY "https://github.com/craftablescience/sourcepp.git" + GIT_TAG "origin/main") +set(SOURCEPP_BUILD_PYTHON_WRAPPERS ON CACHE INTERNAL "" FORCE) +FetchContent_MakeAvailable(sourcepp) diff --git a/lang/python/dist/sourcepp/__init__.py b/lang/python/dist/sourcepp/__init__.py deleted file mode 100644 index d8f3097c9..000000000 --- a/lang/python/dist/sourcepp/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import annotations - -from ._sourcepp_python import __author__, __doc__, __version__, gamepp, sourcepp, steampp, vcryptpp - -__all__ = ['__author__', '__doc__', '__version__', 'gamepp', 'sourcepp', 'steampp', 'vcryptpp'] diff --git a/lang/python/pyproject.toml b/lang/python/pyproject.toml new file mode 100644 index 000000000..e086660d6 --- /dev/null +++ b/lang/python/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["scikit-build-core >=0.10.7", "nanobind >=1.3.2"] +build-backend = "scikit_build_core.build" + + +[project] +name = "sourcepp" +version = "2024.11.5dev1" # change __init__.py when this is updated +authors = [{ name = "craftablescience", email = "lauralewisdev@gmail.com" }] +maintainers = [{ name = "craftablescience", email = "lauralewisdev@gmail.com" }] +description = "Several modern C++20 libraries for sanely parsing Valve formats." +readme = "../../README.md" +requires-python = ">=3.8" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.urls] +"homepage" = "https://github.com/craftablescience/sourcepp" +"repository" = "https://github.com/craftablescience/sourcepp" +"issue tracker" = "https://github.com/craftablescience/sourcepp/issues" +"funding" = "https://ko-fi.com/craftablescience" + +[tool.scikit-build] +minimum-version = "build-system.requires" +build-dir = "build/{wheel_tag}" +build.targets = ["sourcepp_python_all"] +wheel.py-api = "cp312" +wheel.license-files = ["../../LICENSE", "../../THIRDPARTY_LEGAL_NOTICES.txt"] +build.verbose = true +logging.level = "INFO" + + +[tool.cibuildwheel] +archs = ["auto64"] +build-verbosity = 1 +#test-command = "pytest {project}/test" +#test-requires = "pytest" +#test-skip="cp38-macosx_*:arm64" + +[tool.cibuildwheel.macos] +archs = ["arm64"] + +[tool.cibuildwheel.macos.environment] +MACOSX_DEPLOYMENT_TARGET = "14.7" diff --git a/lang/python/src/gamepp.h b/lang/python/src/gamepp.h index 1d390f20e..8ac53f9a7 100644 --- a/lang/python/src/gamepp.h +++ b/lang/python/src/gamepp.h @@ -1,9 +1,11 @@ #pragma once -#include -#include +#include +#include +#include +#include -namespace py = pybind11; +namespace py = nanobind; #include @@ -14,16 +16,16 @@ inline void register_python(py::module_& m) { using namespace gamepp; py::class_(gamepp, "GameInstance") - .def_static("find", &GameInstance::find, py::arg("window_name_override") = "") - .def_property_readonly("window_title", &GameInstance::getWindowTitle) - .def_property_readonly("window_pos", &GameInstance::getWindowPos) - .def_property_readonly("window_size", &GameInstance::getWindowSize) - .def("command", &GameInstance::command) - .def("input_begin", &GameInstance::inputBegin) - .def("input_end", &GameInstance::inputEnd) - .def("input_once", &GameInstance::inputOnce) - .def("input_hold", &GameInstance::inputHold) - .def("wait", &GameInstance::wait); + .def_static("find", &GameInstance::find, py::arg("window_name_override") = "") + .def_prop_ro("window_title", &GameInstance::getWindowTitle) + .def_prop_ro("window_pos", &GameInstance::getWindowPos) + .def_prop_ro("window_size", &GameInstance::getWindowSize) + .def("command", &GameInstance::command, py::arg("command")) + .def("input_begin", &GameInstance::inputBegin, py::arg("input")) + .def("input_end", &GameInstance::inputEnd, py::arg("input")) + .def("input_once", &GameInstance::inputOnce, py::arg("input")) + .def("input_hold", &GameInstance::inputHold, py::arg("input"), py::arg("sec")) + .def("wait", &GameInstance::wait, py::arg("sec")); } } // namespace gamepp diff --git a/lang/python/src/sourcepp.cpp b/lang/python/src/sourcepp.cpp index 353151646..ff7935af8 100644 --- a/lang/python/src/sourcepp.cpp +++ b/lang/python/src/sourcepp.cpp @@ -12,11 +12,12 @@ #include "vcryptpp.h" #endif -PYBIND11_MODULE(_sourcepp_python, m) { - m.doc() = "SourcePP: A Python wrapper around several modern C++20 libraries for sanely parsing Valve's formats."; +#ifdef VTFPP +#include "vtfpp.h" +#endif - m.attr("__author__") = "craftablescience"; - m.attr("__version__") = "dev"; +NB_MODULE(_sourcepp_impl, m) { + m.doc() = "SourcePP: A Python wrapper around several modern C++20 libraries for sanely parsing Valve's formats."; sourcepp::register_python(m); @@ -37,4 +38,10 @@ PYBIND11_MODULE(_sourcepp_python, m) { #else m.def_submodule("vcryptpp"); #endif + +#ifdef VTFPP + vtfpp::register_python(m); +#else + m.def_submodule("vtfpp"); +#endif } diff --git a/lang/python/src/sourcepp.h b/lang/python/src/sourcepp.h index b2908ae0e..079c37ae0 100644 --- a/lang/python/src/sourcepp.h +++ b/lang/python/src/sourcepp.h @@ -2,11 +2,10 @@ #include -#include -#include -#include +#include +#include -namespace py = pybind11; +namespace py = nanobind; #include @@ -21,10 +20,7 @@ inline void register_python(py::module_& m) { using namespace math; const auto registerVecType = [&math](std::string_view name) { - py::class_(math, name.data(), pybind11::buffer_protocol()) - .def_buffer([](V& v) -> py::buffer_info { - return {v.data(), static_cast(v.size())}; - }) + py::class_(math, name.data()) .def("__len__", &V::size) .def("__setitem__", [](V& self, uint8_t index, typename V::value_type val) { self[index] = val; }) .def("__getitem__", [](V& self, uint8_t index) { return self[index]; }) diff --git a/lang/python/src/sourcepp/__init__.py b/lang/python/src/sourcepp/__init__.py new file mode 100644 index 000000000..2cac8b428 --- /dev/null +++ b/lang/python/src/sourcepp/__init__.py @@ -0,0 +1,5 @@ +from ._sourcepp_impl import __doc__, gamepp, sourcepp, steampp, vcryptpp, vtfpp + +__author__ = "craftablescience" +__version__ = "2024.11.5dev1" # change pyproject.toml version when this is updated +__all__ = ['__author__', '__doc__', '__version__', 'gamepp', 'sourcepp', 'steampp', 'vcryptpp', 'vtfpp'] diff --git a/lang/python/src/steampp.h b/lang/python/src/steampp.h index 12497d7aa..b1ff5d7fa 100644 --- a/lang/python/src/steampp.h +++ b/lang/python/src/steampp.h @@ -1,9 +1,11 @@ #pragma once -#include -#include +#include +#include +#include +#include -namespace py = pybind11; +namespace py = nanobind; #include @@ -14,21 +16,21 @@ inline void register_python(py::module_& m) { using namespace steampp; py::class_(steampp, "Steam") - .def(py::init<>()) - .def_property_readonly("install_dir", &Steam::getInstallDir) - .def_property_readonly("library_dirs", &Steam::getLibraryDirs) - .def_property_readonly("sourcemod_dir", &Steam::getSourceModDir) - .def_property_readonly("installed_apps", &Steam::getInstalledApps) - .def("is_app_installed", &Steam::isAppInstalled) - .def("get_app_name", &Steam::getAppName) - .def("get_app_install_dir", &Steam::getAppInstallDir) - .def("get_app_icon_path", &Steam::getAppIconPath) - .def("get_app_logo_path", &Steam::getAppLogoPath) - .def("get_app_box_art_path", &Steam::getAppBoxArtPath) - .def("get_app_store_art_path", &Steam::getAppStoreArtPath) - .def("is_app_using_source_engine", &Steam::isAppUsingSourceEngine) - .def("is_app_using_source_2_engine", &Steam::isAppUsingSource2Engine) - .def_property_readonly("valid", &Steam::operator bool); + .def(py::init<>()) + .def_prop_ro("install_dir", &Steam::getInstallDir) + .def_prop_ro("library_dirs", &Steam::getLibraryDirs) + .def_prop_ro("sourcemod_dir", &Steam::getSourceModDir) + .def_prop_ro("installed_apps", &Steam::getInstalledApps) + .def("is_app_installed", &Steam::isAppInstalled, py::arg("appID")) + .def("get_app_name", &Steam::getAppName, py::arg("appID")) + .def("get_app_install_dir", &Steam::getAppInstallDir, py::arg("appID")) + .def("get_app_icon_path", &Steam::getAppIconPath, py::arg("appID")) + .def("get_app_logo_path", &Steam::getAppLogoPath, py::arg("appID")) + .def("get_app_box_art_path", &Steam::getAppBoxArtPath, py::arg("appID")) + .def("get_app_store_art_path", &Steam::getAppStoreArtPath, py::arg("appID")) + .def("is_app_using_source_engine", &Steam::isAppUsingSourceEngine, py::arg("appID")) + .def("is_app_using_source_2_engine", &Steam::isAppUsingSource2Engine, py::arg("appID")) + .def_prop_ro("__bool__", &Steam::operator bool, py::is_operator()); } } // namespace steampp diff --git a/lang/python/src/vcryptpp.h b/lang/python/src/vcryptpp.h index 636a80aea..1ae642783 100644 --- a/lang/python/src/vcryptpp.h +++ b/lang/python/src/vcryptpp.h @@ -1,9 +1,9 @@ #pragma once -#include -#include +#include +#include -namespace py = pybind11; +namespace py = nanobind; #include @@ -21,10 +21,9 @@ inline void register_python(py::module_& m) { VFONT.attr("MAGIC") = MAGIC; - VFONT.def("decrypt_bytes", [](const py::bytes& data) -> py::bytes { - const std::string_view dataView{data}; - const auto d = decrypt({reinterpret_cast(dataView.data()), dataView.size()}); - return {reinterpret_cast(d.data()), d.size()}; + VFONT.def("decrypt_bytes", [](const py::bytes& data) { + const auto d = decrypt({reinterpret_cast(data.data()), data.size()}); + return py::bytes{d.data(), d.size()}; }, py::arg("data")); } @@ -36,40 +35,39 @@ inline void register_python(py::module_& m) { auto KnownCodes = VICE.def_submodule("KnownCodes"); using namespace KnownCodes; - KnownCodes.attr("DEFAULT") = DEFAULT; - KnownCodes.attr("CONTAGION_WEAPONS") = CONTAGION_WEAPONS; - KnownCodes.attr("CONTAGION_SCRIPTS") = CONTAGION_SCRIPTS; - KnownCodes.attr("COUNTER_STRIKE_SOURCE") = COUNTER_STRIKE_SOURCE; + KnownCodes.attr("DEFAULT") = DEFAULT; + KnownCodes.attr("CONTAGION_WEAPONS") = CONTAGION_WEAPONS; + KnownCodes.attr("CONTAGION_SCRIPTS") = CONTAGION_SCRIPTS; + KnownCodes.attr("COUNTER_STRIKE_SOURCE") = COUNTER_STRIKE_SOURCE; KnownCodes.attr("COUNTER_STRIKE_GLOBAL_OFFENSIVE") = COUNTER_STRIKE_GLOBAL_OFFENSIVE; - KnownCodes.attr("COUNTER_STRIKE_2") = COUNTER_STRIKE_2; - KnownCodes.attr("COUNTER_STRIKE_PROMOD") = COUNTER_STRIKE_PROMOD; - KnownCodes.attr("DAY_OF_DEFEAT_SOURCE") = DAY_OF_DEFEAT_SOURCE; - KnownCodes.attr("DYSTOPIA_1_2") = DYSTOPIA_1_2; - KnownCodes.attr("DYSTOPIA_1_3") = DYSTOPIA_1_3; - KnownCodes.attr("GOLDEN_EYE_SOURCE") = GOLDEN_EYE_SOURCE; - KnownCodes.attr("HALF_LIFE_2_CTF") = HALF_LIFE_2_CTF; - KnownCodes.attr("HALF_LIFE_2_DM") = HALF_LIFE_2_DM; - KnownCodes.attr("INSURGENCY") = INSURGENCY; - KnownCodes.attr("LEFT_4_DEAD_2") = LEFT_4_DEAD_2; - KnownCodes.attr("NO_MORE_ROOM_IN_HELL") = NO_MORE_ROOM_IN_HELL; - KnownCodes.attr("NUCLEAR_DAWN") = NUCLEAR_DAWN; - KnownCodes.attr("TACTICAL_INTERVENTION") = TACTICAL_INTERVENTION; - KnownCodes.attr("TEAM_FORTRESS_2") = TEAM_FORTRESS_2; - KnownCodes.attr("TEAM_FORTRESS_2_ITEMS") = TEAM_FORTRESS_2_ITEMS; - KnownCodes.attr("THE_SHIP") = THE_SHIP; - KnownCodes.attr("ZOMBIE_PANIC_SOURCE") = ZOMBIE_PANIC_SOURCE; - - KnownCodes.attr("EKV_GPU_DEFAULT") = EKV_GPU_DEFAULT; - KnownCodes.attr("EKV_GPU_ALIEN_SWARM") = EKV_GPU_ALIEN_SWARM; + KnownCodes.attr("COUNTER_STRIKE_2") = COUNTER_STRIKE_2; + KnownCodes.attr("COUNTER_STRIKE_PROMOD") = COUNTER_STRIKE_PROMOD; + KnownCodes.attr("DAY_OF_DEFEAT_SOURCE") = DAY_OF_DEFEAT_SOURCE; + KnownCodes.attr("DYSTOPIA_1_2") = DYSTOPIA_1_2; + KnownCodes.attr("DYSTOPIA_1_3") = DYSTOPIA_1_3; + KnownCodes.attr("GOLDEN_EYE_SOURCE") = GOLDEN_EYE_SOURCE; + KnownCodes.attr("HALF_LIFE_2_CTF") = HALF_LIFE_2_CTF; + KnownCodes.attr("HALF_LIFE_2_DM") = HALF_LIFE_2_DM; + KnownCodes.attr("INSURGENCY") = INSURGENCY; + KnownCodes.attr("LEFT_4_DEAD_2") = LEFT_4_DEAD_2; + KnownCodes.attr("NO_MORE_ROOM_IN_HELL") = NO_MORE_ROOM_IN_HELL; + KnownCodes.attr("NUCLEAR_DAWN") = NUCLEAR_DAWN; + KnownCodes.attr("TACTICAL_INTERVENTION") = TACTICAL_INTERVENTION; + KnownCodes.attr("TEAM_FORTRESS_2") = TEAM_FORTRESS_2; + KnownCodes.attr("TEAM_FORTRESS_2_ITEMS") = TEAM_FORTRESS_2_ITEMS; + KnownCodes.attr("THE_SHIP") = THE_SHIP; + KnownCodes.attr("ZOMBIE_PANIC_SOURCE") = ZOMBIE_PANIC_SOURCE; + + KnownCodes.attr("EKV_GPU_DEFAULT") = EKV_GPU_DEFAULT; + KnownCodes.attr("EKV_GPU_ALIEN_SWARM") = EKV_GPU_ALIEN_SWARM; KnownCodes.attr("EKV_GPU_LEFT_4_DEAD_1") = EKV_GPU_LEFT_4_DEAD_1; KnownCodes.attr("EKV_GPU_LEFT_4_DEAD_2") = EKV_GPU_LEFT_4_DEAD_2; - KnownCodes.attr("EKV_GPU_PORTAL_2") = EKV_GPU_PORTAL_2; + KnownCodes.attr("EKV_GPU_PORTAL_2") = EKV_GPU_PORTAL_2; } - VICE.def("decrypt_bytes", [](const py::bytes& data, std::string_view code = KnownCodes::DEFAULT) -> py::bytes { - const std::string_view dataView{data}; - const auto d = decrypt({reinterpret_cast(dataView.data()), dataView.size()}, code); - return {reinterpret_cast(d.data()), d.size()}; + VICE.def("decrypt_bytes", [](const py::bytes& data, std::string_view code = KnownCodes::DEFAULT) { + const auto d = decrypt({reinterpret_cast(data.data()), data.size()}, code); + return py::bytes{d.data(), d.size()}; }, py::arg("data"), py::arg("code") = KnownCodes::DEFAULT); VICE.def("decrypt_str", [](std::string_view data, std::string_view code = KnownCodes::DEFAULT) -> std::string { @@ -77,15 +75,14 @@ inline void register_python(py::module_& m) { return {reinterpret_cast(d.data()), d.size()}; }, py::arg("data"), py::arg("code") = KnownCodes::DEFAULT); - VICE.def("encrypt_bytes", [](const py::bytes& data, std::string_view code = KnownCodes::DEFAULT) -> py::bytes { - const std::string_view dataView{data}; - const auto e = encrypt({reinterpret_cast(dataView.data()), dataView.size()}, code); - return {reinterpret_cast(e.data()), e.size()}; + VICE.def("encrypt_bytes", [](const py::bytes& data, std::string_view code = KnownCodes::DEFAULT) { + const auto d = encrypt({reinterpret_cast(data.data()), data.size()}, code); + return py::bytes{d.data(), d.size()}; }, py::arg("data"), py::arg("code") = KnownCodes::DEFAULT); VICE.def("encrypt_str", [](std::string_view data, std::string_view code = KnownCodes::DEFAULT) -> std::string { - const auto e = decrypt({reinterpret_cast(data.data()), data.size()}, code); - return {reinterpret_cast(e.data()), e.size()}; + const auto d = encrypt({reinterpret_cast(data.data()), data.size()}, code); + return {reinterpret_cast(d.data()), d.size()}; }, py::arg("data"), py::arg("code") = KnownCodes::DEFAULT); } } diff --git a/lang/python/src/vtfpp.h b/lang/python/src/vtfpp.h new file mode 100644 index 000000000..05a4b7a0e --- /dev/null +++ b/lang/python/src/vtfpp.h @@ -0,0 +1,436 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace py = nanobind; + +#include + +namespace vtfpp { + +void register_python(py::module_& m) { + using namespace vtfpp; + auto vtfpp = m.def_submodule("vtfpp"); + + py::enum_(vtfpp, "ImageFormat") + .value("RGBA8888", ImageFormat::RGBA8888) + .value("ABGR8888", ImageFormat::ABGR8888) + .value("RGB888", ImageFormat::RGB888) + .value("BGR888", ImageFormat::BGR888) + .value("RGB565", ImageFormat::RGB565) + .value("I8", ImageFormat::I8) + .value("IA88", ImageFormat::IA88) + .value("P8", ImageFormat::P8) + .value("A8", ImageFormat::A8) + .value("RGB888_BLUESCREEN", ImageFormat::RGB888_BLUESCREEN) + .value("BGR888_BLUESCREEN", ImageFormat::BGR888_BLUESCREEN) + .value("ARGB8888", ImageFormat::ARGB8888) + .value("BGRA8888", ImageFormat::BGRA8888) + .value("DXT1", ImageFormat::DXT1) + .value("DXT3", ImageFormat::DXT3) + .value("DXT5", ImageFormat::DXT5) + .value("BGRX8888", ImageFormat::BGRX8888) + .value("BGR565", ImageFormat::BGR565) + .value("BGRX5551", ImageFormat::BGRX5551) + .value("BGRA4444", ImageFormat::BGRA4444) + .value("DXT1_ONE_BIT_ALPHA", ImageFormat::DXT1_ONE_BIT_ALPHA) + .value("BGRA5551", ImageFormat::BGRA5551) + .value("UV88", ImageFormat::UV88) + .value("UVWQ8888", ImageFormat::UVWQ8888) + .value("RGBA16161616F", ImageFormat::RGBA16161616F) + .value("RGBA16161616", ImageFormat::RGBA16161616) + .value("UVLX8888", ImageFormat::UVLX8888) + .value("R32F", ImageFormat::R32F) + .value("RGB323232F", ImageFormat::RGB323232F) + .value("RGBA32323232F", ImageFormat::RGBA32323232F) + .value("RG1616F", ImageFormat::RG1616F) + .value("RG3232F", ImageFormat::RG3232F) + .value("RGBX8888", ImageFormat::RGBX8888) + .value("EMPTY", ImageFormat::EMPTY) + .value("ATI2N", ImageFormat::ATI2N) + .value("ATI1N", ImageFormat::ATI1N) + .value("RGBA1010102", ImageFormat::RGBA1010102) + .value("BGRA1010102", ImageFormat::BGRA1010102) + .value("R16F", ImageFormat::R16F) + .value("R8", ImageFormat::R8) + .value("BC7", ImageFormat::BC7) + .value("BC6H", ImageFormat::BC6H) + .export_values(); + + { + using namespace ImageFormatDetails; + auto ImageFormatDetails = vtfpp.def_submodule("ImageFormatDetails"); + + ImageFormatDetails.def("red", &red, py::arg("format")); + ImageFormatDetails.def("decompressedRed", &decompressedRed, py::arg("format")); + ImageFormatDetails.def("green", &green, py::arg("format")); + ImageFormatDetails.def("decompressedGreen", &decompressedGreen, py::arg("format")); + ImageFormatDetails.def("blue", &blue, py::arg("format")); + ImageFormatDetails.def("decompressedBlue", &decompressedBlue, py::arg("format")); + ImageFormatDetails.def("alpha", &alpha, py::arg("format")); + ImageFormatDetails.def("decompressedAlpha", &decompressedAlpha, py::arg("format")); + ImageFormatDetails.def("bpp", &bpp, py::arg("format")); + ImageFormatDetails.def("containerFormat", &containerFormat, py::arg("format")); + ImageFormatDetails.def("large", &large, py::arg("format")); + ImageFormatDetails.def("decimal", &decimal, py::arg("format")); + ImageFormatDetails.def("compressed", &compressed, py::arg("format")); + ImageFormatDetails.def("transparent", &transparent, py::arg("format")); + ImageFormatDetails.def("opaque", &opaque, py::arg("format")); + + ImageFormatDetails.def("get_data_length", py::overload_cast(&getDataLength), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("slice_count") = 1); + ImageFormatDetails.def("get_data_length_extended", py::overload_cast(&getDataLength), py::arg("format"), py::arg("mip_count"), py::arg("frame_count"), py::arg("face_count"), py::arg("width"), py::arg("height"), py::arg("slice_count")); + ImageFormatDetails.def("get_data_position", [](ImageFormat format, uint8_t mip, uint8_t mipCount, uint16_t frame, uint16_t frameCount, uint8_t face, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t slice = 0, uint16_t sliceCount = 1) -> std::pair { + uint32_t offset, length; + if (getDataPosition(offset, length, format, mip, mipCount, frame, frameCount, face, faceCount, width, height, slice, sliceCount)) { + return {offset, length}; + } + return {0, 0}; + }, py::arg("format"), py::arg("mip"), py::arg("mip_count"), py::arg("frame"), py::arg("frame_count"), py::arg("face"), py::arg("face_count"), py::arg("width"), py::arg("height"), py::arg("slice") = 0, py::arg("slice_count") = 1); + } + + { + using namespace ImageDimensions; + auto ImageDimensions = vtfpp.def_submodule("ImageDimensions"); + + ImageDimensions.def("get_mip_dim", &getMipDim, py::arg("mip"), py::arg("dim")); + ImageDimensions.def("get_recommended_mip_count_for_dims", &getRecommendedMipCountForDims, py::arg("format"), py::arg("width"), py::arg("height")); + } + + // Skip ImagePixel, difficult to bind + + { + using namespace ImageConversion; + auto ImageConversion = vtfpp.def_submodule("ImageConversion"); + + // todo(python): still need to bind the following: + ImageConversion.def("convert_image_data_to_format", [](const py::bytes& imageData, ImageFormat oldFormat, ImageFormat newFormat, uint16_t width, uint16_t height) { + const auto d = convertImageDataToFormat({reinterpret_cast(imageData.data()), imageData.size()}, oldFormat, newFormat, width, height); + return py::bytes{d.data(), d.size()}; + }, py::arg("image_data"), py::arg("old_format"), py::arg("new_format"), py::arg("width"), py::arg("height")); + + ImageConversion.def("convert_several_image_data_to_format", [](const py::bytes& imageData, ImageFormat oldFormat, ImageFormat newFormat, uint8_t mipCount, uint16_t frameCount, uint16_t faceCount, uint16_t width, uint16_t height, uint16_t sliceCount) { + const auto d = convertSeveralImageDataToFormat({reinterpret_cast(imageData.data()), imageData.size()}, oldFormat, newFormat, mipCount, frameCount, faceCount, width, height, sliceCount); + return py::bytes{d.data(), d.size()}; + }, py::arg("image_data"), py::arg("old_format"), py::arg("new_format"), py::arg("mip_count"), py::arg("frame_count"), py::arg("face_count"), py::arg("width"), py::arg("height"), py::arg("slice_count")); + + py::enum_(ImageConversion, "FileFormat") + .value("DEFAULT", FileFormat::DEFAULT) + .value("PNG", FileFormat::PNG) + .value("JPEG", FileFormat::JPEG) + .value("BMP", FileFormat::BMP) + .value("TGA", FileFormat::TGA) + .value("HDR", FileFormat::HDR) + .export_values(); + + ImageConversion.def("get_default_file_format_for_image_format", &getDefaultFileFormatForImageFormat, py::arg("format")); + + ImageConversion.def("convert_image_data_to_file", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t height, FileFormat fileFormat = FileFormat::DEFAULT) { + const auto d = convertImageDataToFile({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height, fileFormat); + return py::bytes{d.data(), d.size()}; + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("file_format") = FileFormat::DEFAULT); + + ImageConversion.def("convert_file_to_image_data", [](const py::bytes& fileData) -> std::tuple { + ImageFormat format; + int width, height, frame; + const auto d = convertFileToImageData({reinterpret_cast(fileData.data()), fileData.size()}, format, width, height, frame); + return {py::bytes{d.data(), d.size()}, format, width, height, frame}; + }, py::arg("file_data")); + + py::enum_(ImageConversion, "ResizeEdge") + .value("CLAMP", ResizeEdge::CLAMP) + .value("REFLECT", ResizeEdge::REFLECT) + .value("WRAP", ResizeEdge::WRAP) + .value("ZERO", ResizeEdge::ZERO) + .export_values(); + + py::enum_(ImageConversion, "ResizeFilter") + .value("DEFAULT", ResizeFilter::DEFAULT) + .value("BOX", ResizeFilter::BOX) + .value("BILINEAR", ResizeFilter::BILINEAR) + .value("CUBIC_BSPLINE", ResizeFilter::CUBIC_BSPLINE) + .value("CATMULLROM", ResizeFilter::CATMULLROM) + .value("MITCHELL", ResizeFilter::MITCHELL) + .export_values(); + + py::enum_(ImageConversion, "ResizeMethod") + .value("NONE", ResizeMethod::NONE) + .value("POWER_OF_TWO_BIGGER", ResizeMethod::POWER_OF_TWO_BIGGER) + .value("POWER_OF_TWO_SMALLER", ResizeMethod::POWER_OF_TWO_SMALLER) + .value("POWER_OF_TWO_NEAREST", ResizeMethod::POWER_OF_TWO_NEAREST) + .export_values(); + + ImageConversion.def("get_resized_dim", &getResizedDim, py::arg("n"), py::arg("resize_method")); + ImageConversion.def("get_resized_dims", [](uint16_t width, ResizeMethod widthResize, uint16_t height, ResizeMethod heightResize) -> std::pair { + setResizedDims(width, widthResize, height, heightResize); + return {width, height}; + }, py::arg("width"), py::arg("resize_width"), py::arg("height"), py::arg("resize_height")); + + ImageConversion.def("resize_image_data", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t newWidth, uint16_t height, uint16_t newHeight, bool srgb, ResizeFilter filter, ResizeEdge edge = ResizeEdge::CLAMP) { + const auto d = resizeImageData({reinterpret_cast(imageData.data()), imageData.size()}, format, width, newWidth, height, newHeight, srgb, filter, edge); + return py::bytes{d.data(), d.size()}; + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("new_width"), py::arg("height"), py::arg("new_height"), py::arg("srgb"), py::arg("filter"), py::arg("edge") = ResizeEdge::CLAMP); + + ImageConversion.def("resize_image_data_strict", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t newWidth, ResizeMethod widthResize, uint16_t height, uint16_t newHeight, ResizeMethod heightResize, bool srgb, ResizeFilter filter, ResizeEdge edge = ResizeEdge::CLAMP) -> std::tuple { + uint16_t widthOut, heightOut; + const auto d = resizeImageDataStrict({reinterpret_cast(imageData.data()), imageData.size()}, format, width, newWidth, widthOut, widthResize, height, newHeight, heightOut, heightResize, srgb, filter, edge); + return {py::bytes{d.data(), d.size()}, widthOut, heightOut}; + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("new_width"), py::arg("width_resize"), py::arg("height"), py::arg("new_height"), py::arg("height_resize"), py::arg("srgb"), py::arg("filter"), py::arg("edge") = ResizeEdge::CLAMP); + + // Skip extractChannelFromImageData, difficult to bind + } + + py::class_(vtfpp, "PPLImage") + .def_ro("width", &PPL::Image::width) + .def_ro("height", &PPL::Image::height) + .def_prop_ro("data", [](const PPL::Image& self) { + return py::bytes{self.data.data(), self.data.size()}; + }); + + py::class_(vtfpp, "PPL") + .def(py::init(), py::arg("checksum"), py::arg("format") = ImageFormat::RGB888, py::arg("version") = 0) + .def("__init__", [](PPL* self, const py::bytes& pplData) { + return new(self) PPL{{reinterpret_cast(pplData.data()), pplData.size()}}; + }, py::arg("ppl_data")) + .def(py::init(), py::arg("path")) + .def("__bool__", &PPL::operator bool, py::is_operator()) + .def_prop_rw("version", &PPL::getVersion, &PPL::setVersion) + .def_prop_rw("checksum", &PPL::getChecksum, &PPL::setChecksum) + .def_prop_rw("format", &PPL::getFormat, &PPL::setFormat) + .def("has_image_for_lod", &PPL::hasImageForLOD, py::arg("lod")) + .def_prop_ro("image_lods", &PPL::getImageLODs) + .def("get_image_raw", [](const PPL& self, uint32_t lod = 0) -> std::optional { + const auto* image = self.getImageRaw(lod); + if (!image) { + return std::nullopt; + } + return *image; + }, py::arg("lod")) + .def("get_image_as", &PPL::getImageAs, py::arg("new_format"), py::arg("lod")) + .def("get_image_as_rgb888", &PPL::getImageAsRGB888, py::arg("lod")) + .def("set_image", [](PPL& self, const py::bytes& imageData, ImageFormat format, uint32_t width, uint32_t height, uint32_t lod = 0) { + self.setImage({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height, lod); + }, py::arg("imageData"), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("lod") = 0) + .def("set_image_resized", [](PPL& self, const py::bytes& imageData, ImageFormat format, uint32_t width, uint32_t height, uint32_t resizedWidth, uint32_t resizedHeight, uint32_t lod = 0, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR) { + self.setImage({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height, resizedWidth, resizedHeight, lod, filter); + }, py::arg("imageData"), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("resized_width"), py::arg("resized_height"), py::arg("lod") = 0, py::arg("filter") = ImageConversion::ResizeFilter::BILINEAR) + .def("set_image_from_file", py::overload_cast(&PPL::setImage), py::arg("image_path"), py::arg("lod") = 0) + .def("set_image_resized_from_file", py::overload_cast(&PPL::setImage), py::arg("image_path"), py::arg("resized_width"), py::arg("resized_height"), py::arg("lod") = 0, py::arg("filter") = ImageConversion::ResizeFilter::BILINEAR) + .def("save_image", [](const PPL& self, uint32_t lod = 0, ImageConversion::FileFormat fileFormat = ImageConversion::FileFormat::DEFAULT) { + const auto d = self.saveImageToFile(lod, fileFormat); + return py::bytes{d.data(), d.size()}; + }, py::arg("lod") = 0, py::arg("file_format") = ImageConversion::FileFormat::DEFAULT) + .def("save_image_to_file", py::overload_cast(&PPL::saveImageToFile, py::const_), py::arg("image_path"), py::arg("lod") = 0, py::arg("file_format") = ImageConversion::FileFormat::DEFAULT) + .def("bake", [](PPL& self) { + const auto d = self.bake(); + return py::bytes{d.data(), d.size()}; + }) + .def("bake_to_file", py::overload_cast(&PPL::bake), py::arg("ppl_path")); + + vtfpp.attr("VTF_SIGNATURE") = VTF_SIGNATURE; + + py::enum_(vtfpp, "CompressionMethod") + .value("DEFLATE", CompressionMethod::DEFLATE) + .value("ZSTD", CompressionMethod::ZSTD) + .export_values(); + + py::enum_(vtfpp, "ResourceType") + .value("UNKNOWN", Resource::TYPE_UNKNOWN) + .value("THUMBNAIL_DATA", Resource::TYPE_THUMBNAIL_DATA) + .value("IMAGE_DATA", Resource::TYPE_IMAGE_DATA) + .value("PARTICLE_SHEET_DATA", Resource::TYPE_PARTICLE_SHEET_DATA) + .value("CRC", Resource::TYPE_CRC) + .value("LOD_CONTROL_INFO", Resource::TYPE_LOD_CONTROL_INFO) + .value("EXTENDED_FLAGS", Resource::TYPE_EXTENDED_FLAGS) + .value("KEYVALUES_DATA", Resource::TYPE_KEYVALUES_DATA) + .value("AUX_COMPRESSION", Resource::TYPE_AUX_COMPRESSION) + .export_values(); + + py::enum_(vtfpp, "ResourceFlags") + .value("NONE", Resource::FLAG_NONE) + .value("LOCAL_DATA", Resource::FLAG_LOCAL_DATA) + .export_values(); + + // Skip Resource, mostly useless outside C++ + + py::enum_(vtfpp, "VTFFlags") + .value("NONE", VTF::FLAG_NONE) + .value("POINT_SAMPLE", VTF::FLAG_POINT_SAMPLE) + .value("TRILINEAR", VTF::FLAG_TRILINEAR) + .value("CLAMP_S", VTF::FLAG_CLAMP_S) + .value("CLAMP_T", VTF::FLAG_CLAMP_T) + .value("ANISOTROPIC", VTF::FLAG_ANISOTROPIC) + .value("HINT_DXT5", VTF::FLAG_HINT_DXT5) + .value("SRGB", VTF::FLAG_SRGB) + .value("NO_COMPRESS", VTF::FLAG_NO_COMPRESS) + .value("NORMAL", VTF::FLAG_NORMAL) + .value("NO_MIP", VTF::FLAG_NO_MIP) + .value("NO_LOD", VTF::FLAG_NO_LOD) + .value("MIN_MIP", VTF::FLAG_MIN_MIP) + .value("PROCEDURAL", VTF::FLAG_PROCEDURAL) + .value("ONE_BIT_ALPHA", VTF::FLAG_ONE_BIT_ALPHA) + .value("MULTI_BIT_ALPHA", VTF::FLAG_MULTI_BIT_ALPHA) + .value("ENVMAP", VTF::FLAG_ENVMAP) + .value("RENDERTARGET", VTF::FLAG_RENDERTARGET) + .value("DEPTH_RENDERTARGET", VTF::FLAG_DEPTH_RENDERTARGET) + .value("NO_DEBUG_OVERRIDE", VTF::FLAG_NO_DEBUG_OVERRIDE) + .value("SINGLE_COPY", VTF::FLAG_SINGLE_COPY) + .value("ONE_OVER_MIP_LEVEL_IN_ALPHA", VTF::FLAG_ONE_OVER_MIP_LEVEL_IN_ALPHA) + .value("PREMULTIPLY_COLOR_BY_ONE_OVER_MIP_LEVEL", VTF::FLAG_PREMULTIPLY_COLOR_BY_ONE_OVER_MIP_LEVEL) + .value("NORMAL_TO_DUDV", VTF::FLAG_NORMAL_TO_DUDV) + .value("ALPHA_TEST_MIP_GENERATION", VTF::FLAG_ALPHA_TEST_MIP_GENERATION) + .value("NO_DEPTH_BUFFER", VTF::FLAG_NO_DEPTH_BUFFER) + .value("NICE_FILTERED", VTF::FLAG_NICE_FILTERED) + .value("CLAMP_U", VTF::FLAG_CLAMP_U) + .value("VERTEX_TEXTURE", VTF::FLAG_VERTEX_TEXTURE) + .value("SSBUMP", VTF::FLAG_SSBUMP) + .value("UNFILTERABLE_OK", VTF::FLAG_UNFILTERABLE_OK) + .value("BORDER", VTF::FLAG_BORDER) + .value("SPECVAR_RED", VTF::FLAG_SPECVAR_RED) + .value("SPECVAR_ALPHA", VTF::FLAG_SPECVAR_ALPHA) + .export_values(); + + py::class_(vtfpp, "VTFCreationOptions") + .def(py::init<>()) + .def_rw("major_version", &VTF::CreationOptions::majorVersion) + .def_rw("minor_version", &VTF::CreationOptions::minorVersion) + .def_rw("output_format", &VTF::CreationOptions::outputFormat) + .def_rw("width_resize_method", &VTF::CreationOptions::widthResizeMethod) + .def_rw("height_resize_method", &VTF::CreationOptions::heightResizeMethod) + .def_rw("filter", &VTF::CreationOptions::filter) + .def_rw("flags", &VTF::CreationOptions::flags) + .def_rw("initial_frame_count", &VTF::CreationOptions::initialFrameCount) + .def_rw("start_frame", &VTF::CreationOptions::startFrame) + .def_rw("is_cubemap", &VTF::CreationOptions::isCubeMap) + .def_rw("has_spheremap", &VTF::CreationOptions::hasSphereMap) + .def_rw("initial_slice_count", &VTF::CreationOptions::initialSliceCount) + .def_rw("create_mips", &VTF::CreationOptions::createMips) + .def_rw("create_thumbnail", &VTF::CreationOptions::createThumbnail) + .def_rw("create_reflectivity", &VTF::CreationOptions::createReflectivity) + .def_rw("compression_level", &VTF::CreationOptions::compressionLevel) + .def_rw("compression_method", &VTF::CreationOptions::compressionMethod) + .def_rw("bumpmap_scale", &VTF::CreationOptions::bumpMapScale); + + py::class_(vtfpp, "VTF") + .def_ro_static("FLAG_MASK_GENERATED", &VTF::FLAG_MASK_GENERATED) + .def_ro_static("FORMAT_UNCHANGED", &VTF::FORMAT_UNCHANGED) + .def_ro_static("FORMAT_DEFAULT", &VTF::FORMAT_DEFAULT) + .def_ro_static("MAX_RESOURCES", &VTF::MAX_RESOURCES) + .def(py::init<>()) + .def("__init__", [](VTF* self, const py::bytes& vtfData, bool parseHeaderOnly = false) { + return new(self) VTF{std::span{reinterpret_cast(vtfData.data()), vtfData.size()}, parseHeaderOnly}; + }, py::arg("vtf_data"), py::arg("parse_header_only") = false) + .def(py::init(), py::arg("vtf_path"), py::arg("parse_header_only") = false) + .def("__bool__", &VTF::operator bool, py::is_operator()) + .def_static("create_and_bake", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t height, const std::string& vtfPath, VTF::CreationOptions options) { + VTF::create({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height, vtfPath, options); + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("vtf_path"), py::arg("creation_options") = VTF::CreationOptions{}) + .def_static("create_blank_and_bake", py::overload_cast(&VTF::create), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("vtf_path"), py::arg("creation_options") = VTF::CreationOptions{}) + .def_static("create", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t height, VTF::CreationOptions options) { + return VTF::create({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height, options); + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("creation_options") = VTF::CreationOptions{}) + .def_static("create_blank", py::overload_cast(&VTF::create), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("creation_options") = VTF::CreationOptions{}) + .def_static("create_from_file_and_bake", py::overload_cast(&VTF::create), py::arg("image_path"), py::arg("vtf_path"), py::arg("creation_options") = VTF::CreationOptions{}) + .def_static("create_from_file", py::overload_cast(&VTF::create), py::arg("image_path"), py::arg("creation_options") = VTF::CreationOptions{}) + .def_prop_rw("version_major", &VTF::getMajorVersion, &VTF::setMajorVersion) + .def_prop_rw("version_minor", &VTF::getMinorVersion, &VTF::setMinorVersion) + .def_prop_rw("image_width_resize_method", &VTF::getImageWidthResizeMethod, &VTF::setImageWidthResizeMethod) + .def_prop_rw("image_height_resize_method", &VTF::getImageHeightResizeMethod, &VTF::setImageHeightResizeMethod) + .def_prop_ro("width", &VTF::getWidth) + .def("width_for_mip", [](const VTF& self, uint8_t mip = 0) { return self.getWidth(mip); }, py::arg("mip") = 0) + .def_prop_ro("height", &VTF::getHeight) + .def("height_for_mip", [](const VTF& self, uint8_t mip = 0) { return self.getHeight(mip); }, py::arg("mip") = 0) + .def("set_size", &VTF::setSize, py::arg("width"), py::arg("height"), py::arg("filter")) + .def_prop_rw("flags", &VTF::getFlags, &VTF::setFlags) + .def("add_flags", &VTF::addFlags, py::arg("flags")) + .def("remove_flags", &VTF::removeFlags, py::arg("flags")) + .def_prop_ro("format", &VTF::getFormat) + .def("set_format", &VTF::setFormat, py::arg("new_format"), py::arg("filter") = ImageConversion::ResizeFilter::BILINEAR) + .def_prop_rw("mip_count", &VTF::getMipCount, &VTF::setMipCount) + .def("set_recommended_mip_count", &VTF::setRecommendedMipCount) + .def("compute_mips", &VTF::computeMips, py::arg("filter") = ImageConversion::ResizeFilter::BILINEAR) + .def_prop_rw("frame_count", &VTF::getFrameCount, &VTF::setFrameCount) + .def_prop_ro("face_count", &VTF::getFaceCount) + .def("set_face_count", &VTF::setFaceCount, py::arg("is_cubemap"), py::arg("has_spheremap") = false) + .def_prop_rw("slice_count", &VTF::getSliceCount, &VTF::setSliceCount) + .def("set_frame_face_and_slice_count", &VTF::setFrameFaceAndSliceCount, py::arg("new_frame_count"), py::arg("is_cubemap"), py::arg("has_spheremap") = false, py::arg("new_slice_count") = 1) + .def_prop_rw("start_frame", &VTF::getStartFrame, &VTF::setStartFrame) + .def_prop_rw("reflectivity", &VTF::getReflectivity, &VTF::setReflectivity) + .def("compute_reflectivity", &VTF::computeReflectivity) + .def_prop_rw("bumpmap_scale", &VTF::getBumpMapScale, &VTF::setBumpMapScale) + .def_prop_ro("thumbnail_format", &VTF::getThumbnailFormat) + .def_prop_ro("thumbnail_width", &VTF::getThumbnailWidth) + .def_prop_ro("thumbnail_height", &VTF::getThumbnailHeight) + // Skip getResources + // Skip getResource + .def("set_particle_sheet_resource", [](VTF& self, const py::bytes& value) { return self.setParticleSheetResource({reinterpret_cast(value.data()), value.size()}); }, py::arg("value")) + .def("remove_particle_sheet_resource", &VTF::removeParticleSheetResource) + .def("set_crc_resource", &VTF::setCRCResource, py::arg("value")) + .def("remove_crc_resource", &VTF::removeCRCResource) + .def("set_lod_resource", &VTF::setLODResource, py::arg("u"), py::arg("v")) + .def("remove_lod_resource", &VTF::removeLODResource) + .def("set_extended_flags_resource", &VTF::setExtendedFlagsResource, py::arg("value")) + .def("remove_extended_flags_resource", &VTF::removeExtendedFlagsResource) + .def("set_keyvalues_data_resource", &VTF::setKeyValuesDataResource, py::arg("value")) + .def("remove_keyvalues_data_resource", &VTF::removeKeyValuesDataResource) + .def_prop_rw("compression_level", &VTF::getCompressionLevel, &VTF::setCompressionLevel) + .def_prop_rw("compression_method", &VTF::getCompressionMethod, &VTF::setCompressionMethod) + .def("has_image_data", &VTF::hasImageData) + .def("image_data_is_srgb", &VTF::imageDataIsSRGB) + .def("get_image_data_raw", [](const VTF& self, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) { + const auto d = self.getImageDataRaw(mip, frame, face, slice); + return py::bytes{d.data(), d.size()}; + }, py::arg("mip") = 0, py::arg("frame") = 0, py::arg("face") = 0, py::arg("slice") = 0) + .def("get_image_data_as", [](const VTF& self, ImageFormat newFormat, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) { + const auto d = self.getImageDataAs(newFormat, mip, frame, face, slice); + return py::bytes{d.data(), d.size()}; + }, py::arg("new_format"), py::arg("mip") = 0, py::arg("frame") = 0, py::arg("face") = 0, py::arg("slice") = 0) + .def("get_image_data_as_rgba8888", [](const VTF& self, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) { + const auto d = self.getImageDataAsRGBA8888(mip, frame, face, slice); + return py::bytes{d.data(), d.size()}; + }, py::arg("mip") = 0, py::arg("frame") = 0, py::arg("face") = 0, py::arg("slice") = 0) + .def("set_image", [](VTF& self, const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t height, ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::BILINEAR, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0) { + return self.setImage({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height, filter, mip, frame, face, slice); + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("filter"), py::arg("mip") = 0, py::arg("frame") = 0, py::arg("face") = 0, py::arg("slice") = 0) + .def("set_image_from_file", py::overload_cast(&VTF::setImage), py::arg("image_path"), py::arg("filter") = ImageConversion::ResizeFilter::BILINEAR, py::arg("mip") = 0, py::arg("frame") = 0, py::arg("face") = 0, py::arg("slice") = 0) + .def("save_image", [](const VTF& self, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, uint16_t slice = 0, ImageConversion::FileFormat fileFormat = ImageConversion::FileFormat::DEFAULT) { + const auto d = self.saveImageToFile(mip, frame, face, slice, fileFormat); + return py::bytes{d.data(), d.size()}; + }, py::arg("mip") = 0, py::arg("frame") = 0, py::arg("face") = 0, py::arg("slice") = 0, py::arg("file_format") = ImageConversion::FileFormat::DEFAULT) + .def("save_image_to_file", py::overload_cast(&VTF::saveImageToFile, py::const_), py::arg("image_path"), py::arg("mip") = 0, py::arg("frame") = 0, py::arg("face") = 0, py::arg("slice") = 0, py::arg("file_format") = ImageConversion::FileFormat::DEFAULT) + .def("has_thumbnail_data", &VTF::hasThumbnailData) + .def("get_thumbnail_data_raw", [](const VTF& self) { + const auto d = self.getThumbnailDataRaw(); + return py::bytes{d.data(), d.size()}; + }) + .def("get_thumbnail_data_as", [](const VTF& self, ImageFormat newFormat) { + const auto d = self.getThumbnailDataAs(newFormat); + return py::bytes{d.data(), d.size()}; + }, py::arg("new_format")) + .def("get_thumbnail_data_as_rgba8888", [](const VTF& self) { + const auto d = self.getThumbnailDataAsRGBA8888(); + return py::bytes{d.data(), d.size()}; + }) + .def("set_thumbnail", [](VTF& self, const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t height) { + return self.setThumbnail({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height); + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("height")) + .def("compute_thumbnail", &VTF::computeThumbnail, py::arg("filter") = ImageConversion::ResizeFilter::BILINEAR) + .def("remove_thumbnail", &VTF::removeThumbnail) + .def("save_thumbnail", [](const VTF& self, ImageConversion::FileFormat fileFormat = ImageConversion::FileFormat::DEFAULT) { + const auto d = self.saveThumbnailToFile(fileFormat); + return py::bytes{d.data(), d.size()}; + }, py::arg("file_format") = ImageConversion::FileFormat::DEFAULT) + .def("save_thumbnail_to_file", py::overload_cast(&VTF::saveThumbnailToFile, py::const_), py::arg("image_path"), py::arg("file_format") = ImageConversion::FileFormat::DEFAULT) + .def("bake", [](const VTF& self) { + const auto d = self.bake(); + return py::bytes{d.data(), d.size()}; + }) + .def("bake_to_file", py::overload_cast(&VTF::bake, py::const_), py::arg("vtf_path")); +} + +} // namespace vtfpp diff --git a/src/vpkpp/_vpkpp.cmake b/src/vpkpp/_vpkpp.cmake index 320ebe3ff..e264861d9 100644 --- a/src/vpkpp/_vpkpp.cmake +++ b/src/vpkpp/_vpkpp.cmake @@ -1,5 +1,5 @@ add_pretty_parser(vpkpp - DEPS cryptopp::cryptopp MINIZIP::minizip sourcepp::bsppp sourcepp::kvpp + DEPS cryptopp::cryptopp libzstd_static MINIZIP::minizip sourcepp::bsppp sourcepp::kvpp DEPS_PUBLIC tsl::hat_trie PRECOMPILED_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/BSP.h" @@ -32,6 +32,5 @@ add_pretty_parser(vpkpp "${CMAKE_CURRENT_LIST_DIR}/PackFile.cpp") if(SOURCEPP_VPKPP_SUPPORT_VPK_V54) - target_link_libraries(vpkpp PRIVATE libzstd_static) target_compile_definitions(vpkpp PRIVATE VPKPP_SUPPORT_VPK_V54) endif() diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index 598290449..6da2a5165 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -11,6 +11,7 @@ #include #include +#include #include @@ -19,25 +20,66 @@ using namespace vtfpp; namespace { -std::vector compressData(std::span data, int level) { - mz_ulong compressedSize = mz_compressBound(data.size()); - std::vector out(compressedSize); +std::vector compressData(std::span data, int16_t level, CompressionMethod method) { + switch (method) { + using enum CompressionMethod; + case DEFLATE: { + mz_ulong compressedSize = mz_compressBound(data.size()); + std::vector out(compressedSize); + + int status = MZ_OK; + while ((status = mz_compress2(reinterpret_cast(out.data()), &compressedSize, reinterpret_cast(data.data()), data.size(), level)) == MZ_BUF_ERROR) { + compressedSize *= 2; + out.resize(compressedSize); + } - int status = MZ_OK; - while ((status = mz_compress2(reinterpret_cast(out.data()), &compressedSize, reinterpret_cast(data.data()), data.size(), level)) == MZ_BUF_ERROR) { - compressedSize *= 2; - out.resize(compressedSize); - } + if (status != MZ_OK) { + return {}; + } + out.resize(compressedSize); + return out; + } + case ZSTD: { + if (level < 0) { + level = 6; + } - if (status != MZ_OK) { - return {}; + auto expectedSize = ZSTD_compressBound(data.size()); + std::vector out(expectedSize); + + auto compressedSize = ZSTD_compress(out.data(), expectedSize, data.data(), data.size(), level); + if (ZSTD_isError(compressedSize)) { + return {}; + } + + out.resize(compressedSize); + return out; + } } - out.resize(compressedSize); - return out; + return {}; } } // namespace +const std::array& Resource::getOrder() { + static constinit std::array typeArray{ + TYPE_THUMBNAIL_DATA, + TYPE_IMAGE_DATA, + TYPE_PARTICLE_SHEET_DATA, + TYPE_CRC, + TYPE_LOD_CONTROL_INFO, + TYPE_EXTENDED_FLAGS, + TYPE_KEYVALUES_DATA, + TYPE_AUX_COMPRESSION, + }; + static bool unsorted = true; + if (unsorted) { + std::sort(typeArray.begin(), typeArray.end()); + unsorted = false; + } + return typeArray; +} + Resource::ConvertedData Resource::convertData() const { switch (this->type) { case TYPE_CRC: @@ -146,17 +188,17 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly) Resource* lastResource = nullptr; for (int i = 0; i < resourceCount; i++) { - auto& [type, flags, data] = this->resources.emplace_back(); + auto& [type, flags_, data_] = this->resources.emplace_back(); auto typeAndFlags = stream.read(); type = static_cast(typeAndFlags & 0xffffff); // last 3 bytes - flags = static_cast(typeAndFlags >> 24); // first byte - data = stream.read_span(4); + flags_ = static_cast(typeAndFlags >> 24); // first byte + data_ = stream.read_span(4); - if (!(flags & Resource::FLAG_LOCAL_DATA)) { + if (!(flags_ & Resource::FLAG_LOCAL_DATA)) { if (lastResource) { auto lastOffset = *reinterpret_cast(lastResource->data.data()); - auto currentOffset = *reinterpret_cast(data.data()); + auto currentOffset = *reinterpret_cast(data_.data()); auto curPos = stream.tell(); stream.seek(lastOffset); @@ -191,9 +233,20 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly) if (uint32_t newOffset, newLength; ImageFormatDetails::getDataPosition(newOffset, newLength, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, 0, this->getSliceCount())) { // Keep in mind that slices are compressed together mz_ulong decompressedImageDataSize = newLength * this->sliceCount; - if (mz_uncompress(reinterpret_cast(decompressedImageData.data() + newOffset), &decompressedImageDataSize, reinterpret_cast(imageResource->data.data() + oldOffset), oldLength) != MZ_OK) { - this->opened = false; - return; + switch (auxResource->getDataAsAuxCompressionMethod()) { + using enum CompressionMethod; + case DEFLATE: + if (mz_uncompress(reinterpret_cast(decompressedImageData.data() + newOffset), &decompressedImageDataSize, reinterpret_cast(imageResource->data.data() + oldOffset), oldLength) != MZ_OK) { + this->opened = false; + return; + } + break; + case ZSTD: + if (auto decompressedSize = ZSTD_decompress(reinterpret_cast(decompressedImageData.data() + newOffset), decompressedImageDataSize, reinterpret_cast(imageResource->data.data() + oldOffset), oldLength); ZSTD_isError(decompressedSize) || decompressedSize != decompressedImageDataSize) { + this->opened = false; + return; + } + break; } } oldOffset += oldLength; @@ -227,7 +280,8 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly) } if (const auto* resource = this->getResource(Resource::TYPE_AUX_COMPRESSION)) { - this->compressionLevel = static_cast(resource->getDataAsAuxCompressionLevel()); + this->compressionLevel = resource->getDataAsAuxCompressionLevel(); + this->compressionMethod = resource->getDataAsAuxCompressionMethod(); this->removeResourceInternal(Resource::TYPE_AUX_COMPRESSION); } } @@ -263,13 +317,14 @@ VTF& VTF::operator=(const VTF& other) { this->resources.clear(); for (const auto& [otherType, otherFlags, otherData] : other.resources) { - auto& [type, flags, data] = this->resources.emplace_back(); + auto& [type, flags_, data_] = this->resources.emplace_back(); type = otherType; - flags = otherFlags; - data = {this->data.data() + (otherData.data() - other.data.data()), otherData.size()}; + flags_ = otherFlags; + data_ = {this->data.data() + (otherData.data() - other.data.data()), otherData.size()}; } this->compressionLevel = other.compressionLevel; + this->compressionMethod = other.compressionMethod; this->imageWidthResizeMethod = other.imageWidthResizeMethod; this->imageHeightResizeMethod = other.imageHeightResizeMethod; @@ -315,6 +370,7 @@ void VTF::createInternal(VTF& writer, CreationOptions options) { } writer.setFormat(options.outputFormat); writer.setCompressionLevel(options.compressionLevel); + writer.setCompressionMethod(options.compressionMethod); } void VTF::create(std::span imageData, ImageFormat format, uint16_t width, uint16_t height, const std::string& vtfPath, CreationOptions options) { @@ -410,6 +466,14 @@ void VTF::setImageResizeMethods(ImageConversion::ResizeMethod imageWidthResizeMe this->imageHeightResizeMethod = imageHeightResizeMethod_; } +void VTF::setImageWidthResizeMethod(ImageConversion::ResizeMethod imageWidthResizeMethod_) { + this->imageWidthResizeMethod = imageWidthResizeMethod_; +} + +void VTF::setImageHeightResizeMethod(ImageConversion::ResizeMethod imageHeightResizeMethod_) { + this->imageHeightResizeMethod = imageHeightResizeMethod_; +} + uint16_t VTF::getWidth(uint8_t mip) const { return mip > 0 ? ImageDimensions::getMipDim(mip, this->width) : this->width; } @@ -551,7 +615,7 @@ void VTF::computeMips(ImageConversion::ResizeFilter filter) { } #ifdef SOURCEPP_BUILD_WITH_THREADS })); - if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency() * 2) { + if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency()) { for (auto& future : futures) { future.get(); } @@ -689,7 +753,7 @@ void VTF::computeReflectivity() { futures.push_back(std::async(std::launch::async, [this, j, k, l] { return getReflectivityForImage(*this, j, k, l); })); - if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency() * 2) { + if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency()) { for (auto& future : futures) { this->reflectivity += future.get(); } @@ -784,7 +848,7 @@ void VTF::setResourceInternal(Resource::Type type, std::span da this->data.clear(); BufferStream writer{this->data}; - for (auto resourceType : Resource::TYPE_ARRAY_ORDER) { + for (auto resourceType : Resource::getOrder()) { if (!resourceData.contains(resourceType)) { continue; } @@ -933,7 +997,7 @@ void VTF::removeExtendedFlagsResource() { this->removeResourceInternal(Resource::TYPE_EXTENDED_FLAGS); } -void VTF::setKeyValuesData(const std::string& value) { +void VTF::setKeyValuesDataResource(const std::string& value) { std::vector keyValuesData; BufferStream writer{keyValuesData}; @@ -944,18 +1008,26 @@ void VTF::setKeyValuesData(const std::string& value) { this->setResourceInternal(Resource::TYPE_KEYVALUES_DATA, keyValuesData); } -void VTF::removeKeyValuesData() { +void VTF::removeKeyValuesDataResource() { this->removeResourceInternal(Resource::TYPE_KEYVALUES_DATA); } -uint8_t VTF::getCompressionLevel() const { +int16_t VTF::getCompressionLevel() const { return this->compressionLevel; } -void VTF::setCompressionLevel(uint8_t newCompressionLevel) { +void VTF::setCompressionLevel(int16_t newCompressionLevel) { this->compressionLevel = newCompressionLevel; } +CompressionMethod VTF::getCompressionMethod() const { + return this->compressionMethod; +} + +void VTF::setCompressionMethod(CompressionMethod newCompressionMethod) { + this->compressionMethod = newCompressionMethod; +} + bool VTF::hasImageData() const { return this->format != ImageFormat::EMPTY && this->width > 0 && this->height > 0; } @@ -986,6 +1058,10 @@ std::vector VTF::getImageDataAsRGBA8888(uint8_t mip, uint16_t frame, } bool VTF::setImage(std::span imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, ImageConversion::ResizeFilter filter, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) { + if (imageData_.empty()) { + return false; + } + if (!this->hasImageData()) { uint16_t resizedWidth = width_, resizedHeight = height_; ImageConversion::setResizedDims(resizedWidth, this->imageWidthResizeMethod, resizedHeight, this->imageHeightResizeMethod); @@ -1013,11 +1089,13 @@ bool VTF::setImage(std::span imageData_, ImageFormat format_, u } if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->mipCount, frame, this->frameCount, face, faceCount, this->width, this->height, slice, this->sliceCount)) { std::vector image{imageData_.begin(), imageData_.end()}; - if (this->format != format_) { - image = ImageConversion::convertImageDataToFormat(image, format_, this->format, this->width, this->height); + const auto newWidth = ImageDimensions::getMipDim(mip, this->width); + const auto newHeight = ImageDimensions::getMipDim(mip, this->height); + if (width_ != newWidth || height_ != newHeight) { + image = ImageConversion::resizeImageData(image, format_, width_, newWidth, height_, newHeight, this->imageDataIsSRGB(), filter); } - if (width_ != ImageDimensions::getMipDim(mip, this->width) || height_ != ImageDimensions::getMipDim(mip, this->height)) { - image = ImageConversion::resizeImageData(image, this->format, width_, ImageDimensions::getMipDim(mip, this->width), height_, ImageDimensions::getMipDim(mip, this->height), this->imageDataIsSRGB(), filter); + if (format_ != this->format) { + image = ImageConversion::convertImageDataToFormat(image, format_, this->format, newWidth, newHeight); } std::memcpy(imageResource->data.data() + offset, image.data(), image.size()); } @@ -1030,7 +1108,7 @@ bool VTF::setImage(const std::string& imagePath, ImageConversion::ResizeFilter f auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount); // Unable to decode file - if (inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) { + if (imageData_.empty() || inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) { return false; } @@ -1088,6 +1166,16 @@ std::vector VTF::getThumbnailDataAsRGBA8888() const { return this->getThumbnailDataAs(ImageFormat::RGBA8888); } +void VTF::setThumbnail(std::span imageData_, ImageFormat format_, uint16_t width_, uint16_t height_) { + if (format_ != this->thumbnailFormat) { + this->setResourceInternal(Resource::TYPE_THUMBNAIL_DATA, ImageConversion::convertImageDataToFormat(imageData_, format_, this->thumbnailFormat, width_, height_)); + } else { + this->setResourceInternal(Resource::TYPE_THUMBNAIL_DATA, imageData_); + } + this->thumbnailWidth = width_; + this->thumbnailHeight = height_; +} + void VTF::computeThumbnail(ImageConversion::ResizeFilter filter) { if (!this->hasImageData()) { return; @@ -1169,19 +1257,20 @@ std::vector VTF::bake() const { auxCompressionResourceData.resize((this->mipCount * this->frameCount * faceCount + 2) * sizeof(uint32_t)); BufferStream auxWriter{auxCompressionResourceData, false}; - // Format of aux resource is as follows, with each item being a 4 byte integer: + // Format of aux resource is as follows, with each item of unspecified type being a 4 byte integer: // - Size of resource in bytes, not counting this int - // - Compression level + // - Compression level, method (2 byte integers) // - (X times) Size of each mip-face-frame combo auxWriter .write(auxCompressionResourceData.size() - sizeof(uint32_t)) - .write(this->compressionLevel); + .write(this->compressionLevel) + .write(this->compressionMethod); for (int i = this->mipCount - 1; i >= 0; i--) { for (int j = 0; j < this->frameCount; j++) { for (int k = 0; k < faceCount; k++) { if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, 0, this->sliceCount)) { - auto compressedData = ::compressData({imageResource->data.data() + offset, length * this->sliceCount}, this->compressionLevel); + auto compressedData = ::compressData({imageResource->data.data() + offset, length * this->sliceCount}, this->compressionLevel, this->compressionMethod); compressedImageResourceData.insert(compressedImageResourceData.end(), compressedData.begin(), compressedData.end()); auxWriter.write(compressedData.size()); } @@ -1214,7 +1303,7 @@ std::vector VTF::bake() const { writer_.write(data); writer_.seek_u(resourceOffsetPos).write(resourceOffsetValue); }; - for (const auto resourceType : Resource::TYPE_ARRAY_ORDER) { + for (const auto resourceType : Resource::getOrder()) { if (hasAuxCompression && resourceType == Resource::TYPE_AUX_COMPRESSION) { writeNonLocalResource(writer, resourceType, auxCompressionResourceData); } else if (hasAuxCompression && resourceType == Resource::TYPE_IMAGE_DATA) { diff --git a/src/vtfpp/_vtfpp.cmake b/src/vtfpp/_vtfpp.cmake index c180bd685..bb3527974 100644 --- a/src/vtfpp/_vtfpp.cmake +++ b/src/vtfpp/_vtfpp.cmake @@ -1,5 +1,5 @@ add_pretty_parser(vtfpp - DEPS miniz sourcepp_stb sourcepp_tinyexr + DEPS miniz libzstd_static sourcepp_stb sourcepp_tinyexr PRECOMPILED_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageConversion.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageFormats.h" diff --git a/test/vtfpp.cpp b/test/vtfpp.cpp index a3b7b40f3..425be5391 100644 --- a/test/vtfpp.cpp +++ b/test/vtfpp.cpp @@ -836,6 +836,7 @@ TEST(vtfpp, read_v76_c9) { EXPECT_EQ(vtf.getThumbnailWidth(), 16); EXPECT_EQ(vtf.getThumbnailHeight(), 16); EXPECT_EQ(vtf.getCompressionLevel(), 9); + EXPECT_EQ(vtf.getCompressionMethod(), CompressionMethod::DEFLATE); // Resources EXPECT_EQ(vtf.getResources().size(), 2); @@ -876,7 +877,8 @@ TEST(vtfpp, write_v76_c6) { EXPECT_EQ(vtf.getThumbnailFormat(), ImageFormat::DXT1); EXPECT_EQ(vtf.getThumbnailWidth(), 16); EXPECT_EQ(vtf.getThumbnailHeight(), 16); - EXPECT_EQ(vtf.getCompressionLevel(), 6); + EXPECT_EQ(vtf.getCompressionLevel(), -1); + EXPECT_EQ(vtf.getCompressionMethod(), CompressionMethod::ZSTD); } TEST(vtfpp, read_v76_nomip_c9) { @@ -903,6 +905,7 @@ TEST(vtfpp, read_v76_nomip_c9) { EXPECT_EQ(vtf.getThumbnailWidth(), 16); EXPECT_EQ(vtf.getThumbnailHeight(), 16); EXPECT_EQ(vtf.getCompressionLevel(), 9); + EXPECT_EQ(vtf.getCompressionMethod(), CompressionMethod::DEFLATE); // Resources EXPECT_EQ(vtf.getResources().size(), 2);