From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- libs/libbsarch/.clang-format | 117 + libs/libbsarch/.gitattributes | 7 + libs/libbsarch/.github/workflows/build.yml | 64 + libs/libbsarch/.github/workflows/linting.yml | 17 + libs/libbsarch/.gitignore | 539 ++++ libs/libbsarch/.pre-commit-config.yaml | 20 + libs/libbsarch/CMakeLists.txt | 144 ++ libs/libbsarch/CMakePresets.json | 17 + libs/libbsarch/LICENSE | 21 + libs/libbsarch/README.md | 25 + libs/libbsarch/Version.cmake | 3 + libs/libbsarch/cmake/config.cmake.in | 37 + libs/libbsarch/delphi/libbsarch.dpr | 412 +++ libs/libbsarch/delphi/libbsarch.dproj | 916 +++++++ libs/libbsarch/delphi/wbBSArchive.pas | 2637 ++++++++++++++++++++ .../examples/libbsarch-visualstudio-test.cpp | 190 ++ .../examples/libbsarch-visualstudio-test.sln | 31 + libs/libbsarch/examples/test_read.ba2 | Bin 0 -> 5592601 bytes libs/libbsarch/examples/test_read.bsa | Bin 0 -> 5592685 bytes libs/libbsarch/include/libbsarch/base_types.hpp | 66 + libs/libbsarch/include/libbsarch/bs_archive.h | 100 + .../include/libbsarch/bs_archive_auto.hpp | 39 + .../include/libbsarch/bs_archive_entries.h | 24 + libs/libbsarch/include/libbsarch/libbsarch.h | 157 ++ libs/libbsarch/include/libbsarch/libbsarch.hpp | 37 + .../libbsarch/utils/convertible_ostream.hpp | 37 + .../include/libbsarch/utils/convertible_string.hpp | 50 + .../include/libbsarch/utils/string_convert.hpp | 18 + libs/libbsarch/src/bs_archive.cpp | 185 ++ libs/libbsarch/src/bs_archive_auto.cpp | 87 + libs/libbsarch/src/bs_archive_entries.cpp | 52 + libs/libbsarch/src/libbsarch.cpp | 206 ++ libs/libbsarch/src/libbsarch.def | 38 + libs/libbsarch/src/utils/convertible_ostream.cpp | 58 + libs/libbsarch/src/utils/convertible_string.cpp | 108 + libs/libbsarch/src/utils/string_convert.cpp | 39 + libs/libbsarch/vcpkg.json | 18 + 37 files changed, 6516 insertions(+) create mode 100644 libs/libbsarch/.clang-format create mode 100644 libs/libbsarch/.gitattributes create mode 100644 libs/libbsarch/.github/workflows/build.yml create mode 100644 libs/libbsarch/.github/workflows/linting.yml create mode 100644 libs/libbsarch/.gitignore create mode 100644 libs/libbsarch/.pre-commit-config.yaml create mode 100644 libs/libbsarch/CMakeLists.txt create mode 100644 libs/libbsarch/CMakePresets.json create mode 100644 libs/libbsarch/LICENSE create mode 100644 libs/libbsarch/README.md create mode 100644 libs/libbsarch/Version.cmake create mode 100644 libs/libbsarch/cmake/config.cmake.in create mode 100644 libs/libbsarch/delphi/libbsarch.dpr create mode 100644 libs/libbsarch/delphi/libbsarch.dproj create mode 100644 libs/libbsarch/delphi/wbBSArchive.pas create mode 100644 libs/libbsarch/examples/libbsarch-visualstudio-test.cpp create mode 100644 libs/libbsarch/examples/libbsarch-visualstudio-test.sln create mode 100644 libs/libbsarch/examples/test_read.ba2 create mode 100644 libs/libbsarch/examples/test_read.bsa create mode 100644 libs/libbsarch/include/libbsarch/base_types.hpp create mode 100644 libs/libbsarch/include/libbsarch/bs_archive.h create mode 100644 libs/libbsarch/include/libbsarch/bs_archive_auto.hpp create mode 100644 libs/libbsarch/include/libbsarch/bs_archive_entries.h create mode 100644 libs/libbsarch/include/libbsarch/libbsarch.h create mode 100644 libs/libbsarch/include/libbsarch/libbsarch.hpp create mode 100644 libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp create mode 100644 libs/libbsarch/include/libbsarch/utils/convertible_string.hpp create mode 100644 libs/libbsarch/include/libbsarch/utils/string_convert.hpp create mode 100644 libs/libbsarch/src/bs_archive.cpp create mode 100644 libs/libbsarch/src/bs_archive_auto.cpp create mode 100644 libs/libbsarch/src/bs_archive_entries.cpp create mode 100644 libs/libbsarch/src/libbsarch.cpp create mode 100644 libs/libbsarch/src/libbsarch.def create mode 100644 libs/libbsarch/src/utils/convertible_ostream.cpp create mode 100644 libs/libbsarch/src/utils/convertible_string.cpp create mode 100644 libs/libbsarch/src/utils/string_convert.cpp create mode 100644 libs/libbsarch/vcpkg.json (limited to 'libs/libbsarch') diff --git a/libs/libbsarch/.clang-format b/libs/libbsarch/.clang-format new file mode 100644 index 0000000..42a3a23 --- /dev/null +++ b/libs/libbsarch/.clang-format @@ -0,0 +1,117 @@ +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - forever + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - run: cmake --preset vs2022-windows "-DCMAKE_INSTALL_PREFIX=install" + + # build both Debug and Release for package + - name: Build libbsarch + run: | + cmake --build vsbuild --config Debug --verbose + cmake --build vsbuild --config Release --verbose + + - name: Install libbsarch + run: | + cmake --install vsbuild --config Debug + cmake --install vsbuild --config Release + + - name: Upload libbsarch artifact + uses: actions/upload-artifact@master + with: + name: libbsarch + path: ./install + + publish: + if: github.ref_type == 'tag' + needs: build + runs-on: windows-2022 + permissions: + contents: write + steps: + - name: Download Artifact + uses: actions/download-artifact@master + with: + name: libbsarch + path: ./install + + - name: Create libbsarch archive + run: 7z a libbsarch_${{ github.ref_name }}.7z ./install/* + + - name: Publish Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release create --draft=false --notes="${{ github.ref_name }}" "${{ github.ref_name }}" ./libbsarch_${{ github.ref_name }}.7z diff --git a/libs/libbsarch/.github/workflows/linting.yml b/libs/libbsarch/.github/workflows/linting.yml new file mode 100644 index 0000000..12681a8 --- /dev/null +++ b/libs/libbsarch/.github/workflows/linting.yml @@ -0,0 +1,17 @@ +name: Lint libbsarch + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check format + uses: ModOrganizer2/check-formatting-action@master + with: + check-path: "." + exclude-regex: "third-party" diff --git a/libs/libbsarch/.gitignore b/libs/libbsarch/.gitignore new file mode 100644 index 0000000..4c54425 --- /dev/null +++ b/libs/libbsarch/.gitignore @@ -0,0 +1,539 @@ + +# Created by https://www.gitignore.io/api/qt,c++,cmake,delphi,qtcreator,visualstudio +# Edit at https://www.gitignore.io/?templates=qt,c++,cmake,delphi,qtcreator,visualstudio + +### C++ ### +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + + +### CMake Patch ### +# External projects +*-prefix/ + +### Delphi ### +# Uncomment these types if you want even more clean repository. But be careful. +# It can make harm to an existing project source. Read explanations below. +# +# Resource files are binaries containing manifest, project icon and version info. +# They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files. +#*.res +# Type library file (binary). In old Delphi versions it should be stored. +# Since Delphi 2009 it is produced from .ridl file and can safely be ignored. +#*.tlb +# Diagram Portfolio file. Used by the diagram editor up to Delphi 7. +# Uncomment this if you are not using diagrams or use newer Delphi version. +#*.ddp +# Visual LiveBindings file. Added in Delphi XE2. +# Uncomment this if you are not using LiveBindings Designer. +#*.vlb +# Deployment Manager configuration file for your project. Added in Delphi XE2. +# Uncomment this if it is not mobile development and you do not use remote debug feature. +#*.deployproj +# C++ object files produced when C/C++ Output file generation is configured. +# Uncomment this if you are not using external objects (zlib library for example). +#*.obj + +# Delphi compiler-generated binaries (safe to delete) +*.bpl +*.bpi +*.dcp +*.apk +*.drc +*.map +*.dres +*.rsm +*.tds +*.dcu +*.ocx + +# Delphi autogenerated files (duplicated info) +*.cfg +# *.hpp +*Resource.rc + +# Delphi local files (user-specific info) +*.local +*.identcache +*.projdata +*.tvsconfig +*.dsk + +# Delphi history and backups +__history/ +__recovery/ +*.~* + +# Castalia statistics file (since XE7 Castalia is distributed with Delphi) +*.stat + +# Boss dependency manager vendor folder https://github.com/HashLoad/boss +modules/ + +### Qt ### +# C++ objects and libs + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +vsbuild + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database + +# QtCreator local machine specific files for imported projects +*creator.user* + +### QtCreator ### +# gitignore for Qt Creator like IDE for pure C/C++ project without Qt +# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html + + + +# Qt Creator autogenerated files + + +# A listing of all the files included in the project +*.files + +# Include directories +*.includes + +# Project configuration settings like predefined Macros +*.config + +# Qt Creator settings +*.creator + +# User project settings +*.creator.user* + +# Qt Creator backups + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.iobj +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# End of https://www.gitignore.io/api/qt,c++,cmake,delphi,qtcreator,visualstudio + +install +examples/build +examples/vsbuild +examples/test_ba1 +examples/test_ba2 diff --git a/libs/libbsarch/.pre-commit-config.yaml b/libs/libbsarch/.pre-commit-config.yaml new file mode 100644 index 0000000..3103a1f --- /dev/null +++ b/libs/libbsarch/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/libbsarch/CMakeLists.txt b/libs/libbsarch/CMakeLists.txt new file mode 100644 index 0000000..d3e994e --- /dev/null +++ b/libs/libbsarch/CMakeLists.txt @@ -0,0 +1,144 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakePackageConfigHelpers) + +project(libbsarch CXX) + +find_package(mo2-dds-header CONFIG REQUIRED) + +get_target_property(SRCS mo2::dds-header SOURCES) + +# libbsarch +add_library(libbsarch SHARED) +if(WIN32) + target_sources(libbsarch + PRIVATE + src/libbsarch.cpp + src/libbsarch.def + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.h + ) +else() + target_sources(libbsarch + PRIVATE + src/libbsarch.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.h + ) + # Set default symbol visibility to hidden, export only marked symbols + target_compile_options(libbsarch PRIVATE -fvisibility=hidden) +endif() +target_include_directories(libbsarch PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch) +set_target_properties(libbsarch PROPERTIES DEBUG_POSTFIX "d" CXX_STANDARD 11) +target_link_libraries(libbsarch PUBLIC mo2::dds-header) +target_compile_definitions(libbsarch PRIVATE BSARCH_DLL_EXPORT) + +# preprocessor definitions +if(MSVC) + target_compile_options(libbsarch PRIVATE + /W3 /EHsc /MP + $<$:/MDd /Od> + $<$:/Zi> + ) +endif() + + +# libbsarch_OOP +add_library(libbsarch_OOP STATIC) + +target_sources(libbsarch_OOP + PRIVATE + src/bs_archive_entries.cpp + src/utils/string_convert.cpp + src/utils/convertible_ostream.cpp + src/utils/convertible_string.cpp + src/bs_archive_auto.cpp + src/bs_archive.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/base_types.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/bs_archive_auto.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/bs_archive.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/bs_archive_entries.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/libbsarch.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils/convertible_string.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils/convertible_ostream.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils/string_convert.hpp +) +target_include_directories(libbsarch_OOP PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch + ${CMAKE_CURRENT_SOURCE_DIR}/include/libbsarch/utils) +target_link_libraries(libbsarch_OOP PUBLIC mo2::dds-header) + +set_target_properties(libbsarch_OOP PROPERTIES + DEBUG_POSTFIX "d" + CXX_STANDARD 17 +) + +if (MSVC) + target_compile_options(libbsarch_OOP PRIVATE /MP) +endif() + +# target_link_libraries(libbsarch_OOP PUBLIC libbsarch) + +# install + +# finding 32 or 64bits +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + # 64 bits + set(lib_delphi "${CMAKE_CURRENT_SOURCE_DIR}/delphi/lib64") + message("libbsarch: using 64bits DLL") +elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) + # 32 bits + set(lib_delphi "${CMAKE_CURRENT_SOURCE_DIR}/delphi/lib") + message("libbsarch: using 32bits DLL") +endif() + +install(TARGETS libbsarch libbsarch_OOP EXPORT libbsarchTargets FILE_SET HEADERS) +if(WIN32) + install(FILES $ DESTINATION pdb OPTIONAL) +endif() +install(EXPORT libbsarchTargets + FILE mo2-libbsarch-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-libbsarch +) + +if(WIN32) + # install the true DLLs (Windows only - Delphi-built BSArch) + install(FILES ${lib_delphi}/libbsarch.dll DESTINATION bin CONFIGURATIONS Release RelWithDebInfo MinSizeRel) + install(FILES ${lib_delphi}/libbsarchd.dll DESTINATION bin CONFIGURATIONS Debug) +endif() + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-libbsarch" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + + +include(${CMAKE_CURRENT_SOURCE_DIR}/Version.cmake) +set(LIBBSARCH_VERSION + ${LIBBSARCH_MAJOR_VERSION}.${LIBBSARCH_MINOR_VERSION}.${LIBBSARCH_PATCH_VERSION}) + + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config-version.cmake" + VERSION "${LIBBSARCH_VERSION}" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-libbsarch-config-version.cmake + DESTINATION lib/cmake/mo2-libbsarch +) diff --git a/libs/libbsarch/CMakePresets.json b/libs/libbsarch/CMakePresets.json new file mode 100644 index 0000000..acc6d97 --- /dev/null +++ b/libs/libbsarch/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "configurePresets": [ + { + "binaryDir": "${sourceDir}/vsbuild", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "generator": "Visual Studio 17 2022", + "name": "vs2022-windows" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/libs/libbsarch/LICENSE b/libs/libbsarch/LICENSE new file mode 100644 index 0000000..c2799e0 --- /dev/null +++ b/libs/libbsarch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Deorder + +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/libs/libbsarch/README.md b/libs/libbsarch/README.md new file mode 100644 index 0000000..671b6ff --- /dev/null +++ b/libs/libbsarch/README.md @@ -0,0 +1,25 @@ +# BSArchive DLL and C++ bindings + +BSArchive Dynamic Link Library and C++ bindings + +## How to build + +1) Create a `XEditPath` environment variable and set it to the directory containing the XEdit code +2) Build the DLL in Delphi by opening the `libbsarch.dproj` project +3) Build the LIB in Visual Studio by opening the `libbsarch.sln` project + +## How to use + +With CMake: +Use ``add_subdirectory`` and ``target_link_library`` +Then ``#include "libbsarch.h"`` for the C wrapper, or ``#include "bs_archive_auto"`` for the C++ wrapper. +See the example for code use. + +Without CMake: +Use `libbsarch.dll` (provided in delphi/lib), `libbsarch.lib` and `libbsarch.h` in your project. + +## Credits + +The original BSArchive can be found at: https://github.com/TES5Edit/TES5Edit/tree/dev/Tools/BSArchive +The version in this project had been modified for better compatibility with C/C++ and +allow the users to allocate their own memory in some cases. diff --git a/libs/libbsarch/Version.cmake b/libs/libbsarch/Version.cmake new file mode 100644 index 0000000..59f33b6 --- /dev/null +++ b/libs/libbsarch/Version.cmake @@ -0,0 +1,3 @@ +set(LIBBSARCH_MAJOR_VERSION 0) +set(LIBBSARCH_MINOR_VERSION 1) +set(LIBBSARCH_PATCH_VERSION 2) diff --git a/libs/libbsarch/cmake/config.cmake.in b/libs/libbsarch/cmake/config.cmake.in new file mode 100644 index 0000000..8bb228f --- /dev/null +++ b/libs/libbsarch/cmake/config.cmake.in @@ -0,0 +1,37 @@ +@PACKAGE_INIT@ + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-libbsarch-targets.cmake" ) + +target_link_libraries(mo2::libbsarch INTERFACE mo2::libbsarch_OOP) + +# libbsarch is usually be build in Debug or Release/RelWithDebInfo, so we need to map +# missing configurations to avoid issue with CMP0111 +get_target_property(_LIBBSARCH_CONFIGURATIONS mo2::libbsarch IMPORTED_CONFIGURATIONS) +list(FIND _LIBBSARCH_CONFIGURATIONS "RELEASE" _LIBBSARCH_HAS_RELEASE) +if (_LIBBSARCH_HAS_RELEASE EQUAL -1) + set_target_properties(mo2::libbsarch PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL RelWithDebInfo + MAP_IMPORTED_CONFIG_RELEASE RelWithDebInfo + ) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) + set_target_properties(mo2::libbsarch_OOP PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL RelWithDebInfo + MAP_IMPORTED_CONFIG_RELEASE RelWithDebInfo + ) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) +else() + set_target_properties(mo2::libbsarch PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL Release + MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release + ) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO) + set_target_properties(mo2::libbsarch_OOP PROPERTIES + MAP_IMPORTED_CONFIG_MINSIZEREL Release + MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release + ) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL) + set_property(TARGET mo2::libbsarch_OOP APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO) +endif() diff --git a/libs/libbsarch/delphi/libbsarch.dpr b/libs/libbsarch/delphi/libbsarch.dpr new file mode 100644 index 0000000..34c0e35 --- /dev/null +++ b/libs/libbsarch/delphi/libbsarch.dpr @@ -0,0 +1,412 @@ +library libbsarch; + +uses + Math, + Classes, + Types, + SysUtils, + wbStreams, + wbBSArchive; + +type + TwbBSResultCode = ( + BSA_RESULT_NONE = 0, + BSA_RESULT_EXCEPTION = -1 + ); + TwbBSResultMessage = packed record + code: ShortInt; + text: array[0..1023] of WideChar; + end; + TwbBSResultMessageBuffer = packed record + buffer: TwbBSResultBuffer; + message: TwbBSResultMessage; + end; + +{Shared} + +function string_to_cstring( + aString: String; + const aStringBufferSize: Cardinal; + const aStringBuffer: PChar +): Integer; stdcall; +begin + Result := Min(aStringBufferSize, Length(AString)); + if (aStringBuffer <> nil) and (Result > 0) then + Move(AString[1], aStringBuffer^, Result * SizeOf(WideChar)); +end; + +function string_to_cstring_end( + aString: String; + const aStringBufferSize: Cardinal; + const aStringBuffer: PChar +): Integer; stdcall; +begin + Result := Min(aStringBufferSize - 1, Length(AString)); + if (aStringBuffer <> nil) and (Result > 0) then + begin + Move(AString[1], aStringBuffer^, Result * SizeOf(WideChar)); + aStringBuffer[Result] := #0; + end; +end; + +procedure exception_handler(E: Exception; var Result: TwbBSResultMessage); stdcall; +begin + Result.code := ShortInt(BSA_RESULT_EXCEPTION); + string_to_cstring_end(E.Message, Length(Result.text), Result.text); +end; + +procedure buffer_exception_handler(E: Exception; var Result: TwbBSResultMessageBuffer); stdcall; +begin + Result.buffer.size := 0; + Result.buffer.data := nil; + Result.message.code := ShortInt(BSA_RESULT_EXCEPTION); + string_to_cstring_end(E.Message, Length(Result.message.text), Result.message.text); +end; + +{BSA File List} + +function bsa_entry_list_create:Pointer; stdcall; +begin + try + Result := TStringList.Create; + except + Result := nil; + end; +end; + +function bsa_entry_list_free(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TStringList(obj).Clear(); + TStringList(obj).free; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_entry_list_count(obj: Pointer): Integer; stdcall; +begin + try + Result := TStringList(obj).Count; + except + Result := Ord(BSA_RESULT_EXCEPTION); + end; +end; + +function bsa_entry_list_add(obj: Pointer; const aEntryString: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TStringList(obj).Add(String(aEntryString)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_entry_list_get(obj: Pointer; const aIndex: Cardinal; const aStringBufferSize: Cardinal; const aStringBuffer: PChar): Integer; stdcall; +begin + try + Result := string_to_cstring_end(TStringList(obj).Strings[aIndex], aStringBufferSize, aStringBuffer); + except + Result := Integer(BSA_RESULT_EXCEPTION); + end; +end; + +{BSArchive} + +function bsa_create:Pointer; stdcall; +begin + try + Result := TwbBSArchive.Create; + except + Result := nil; + end; +end; + +function bsa_free(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).free; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_load_from_file(obj: Pointer; const aFilePath: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).LoadFromFile(String(aFilePath)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_create_archive(obj: Pointer; const aFilePath: PChar; aType: TBSArchiveType; aFileList: TStringList): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).CreateArchive(String(aFilePath), aType, aFileList); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_save(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).Save; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_add_file_from_disk_root(obj: Pointer; const aRootDir, aSourcePath: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).AddFileDiskRoot(String(aRootDir), String(aSourcePath)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_add_file_from_disk(obj: Pointer; const aFilePath, aSourcePath: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).AddFileDisk(String(aFilePath), String(aSourcePath)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_add_file_from_memory(obj: Pointer; const aFilePath: PChar; const aSize: Cardinal; const aData: PByte): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).AddFileData(String(aFilePath), aSize, aData); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_find_file_record(obj: Pointer; const aFilePath: PChar): Pointer; stdcall; +begin + try + Result := TwbBSArchive(obj).FindFileRecord(String(aFilePath)); + except + Result := nil + end; +end; + +function bsa_extract_file_data_by_record(obj: Pointer; aFileRecord: Pointer): TwbBSResultMessageBuffer; stdcall; +begin + Result.message.code := Ord(BSA_RESULT_NONE); + try + Result.buffer := TwbBSArchive(obj).ExtractFileData(aFileRecord); + except + on E: Exception do + buffer_exception_handler(E, Result); + end; +end; + +function bsa_extract_file_data_by_filename(obj: Pointer; const aFilePath: PChar): TwbBSResultMessageBuffer; stdcall; +begin + Result.message.code := Ord(BSA_RESULT_NONE); + try + Result.buffer := TwbBSArchive(obj).ExtractFileData(String(aFilePath)); + except + on E: Exception do + buffer_exception_handler(E, Result); + end; +end; + +function bsa_file_data_free(obj: Pointer; fileDataResult: TwbBSResultMessageBuffer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).ReleaseFileData(fileDataResult.buffer); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_extract_file(obj: Pointer; const aFilePath, aSaveAs: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).ExtractFile(String(aFilePath), String(aSaveAs)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_iterate_files(obj: Pointer; aProc: TBSFileIterationProc; aContext: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).IterateFiles(aProc, aContext); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_file_exists(obj: Pointer; const aFilePath: string): Boolean; stdcall; +begin + try + Result := TwbBSArchive(obj).FileExists(aFilePath); + except + Result := False; + end; +end; + +function bsa_get_resource_list(obj: Pointer; const aEntryResultList: TStringList; aFolder: PChar): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).ResourceList(aEntryResultList, String(aFolder)); + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_close(obj: Pointer): TwbBSResultMessage; stdcall; +begin + Result.code := Ord(BSA_RESULT_NONE); + try + TwbBSArchive(obj).Close; + except + on E: Exception do + exception_handler(E, Result); + end; +end; + +function bsa_filename_get(obj: Pointer; const aStringBufferSize: Cardinal; const aStringBuffer: PChar): Cardinal; stdcall; +begin + Result := string_to_cstring_end(TwbBSArchive(obj).FileName, aStringBufferSize, aStringBuffer); +end; + +function bsa_archive_type_get(obj: Pointer): TBSArchiveType; stdcall; +begin + Result := TwbBSArchive(obj).ArchiveType; +end; + +function bsa_version_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).Version; +end; + +function bsa_format_name_get(obj: Pointer; const aStringBufferSize: Cardinal; const aStringBuffer: PChar): Cardinal; stdcall; +begin + Result := string_to_cstring_end(TwbBSArchive(obj).FormatName, aStringBufferSize, aStringBuffer); +end; + +function bsa_file_count_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).FileCount; +end; + +function bsa_archive_flags_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).ArchiveFlags; +end; + +procedure bsa_archive_flags_set(obj: Pointer; flags: Cardinal); stdcall; +begin + TwbBSArchive(obj).ArchiveFlags := flags; +end; + +function bsa_file_flags_get(obj: Pointer): Cardinal; stdcall; +begin + Result := TwbBSArchive(obj).FileFlags; +end; + +procedure bsa_file_flags_set(obj: Pointer; flags: Cardinal); stdcall; +begin + TwbBSArchive(obj).FileFlags := flags; +end; + +function bsa_compress_get(obj: Pointer): Boolean; stdcall; +begin + Result := TwbBSArchive(obj).Compress; +end; + +procedure bsa_compress_set(obj: Pointer; compress: Boolean); stdcall; +begin + TwbBSArchive(obj).Compress := compress; +end; + +function bsa_share_data_get(obj: Pointer): Boolean; stdcall; +begin + Result := TwbBSArchive(obj).ShareData; +end; + +procedure bsa_share_data_set(obj: Pointer; shareData: Boolean); stdcall; +begin + TwbBSArchive(obj).ShareData := shareData; +end; + +procedure bsa_file_dds_info_callback_set(obj: Pointer; aProc: TBSFileDDSInfoProc; aContext: Pointer); stdcall; +begin + TwbBSArchive(obj).DDSInfoProc := aProc; + TwbBSArchive(obj).DDSInfoProcContext := aContext; +end; + +exports + bsa_entry_list_create, + bsa_entry_list_free, + bsa_entry_list_count, + bsa_entry_list_add, + bsa_entry_list_get, + + bsa_filename_get, + bsa_archive_type_get, + bsa_version_get, + bsa_format_name_get, + bsa_file_count_get, + bsa_archive_flags_get, + bsa_archive_flags_set, + bsa_file_flags_get, + bsa_file_flags_set, + bsa_compress_get, + bsa_compress_set, + bsa_share_data_get, + bsa_share_data_set, + bsa_file_dds_info_callback_set, + + bsa_close, + bsa_get_resource_list, + bsa_file_exists, + bsa_iterate_files, + bsa_extract_file, + bsa_file_data_free, + bsa_extract_file_data_by_filename, + bsa_extract_file_data_by_record, + bsa_find_file_record, + bsa_add_file_from_memory, + bsa_add_file_from_disk_root, + bsa_add_file_from_disk, + bsa_save, + bsa_create_archive, + bsa_load_from_file, + bsa_create, + bsa_free + ; +end. diff --git a/libs/libbsarch/delphi/libbsarch.dproj b/libs/libbsarch/delphi/libbsarch.dproj new file mode 100644 index 0000000..1e928c6 --- /dev/null +++ b/libs/libbsarch/delphi/libbsarch.dproj @@ -0,0 +1,916 @@ + + + {381E4D59-4D34-46B3-B039-1B3C3ABEC58F} + libbsarch.dpr + True + Release + 3 + Library + None + 19.5 + Win64 + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + false + false + false + false + false + 00400000 + true + libbsarch + 1033 + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= + System;Xml;Data;Datasnap;Web;Soap;Winapi;$(DCC_Namespace) + C:\dev\TES5Edit + $(XEditPath)\Core;$(XEditPath)\External\lz4;$(XEditPath)\External\TForge;$(XEditPath)\External\TForge\Source\Include;$(XEditPath)\External\TForge\Source\Shared;$(XEditPath)\External\TForge\Source\Engine\Hashes;$(XEditPath)\External\TForge\Source\Engine\Forge;$(XEditPath)\External\ZLibEx;$(XEditPath)\External\lz4\lib\delphi;$(XEditPath)\External\lz4\common;$(DCC_UnitSearchPath) + + + System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + 1033 + + + System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + (None) + none + + + RELEASE;$(DCC_Define) + 0 + false + 0 + + + Win32\Release\ + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + (None) + Win32\Release\libbsarch-visualstudio-test.exe + + + X64\Release\ + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + (None) + X64\Release\libbsarch-visualstudio-test.exe + + + DEBUG;$(DCC_Define) + false + true + + + Win32\Debug\libbsarch-visualstudio-test.exe + Win32\Debug\ + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + (None) + + + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + (None) + X64\Debug\ + D:\Projects\libbsarch\x64\Debug\libbsarch-visualstudio-test.exe + D:\Projects\libbsarch\x64\Debug + + + + MainSource + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + + + + + libbsarch.dpr + + + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components + + + + True + True + + False + + + + true + + + + + true + + + + + true + + + + + libbsarch.dll + true + + + + + 1 + + + 0 + + + + + classes + 64 + + + classes + 64 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + 1 + + + 1 + + + 0 + + + + + 1 + .framework + + + 1 + .framework + + + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + 1 + + + + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + 12 + + + + + diff --git a/libs/libbsarch/delphi/wbBSArchive.pas b/libs/libbsarch/delphi/wbBSArchive.pas new file mode 100644 index 0000000..533ee1c --- /dev/null +++ b/libs/libbsarch/delphi/wbBSArchive.pas @@ -0,0 +1,2637 @@ +{****************************************************************************** + + This Source Code Form is subject to the terms of the Mozilla Public License, + v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain + one at https://mozilla.org/MPL/2.0/. + +*******************************************************************************} + +unit wbBSArchive; + +interface + +uses + System.SysUtils, + System.Classes, + Winapi.Windows, + System.Threading, + System.SyncObjs, + System.Generics.Defaults, + System.Generics.Collections, + wbStreams, + tfTypes, + tfMD5; + +const + csBSAVersion = '0.9d'; + +type + // per file compression options + TPackingCompression = (pcGlobal, pcCompress, pcUncompress); + + TBGSCompressionType = (ctZlib, ctLZ4Frame, ctLZ4Block); + + TMagic4 = array [0..3] of AnsiChar; + PMagic4 = ^TMagic4; + + TDXGI = ( + DXGI_FORMAT_UNKNOWN, + DXGI_FORMAT_R32G32B32A32_TYPELESS, + DXGI_FORMAT_R32G32B32A32_FLOAT, + DXGI_FORMAT_R32G32B32A32_UINT, + DXGI_FORMAT_R32G32B32A32_SINT, + DXGI_FORMAT_R32G32B32_TYPELESS, + DXGI_FORMAT_R32G32B32_FLOAT, + DXGI_FORMAT_R32G32B32_UINT, + DXGI_FORMAT_R32G32B32_SINT, + DXGI_FORMAT_R16G16B16A16_TYPELESS, + DXGI_FORMAT_R16G16B16A16_FLOAT, + DXGI_FORMAT_R16G16B16A16_UNORM, + DXGI_FORMAT_R16G16B16A16_UINT, + DXGI_FORMAT_R16G16B16A16_SNORM, + DXGI_FORMAT_R16G16B16A16_SINT, + DXGI_FORMAT_R32G32_TYPELESS, + DXGI_FORMAT_R32G32_FLOAT, + DXGI_FORMAT_R32G32_UINT, + DXGI_FORMAT_R32G32_SINT, + DXGI_FORMAT_R32G8X24_TYPELESS, + DXGI_FORMAT_D32_FLOAT_S8X24_UINT, + DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS, + DXGI_FORMAT_X32_TYPELESS_G8X24_UINT, + DXGI_FORMAT_R10G10B10A2_TYPELESS, + DXGI_FORMAT_R10G10B10A2_UNORM, + DXGI_FORMAT_R10G10B10A2_UINT, + DXGI_FORMAT_R11G11B10_FLOAT, + DXGI_FORMAT_R8G8B8A8_TYPELESS, + DXGI_FORMAT_R8G8B8A8_UNORM, + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + DXGI_FORMAT_R8G8B8A8_UINT, + DXGI_FORMAT_R8G8B8A8_SNORM, + DXGI_FORMAT_R8G8B8A8_SINT, + DXGI_FORMAT_R16G16_TYPELESS, + DXGI_FORMAT_R16G16_FLOAT, + DXGI_FORMAT_R16G16_UNORM, + DXGI_FORMAT_R16G16_UINT, + DXGI_FORMAT_R16G16_SNORM, + DXGI_FORMAT_R16G16_SINT, + DXGI_FORMAT_R32_TYPELESS, + DXGI_FORMAT_D32_FLOAT, + DXGI_FORMAT_R32_FLOAT, + DXGI_FORMAT_R32_UINT, + DXGI_FORMAT_R32_SINT, + DXGI_FORMAT_R24G8_TYPELESS, + DXGI_FORMAT_D24_UNORM_S8_UINT, + DXGI_FORMAT_R24_UNORM_X8_TYPELESS, + DXGI_FORMAT_X24_TYPELESS_G8_UINT, + DXGI_FORMAT_R8G8_TYPELESS, + DXGI_FORMAT_R8G8_UNORM, + DXGI_FORMAT_R8G8_UINT, + DXGI_FORMAT_R8G8_SNORM, + DXGI_FORMAT_R8G8_SINT, + DXGI_FORMAT_R16_TYPELESS, + DXGI_FORMAT_R16_FLOAT, + DXGI_FORMAT_D16_UNORM, + DXGI_FORMAT_R16_UNORM, + DXGI_FORMAT_R16_UINT, + DXGI_FORMAT_R16_SNORM, + DXGI_FORMAT_R16_SINT, + DXGI_FORMAT_R8_TYPELESS, + DXGI_FORMAT_R8_UNORM, + DXGI_FORMAT_R8_UINT, + DXGI_FORMAT_R8_SNORM, + DXGI_FORMAT_R8_SINT, + DXGI_FORMAT_A8_UNORM, + DXGI_FORMAT_R1_UNORM, + DXGI_FORMAT_R9G9B9E5_SHAREDEXP, + DXGI_FORMAT_R8G8_B8G8_UNORM, + DXGI_FORMAT_G8R8_G8B8_UNORM, + DXGI_FORMAT_BC1_TYPELESS, + DXGI_FORMAT_BC1_UNORM, + DXGI_FORMAT_BC1_UNORM_SRGB, + DXGI_FORMAT_BC2_TYPELESS, + DXGI_FORMAT_BC2_UNORM, + DXGI_FORMAT_BC2_UNORM_SRGB, + DXGI_FORMAT_BC3_TYPELESS, + DXGI_FORMAT_BC3_UNORM, + DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT_BC4_TYPELESS, + DXGI_FORMAT_BC4_UNORM, + DXGI_FORMAT_BC4_SNORM, + DXGI_FORMAT_BC5_TYPELESS, + DXGI_FORMAT_BC5_UNORM, + DXGI_FORMAT_BC5_SNORM, + DXGI_FORMAT_B5G6R5_UNORM, + DXGI_FORMAT_B5G5R5A1_UNORM, + DXGI_FORMAT_B8G8R8A8_UNORM, + DXGI_FORMAT_B8G8R8X8_UNORM, + DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, + DXGI_FORMAT_B8G8R8A8_TYPELESS, + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, + DXGI_FORMAT_B8G8R8X8_TYPELESS, + DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, + DXGI_FORMAT_BC6H_TYPELESS, + DXGI_FORMAT_BC6H_UF16, + DXGI_FORMAT_BC6H_SF16, + DXGI_FORMAT_BC7_TYPELESS, + DXGI_FORMAT_BC7_UNORM, + DXGI_FORMAT_BC7_UNORM_SRGB, + DXGI_FORMAT_AYUV, + DXGI_FORMAT_Y410, + DXGI_FORMAT_Y416, + DXGI_FORMAT_NV12, + DXGI_FORMAT_P010, + DXGI_FORMAT_P016, + DXGI_FORMAT_420_OPAQUE, + DXGI_FORMAT_YUY2, + DXGI_FORMAT_Y210, + DXGI_FORMAT_Y216, + DXGI_FORMAT_NV11, + DXGI_FORMAT_AI44, + DXGI_FORMAT_IA44, + DXGI_FORMAT_P8, + DXGI_FORMAT_A8P8, + DXGI_FORMAT_B4G4R4A4_UNORM, + DXGI_FORMAT_P208, + DXGI_FORMAT_V208, + DXGI_FORMAT_V408 + ); + + TDDSHeader = packed record + Magic: TMagic4; + dwSize: Cardinal; + dwFlags: Cardinal; + dwHeight: Cardinal; + dwWidth: Cardinal; + dwPitchOrLinearSize: Cardinal; + dwDepth: Cardinal; + dwMipMapCount: Cardinal; + dwReserved1: array [0..10] of Cardinal; + ddspf: packed record + dwSize: Cardinal; + dwFlags: Cardinal; + dwFourCC: TMagic4; + dwRGBBitCount: Cardinal; + dwRBitMask: Cardinal; + dwGBitMask: Cardinal; + dwBBitMask: Cardinal; + dwABitMask: Cardinal; + end; + dwCaps: Cardinal; + dwCaps2: Cardinal; + dwCaps3: Cardinal; + dwCaps4: Cardinal; + dwReserved2: Cardinal; + end; + PDDSHeader = ^TDDSHeader; + + TDDSHeaderDX10 = packed record + dxgiFormat: Integer; + resourceDimension: Cardinal; + miscFlags: Cardinal; + arraySize: Cardinal; + miscFlags2: Cardinal; + end; + PDDSHeaderDX10 = ^TDDSHeaderDX10; + +const + DDSD_CAPS = $00000001; + DDSD_HEIGHT = $00000002; + DDSD_WIDTH = $00000004; + DDSD_PITCH = $00000008; + DDSD_PIXELFORMAT = $00001000; + DDSD_MIPMAPCOUNT = $00020000; + DDSD_LINEARSIZE = $00080000; + DDSD_DEPTH = $00800000; + DDSCAPS_COMPLEX = $00000008; + DDSCAPS_TEXTURE = $00001000; + DDSCAPS_MIPMAP = $00400000; + DDSCAPS2_CUBEMAP = $00000200; + DDSCAPS2_POSITIVEX = $00000400; + DDSCAPS2_NEGATIVEX = $00000800; + DDSCAPS2_POSITIVEY = $00001000; + DDSCAPS2_NEGATIVEY = $00002000; + DDSCAPS2_POSITIVEZ = $00004000; + DDSCAPS2_NEGATIVEZ = $00008000; + DDSCAPS2_VOLUME = $00200000; + DDPF_ALPHAPIXELS = $00000001; + DDPF_ALPHA = $00000002; + DDPF_FOURCC = $00000004; + DDPF_RGB = $00000040; + DDPF_YUV = $00000200; + DDPF_LUMINANCE = $00020000; + + // DX10 + DDS_DIMENSION_TEXTURE2D = $00000003; + DDS_RESOURCE_MISC_TEXTURECUBE = $00000004; + +type + TwbBSArchive = class; + + TwbResourceDict = TDictionary; + + TBSArchiveType = (baNone, baTES3, baTES4, baFO3, baSSE, baFO4, baFO4dds, baSF, baSFdds); + TBSArchiveState = (stReading, stWriting); + TBSArchiveStates = set of TBSArchiveState; + TBSFileIterationProc = function(aArchive: Pointer; const aFileName: string; + aFileRecord: Pointer; aFolderRecord: Pointer; aData: Pointer): Boolean; stdcall; + + TDDSInfo = record Width, Height, MipMaps: Integer; end; + TBSFileDDSInfoProc = procedure(aArchive: Pointer; const aFileName: string; + var aInfo: TDDSInfo; aContext: Pointer); stdcall; + + TwbBSHeaderTES3 = packed record + HashOffset: Cardinal; + FileCount: Cardinal; + end; + + TwbBSFileTES3 = record + Hash: UInt64; + Size: Cardinal; + Offset: Cardinal; + Name: string; + end; + PwbBSFileTES3 = ^TwbBSFileTES3; + + TwbBSHeaderTES4 = packed record + FoldersOffset: Cardinal; + Flags: Cardinal; + FolderCount: Cardinal; + FileCount: Cardinal; + FolderNamesLength: Cardinal; + FileNamesLength: Cardinal; + FileFlags: Cardinal; + end; + + TwbBSFileTES4 = record + Hash: UInt64; + Size: Cardinal; + Offset: Int64; + Name: string; + PackingCompression: TPackingCompression; + function Compress(bsa: TwbBSArchive): Boolean; // compress when packing into a new archive + function Compressed(bsa: TwbBSArchive): Boolean; // compressed in existing archive + function RawSize: Cardinal; + end; + PwbBSFileTES4 = ^TwbBSFileTES4; + + TwbBSFolderTES4 = record + Hash: UInt64; + FileCount: Cardinal; + Unk32: Cardinal; + Offset: Int64; + Name: string; + Files: array of TwbBSFileTES4; + end; + PwbBSFolderTES4 = ^TwbBSFolderTES4; + + TwbBSHeaderFO4 = packed record + Magic: TMagic4; + FileCount: Cardinal; + FileTableOffset: Int64; + end; + + TwbBSHeaderSFv2 = packed record + Unknown1: Cardinal; + Unknown2: Cardinal; + end; + + TwbBSHeaderSFv3 = packed record + Unknown1: Cardinal; + Unknown2: Cardinal; + CompressionMethod: Cardinal; + end; + + TwbBSTexChunkRec = record + Size : Cardinal; + PackedSize : Cardinal; + Offset : Int64; + StartMip : Word; + EndMip : Word; + end; + PwbBSTexChunkRec = ^TwbBSTexChunkRec; + + TwbBSFileFO4 = record + NameHash: Cardinal; + Ext: TMagic4; + DirHash: Cardinal; + // GNRL archive format + Unknown: Cardinal; + Offset: Int64; + PackedSize: Cardinal; + Size: Cardinal; + // + // DX10 archive format + UnknownTex : Byte; + //ChunkHeaderSize: Word; + Height : Word; + Width : Word; + NumMips : Byte; + DXGIFormat : Byte; + CubeMaps : Word; + TexChunks : array of TwbBSTexChunkRec; + // + Name: string; + PackingCompression: TPackingCompression; + function DXGIFormatName: string; + function Compress(bsa: TwbBSArchive): Boolean; // compress when packing into a new archive + function Compressed(bsa: TwbBSArchive): Boolean; // compressed in existing archive + end; + PwbBSFileFO4 = ^TwbBSFileFO4; + + TPackedDataHash = TMD5Digest; + TPackedDataInfo = record + Size: Cardinal; + Hash: TPackedDataHash; + FileRecord: Pointer; + end; + PPackedDataInfo = ^TPackedDataInfo; + + TwbBSResultBuffer = packed record + size: Cardinal; + data: PByte; + end; + + TwbBSArchive = class + private + fStream: TwbBaseCachedFileStream; + fStates: TBSArchiveStates; + fType: TBSArchiveType; + fFileName: string; + fMagic: TMagic4; + fVersion: Cardinal; + fCompress: Boolean; + fCompressionType: TBGSCompressionType; + fShareData: Boolean; + fMultiThreaded: Boolean; + fDDSInfoProc: TBSFileDDSInfoProc; + fDDSInfoProcContext: Pointer; + + fHeaderTES3: TwbBSHeaderTES3; + fFilesTES3: array of TwbBSFileTES3; + + fHeaderTES4: TwbBSHeaderTES4; + fFoldersTES4: array of TwbBSFolderTES4; + + fHeaderFO4: TwbBSHeaderFO4; + fHeaderSFv2: TwbBSHeaderSFv2; + fHeaderSFv3: TwbBSHeaderSFv3; + fFilesFO4: array of TwbBSFileFO4; + fMaxChunkCount: Integer; + fSingleMipChunkX: Integer; + fSingleMipChunkY: Integer; + + fDataOffset: Int64; + + fPackedData: array of TPackedDataInfo; + fPackedDataCount: Integer; + + {$IF CompilerVersion >= 34.0} { Delphi 10.4 } + Sync: TLightweightMREW; + {$ELSE} + Sync: IReadWriteSync; + {$IFEND} + + function GetArchiveFormatName: string; + function GetFileCount: Cardinal; + function GetCreatedArchiveSize: Int64; + procedure SetArchiveFlags(aFlags: Cardinal); + procedure SetMultiThreaded(aValue: Boolean); + function FindFileRecordTES3(const aFileName: string; var aFileIdx: Integer): Boolean; + function FindFileRecordTES4(const aFileName: string; var aFolderIdx, aFileIdx: Integer): Boolean; + function FindFileRecordFO4(const aFileName: string; var aFileIdx: Integer): Boolean; + function GetDDSMipChunkNum(var aDDSInfo: TDDSInfo): Integer; + function CalcDataHash(aData: Pointer; aLen: Cardinal): TPackedDataHash; + function FindPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer): Boolean; + procedure AddPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer); + procedure PackData(aFileRecord: Pointer; const aFileName: string; + aDataHash: TPackedDataHash; aData: PByte; aSize: Integer; + aCompress: Boolean; aDoCompress: Boolean = False); + procedure CompressStream(aSrc, aDst: TStream); + procedure DecompressBuf(aSrc: Pointer; aSrcSize: Integer; aDst: Pointer; aDstSize: Integer); + + public + constructor Create; + destructor Destroy; override; + procedure LoadFromFile(const aFileName: string); + procedure CreateArchive(const aFileName: string; aType: TBSArchiveType; + aFilesList: TStringList = nil); + procedure Save; + procedure AddFileDisk(const aFilePath, aSourcePath: string); + procedure AddFileDiskRoot(const aRootDir, aSourcePath: string); + procedure AddFileData(const aFileName: string; const aSize: Cardinal; const aData: PByte); overload; + function FindFileRecord(const aFileName: string): Pointer; + function ExtractFileData(aFileRecord: Pointer): TwbBSResultBuffer; overload; + function ExtractFileData(const aFileName: string): TwbBSResultBuffer; overload; + procedure ReleaseFileData(fileDataResult: TwbBSResultBuffer); + procedure ExtractFile(const aFileName, aSaveAs: string); + procedure IterateFiles(aProc: TBSFileIterationProc; aData: Pointer = nil; + aSingleThreaded: Boolean = False); + function FileExists(const aFileName: string): Boolean; + procedure ResourceList(const aList: TStrings; aFolder: string = ''); + procedure ResourceDict(const aDict: TwbResourceDict; aFolder: string = ''); + //procedure IterateFolders(aProc: TBSFileIterationProc); + procedure Close; + + procedure SyncBeginWrite; + procedure SyncEndWrite; + + property FileName: string read fFileName; + property ArchiveType: TBSArchiveType read fType; + property Version: Cardinal read fVersion; + property FormatName: string read GetArchiveFormatName; + property FileCount: Cardinal read GetFileCount; + property CreatedArchiveSize: Int64 read GetCreatedArchiveSize; + property ArchiveFlags: Cardinal read fHeaderTES4.Flags write SetArchiveFlags; + property FileFlags: Cardinal read fHeaderTES4.FileFlags write fHeaderTES4.FileFlags; + property Compress: Boolean read fCompress write fCompress; + property ShareData: Boolean read fShareData write fShareData; + property MultiThreaded: Boolean read fMultiThreaded write SetMultiThreaded; + property DDSInfoProc: TBSFileDDSInfoProc read fDDSInfoProc write fDDSInfoProc; + property DDSInfoProcContext: Pointer read fDDSInfoProcContext write fDDSInfoProcContext; + end; + +const + cArchiveFormatNames: array[TBSArchiveType] of string = ( + 'None', + 'Morrowind', + 'Oblivion', + 'Skyrim LE, New Vegas, Fallout 3', + 'Skyrim SE, Skyrim AE', + 'Fallout 4', + 'Fallout 4 DDS', + 'Starfield', + 'Starfield DDS' + ); + + cArchiveTypeExtensions: array[TBSArchiveType] of string = ( + '.bsa', + '.bsa', + '.bsa', + '.bsa', + '.bsa', + '.ba2', + '.ba2', + '.ba2', + '.ba2' + ); + + cArchiveFlagNames: array [0..31] of string = ( + 'Include Directory Names', 'Include File Names', 'Compressed', + 'Retain Directory Names', 'Retain File Names', + 'Retain File Name Offsets', 'XBox 360 Archive', + 'Retain Strings During Startup', + 'Embed File Names', 'XMem Codec', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' + ); + + cFileFlagNames: array [0..31] of string = ( + 'Meshes', 'Textures', 'Menus', 'Sounds', + 'Voices', 'Shaders', 'Trees', 'Fonts', + 'Misc', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' + ); + +function SplitDirName(const aFileName: string; var Dir, Name: string): Integer; +function SplitNameExt(const aFileName: string; var Name, Ext: string; aNoExtDot: Boolean = False): Integer; +function CreateHashTES3(const aFileName: string): UInt64; +function CreateHashTES4(const aFileName: string): UInt64; overload; +function CreateHashTES4(const aName, aExt: string): UInt64; overload; +function CreateHashFO4(const aFileName: string): Cardinal; + +implementation + +uses + TypInfo, + zlibEx, + lz4io; + +const + MAGIC_TES3: TMagic4 = #0#1#0#0; + MAGIC_BSA : TMagic4 = 'BSA'#0; + MAGIC_BTDX: TMagic4 = 'BTDX'; + MAGIC_GNRL: TMagic4 = 'GNRL'; + MAGIC_DX10: TMagic4 = 'DX10'; + MAGIC_DDS : TMagic4 = 'DDS '; + MAGIC_DXT1: TMagic4 = 'DXT1'; + MAGIC_DXT3: TMagic4 = 'DXT3'; + MAGIC_DXT5: TMagic4 = 'DXT5'; + MAGIC_ATI1: TMagic4 = 'ATI1'; + MAGIC_ATI2: TMagic4 = 'ATI2'; + MAGIC_BC4S: TMagic4 = 'BC4S'; + MAGIC_BC4U: TMagic4 = 'BC4U'; + MAGIC_BC5S: TMagic4 = 'BC5S'; + MAGIC_BC5U: TMagic4 = 'BC5U'; + + iFileFO4Unknown = $00100100; + iFileFO4Tail = $BAADF00D; + + { https://github.com/jonwd7/bae/blob/master/src/bsa.h } + + // header versions + HEADER_VERSION_TES4 = $67; // Oblivion + HEADER_VERSION_FO3 = $68; // FO3, FNV, TES5 + HEADER_VERSION_SSE = $69; // SSE + HEADER_VERSION_FO4v1 = $01; // FO4 + HEADER_VERSION_SF2 = $02; // SF + HEADER_VERSION_SF3 = $03; // SF + HEADER_VERSION_FO4NGv7 = $07; // FO4NG + HEADER_VERSION_FO4NGv8 = $08; // FO4NG2 + + + // archive flags + ARCHIVE_PATHNAMES = $0001; // Whether the BSA has names for paths + ARCHIVE_FILENAMES = $0002; // Whether the BSA has names for files + ARCHIVE_COMPRESS = $0004; // Whether the files are compressed in archive (invert file's compression flag) + ARCHIVE_RETAINDIR = $0008; + ARCHIVE_RETAINNAME = $0010; + ARCHIVE_RETAINFOFF = $0020; + ARCHIVE_XBOX360 = $0040; + ARCHIVE_STARTUPSTR = $0080; + ARCHIVE_EMBEDNAME = $0100; // Whether the name is prefixed to the data + ARCHIVE_XMEM = $0200; + ARCHIVE_UNKNOWN10 = $0400; + + // file flags + FILE_NIF = $0001; + FILE_DDS = $0002; + FILE_XML = $0004; + FILE_WAV = $0008; + FILE_MP3 = $0010; + FILE_TXT = $0020; // TXT, HTML, BAT, SCC + FILE_SPT = $0040; + FILE_FNT = $0080; // TEX, FNT + FILE_MISC = $0100; // CTL and others + + FILE_SIZE_COMPRESS = $40000000; // Whether the file is compressed + + crc32table : array [0..255] of Cardinal = ( + $00000000, $77073096, $ee0e612c, $990951ba, $076dc419, $706af48f, + $e963a535, $9e6495a3, $0edb8832, $79dcb8a4, $e0d5e91e, $97d2d988, + $09b64c2b, $7eb17cbd, $e7b82d07, $90bf1d91, $1db71064, $6ab020f2, + $f3b97148, $84be41de, $1adad47d, $6ddde4eb, $f4d4b551, $83d385c7, + $136c9856, $646ba8c0, $fd62f97a, $8a65c9ec, $14015c4f, $63066cd9, + $fa0f3d63, $8d080df5, $3b6e20c8, $4c69105e, $d56041e4, $a2677172, + $3c03e4d1, $4b04d447, $d20d85fd, $a50ab56b, $35b5a8fa, $42b2986c, + $dbbbc9d6, $acbcf940, $32d86ce3, $45df5c75, $dcd60dcf, $abd13d59, + $26d930ac, $51de003a, $c8d75180, $bfd06116, $21b4f4b5, $56b3c423, + $cfba9599, $b8bda50f, $2802b89e, $5f058808, $c60cd9b2, $b10be924, + $2f6f7c87, $58684c11, $c1611dab, $b6662d3d, $76dc4190, $01db7106, + $98d220bc, $efd5102a, $71b18589, $06b6b51f, $9fbfe4a5, $e8b8d433, + $7807c9a2, $0f00f934, $9609a88e, $e10e9818, $7f6a0dbb, $086d3d2d, + $91646c97, $e6635c01, $6b6b51f4, $1c6c6162, $856530d8, $f262004e, + $6c0695ed, $1b01a57b, $8208f4c1, $f50fc457, $65b0d9c6, $12b7e950, + $8bbeb8ea, $fcb9887c, $62dd1ddf, $15da2d49, $8cd37cf3, $fbd44c65, + $4db26158, $3ab551ce, $a3bc0074, $d4bb30e2, $4adfa541, $3dd895d7, + $a4d1c46d, $d3d6f4fb, $4369e96a, $346ed9fc, $ad678846, $da60b8d0, + $44042d73, $33031de5, $aa0a4c5f, $dd0d7cc9, $5005713c, $270241aa, + $be0b1010, $c90c2086, $5768b525, $206f85b3, $b966d409, $ce61e49f, + $5edef90e, $29d9c998, $b0d09822, $c7d7a8b4, $59b33d17, $2eb40d81, + $b7bd5c3b, $c0ba6cad, $edb88320, $9abfb3b6, $03b6e20c, $74b1d29a, + $ead54739, $9dd277af, $04db2615, $73dc1683, $e3630b12, $94643b84, + $0d6d6a3e, $7a6a5aa8, $e40ecf0b, $9309ff9d, $0a00ae27, $7d079eb1, + $f00f9344, $8708a3d2, $1e01f268, $6906c2fe, $f762575d, $806567cb, + $196c3671, $6e6b06e7, $fed41b76, $89d32be0, $10da7a5a, $67dd4acc, + $f9b9df6f, $8ebeeff9, $17b7be43, $60b08ed5, $d6d6a3e8, $a1d1937e, + $38d8c2c4, $4fdff252, $d1bb67f1, $a6bc5767, $3fb506dd, $48b2364b, + $d80d2bda, $af0a1b4c, $36034af6, $41047a60, $df60efc3, $a867df55, + $316e8eef, $4669be79, $cb61b38c, $bc66831a, $256fd2a0, $5268e236, + $cc0c7795, $bb0b4703, $220216b9, $5505262f, $c5ba3bbe, $b2bd0b28, + $2bb45a92, $5cb36a04, $c2d7ffa7, $b5d0cf31, $2cd99e8b, $5bdeae1d, + $9b64c2b0, $ec63f226, $756aa39c, $026d930a, $9c0906a9, $eb0e363f, + $72076785, $05005713, $95bf4a82, $e2b87a14, $7bb12bae, $0cb61b38, + $92d28e9b, $e5d5be0d, $7cdcefb7, $0bdbdf21, $86d3d2d4, $f1d4e242, + $68ddb3f8, $1fda836e, $81be16cd, $f6b9265b, $6fb077e1, $18b74777, + $88085ae6, $ff0f6a70, $66063bca, $11010b5c, $8f659eff, $f862ae69, + $616bffd3, $166ccf45, $a00ae278, $d70dd2ee, $4e048354, $3903b3c2, + $a7672661, $d06016f7, $4969474d, $3e6e77db, $aed16a4a, $d9d65adc, + $40df0b66, $37d83bf0, $a9bcae53, $debb9ec5, $47b2cf7f, $30b5ffe9, + $bdbdf21c, $cabac28a, $53b39330, $24b4a3a6, $bad03605, $cdd70693, + $54de5729, $23d967bf, $b3667a2e, $c4614ab8, $5d681b02, $2a6f2b94, + $b40bbe37, $c30c8ea1, $5a05df1b, $2d02ef8d + ); + +type + TPreallocatedMemoryStream = class(TCustomMemoryStream) + public + constructor Create(Ptr: Pointer; Size: Int64); + function Write(const Buffer; Count: Longint): Longint; override; + end; + +constructor TPreallocatedMemoryStream.Create(Ptr: Pointer; Size: Int64); +begin + inherited Create; + SetPointer(Ptr, Size); +end; + +function TPreallocatedMemoryStream.Write(const Buffer; Count: Integer): Longint; +begin + Result := Size-Position; + if Result > Count then + Result := Count; + System.Move(Buffer, Pointer(PByte(Memory) + Position)^, Result); + Seek(Result, soCurrent); +end; + +function Magic2Int(aMagic: TMagic4): Cardinal; inline; +begin + Result := PCardinal(@aMagic)^; +end; + +function Int2Magic(aInt: Cardinal): TMagic4; inline; +begin + Result := PMagic4(@aInt)^; +end; + +function String2Magic(const aStr: string): TMagic4; +begin + Result := #0#0#0#0; + if Length(aStr) > 0 then Result[0] := AnsiChar(aStr[1]); + if Length(aStr) > 1 then Result[1] := AnsiChar(aStr[2]); + if Length(aStr) > 2 then Result[2] := AnsiChar(aStr[3]); + if Length(aStr) > 3 then Result[3] := AnsiChar(aStr[4]); +end; + +function LowerByte(ch: AnsiChar): Byte; inline; +begin + case ch of + 'A'..'Z': + Result := Byte(Ord(ch) + Ord('a')-Ord('A')); + else + Result := Byte(ch); + end; +end; + +function LastCharPos(const s: string; const Chr: char): Integer; inline; +begin + for Result := Length(s) downto 1 do + if s[Result] = Chr then + Exit; + Result := 0; +end; + +function Str2MagicInt(const s: string): Cardinal; +var + i: integer; +begin + Result := 0; + for i := 1 to Length(s) do begin + PByte(PByte(@Result) + i-1)^ := LowerByte(AnsiChar(s[i])); + if i = 4 then Break; + end; +end; + +function SplitDirName(const aFileName: string; var Dir, Name: string): Integer; +begin + Result := LastCharPos(aFileName, '\'); + if Result = 0 then + Result := LastCharPos(aFileName, '/'); + if Result <> 0 then begin + Dir := Copy(aFileName, 1, Pred(Result)); + Name := Copy(aFileName, Succ(Result), Length(aFileName) - Result); + end + else begin + Dir := ''; + Name := aFileName; + end; +end; + +function SplitNameExt(const aFileName: string; var Name, Ext: string; aNoExtDot: Boolean = False): Integer; +begin + Result := LastCharPos(aFileName, '.'); + if Result <> 0 then begin + Name := Copy(aFileName, 1, Pred(Result)); + if aNoExtDot then + Inc(Result); + Ext := Copy(aFileName, Result, Length(aFileName) - Result + 1); + end + else begin + Name := aFileName; + Ext := ''; + end; +end; + +function CreateHashTES3(const aFileName: string): UInt64; +var + s: AnsiString; + i, l: integer; + sum, off, temp, n: Cardinal; +begin + s := AnsiString(aFileName); + l := Length(s) shr 1; + + sum := 0; off := 0; + for i := 1 to l do begin + temp := Cardinal(LowerByte(s[i])) shl (off and $1F); + sum := sum xor temp; + off := off + 8; + end; + Result := UInt64(sum) shl 32; + + sum := 0; off := 0; + for i := l + 1 to Length(s) do begin + temp := Cardinal(LowerByte(s[i])) shl (off and $1F); + sum := sum xor temp; + n := temp and $1F; + sum := (sum shr n) or (sum shl (32 - n)); + off := off + 8; + end; + Result := Result or sum; +end; + +function CreateHashTES4(const aName, aExt: string): UInt64; overload; +var + i, l: integer; + hash: Cardinal; + ext: array [0..3] of Byte; + s, e: AnsiString; +begin + Result := 0; + s := AnsiString(aName); + e := AnsiString(aExt); + + l := Length(s); + if l = 0 then + Exit; + + Result := LowerByte(s[l]); + if l > 2 then + Result := Result or (Cardinal(LowerByte(s[l-1])) shl 8); + Result := Result or (l shl 16); + Result := Result or (Cardinal(LowerByte(s[1])) shl 24); + + PCardinal(@ext)^ := 0; + for i := 1 to Length(e) do begin + ext[i-1] := LowerByte(e[i]); + if i = 4 then Break; + end; + + case PCardinal(@ext)^ of + $00666B2E: Result := Result or $80; // .kf + $66696E2E: Result := Result or $8000; // .nif + $7364642E: Result := Result or $8080; // .dds + $7661772E: Result := Result or $80000000; // .wav + end; + + hash := 0; + for i := 2 to l-2 do + hash := LowerByte(s[i]) + (hash shl 6) + (hash shl 16) - hash; + Result := Result + UInt64(hash) shl 32; + + hash := 0; + for i := 1 to Length(e) do + hash := LowerByte(e[i]) + (hash shl 6) + (hash shl 16) - hash; + Result := Result + UInt64(hash) shl 32; +end; + +function CreateHashTES4(const aFileName: string): UInt64; overload; +var + fname, fext: string; +begin + SplitNameExt(aFileName, fname, fext); + Result := CreateHashTES4(fname, fext); +end; + +function CreateHashFO4(const aFileName: string): Cardinal; +var + i: Integer; + s: AnsiString; + c: AnsiChar; +begin + Result := 0; + s := AnsiString(aFileName); + for i := 1 to Length(s) do begin + c := s[i]; + if Byte(c) > 127 then Continue; + if c = '/' then c := '\'; + Result := (Result shr 8) xor crc32table[(Result xor LowerByte(c)) and $FF]; + end; +end; + +function TwbBSFileTES4.Compress(bsa: TwbBSArchive): Boolean; +begin + case PackingCompression of + pcCompress : Result := True; + pcUncompress: Result := False; + else + Result := bsa.Compress; + end; +end; + +function TwbBSFileTES4.Compressed(bsa: TwbBSArchive): Boolean; +begin + Result := (bsa.ArchiveFlags and ARCHIVE_COMPRESS <> 0) xor (Size and FILE_SIZE_COMPRESS <> 0); +end; + +function TwbBSFileTES4.RawSize: Cardinal; +begin + Result := Size and not FILE_SIZE_COMPRESS; +end; + +function TwbBSFileFO4.DXGIFormatName: string; +begin + Result := GetEnumName(TypeInfo(TDXGI), Integer(DXGIFormat)); +end; + +function TwbBSFileFO4.Compress(bsa: TwbBSArchive): Boolean; +begin + case PackingCompression of + pcCompress : Result := True; + pcUncompress: Result := False; + else + Result := bsa.Compress; + end; +end; + +function TwbBSFileFO4.Compressed(bsa: TwbBSArchive): Boolean; +begin + if bsa.ArchiveType = baFO4 then + Result := PackedSize <> 0 + else + Result := (Length(TexChunks) <> 0) and (TexChunks[0].PackedSize <> 0); +end; + + +{ TwbBSArchive } + +constructor TwbBSArchive.Create; +begin + fType := baNone; + fMaxChunkCount := 4; + fSingleMipChunkX := 512; + fSingleMipChunkY := 512; +end; + +destructor TwbBSArchive.Destroy; +begin + if fStates * [stReading, stWriting] <> [] then + Close; +end; + +function TwbBSArchive.GetArchiveFormatName: string; +begin + Result := cArchiveFormatNames[fType]; +end; + +function TwbBSArchive.GetFileCount: Cardinal; +begin + case fType of + baTES3: + Result := fHeaderTES3.FileCount; + baTES4, baFO3, baSSE: + Result := fHeaderTES4.FileCount; + baFO4, baFO4dds, baSF, baSFdds: + Result := fHeaderFO4.FileCount; + else + Result := 0; + end; +end; + +procedure TwbBSArchive.SetArchiveFlags(aFlags: Cardinal); +begin + if not (fType in [baTES4, baFO3, baSSE]) then + raise Exception.Create('Archive flags are not supported for this archive type'); + + fHeaderTES4.Flags := aFlags; + + // force compression flag if needed + if fCompress then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_COMPRESS; +end; + +procedure TwbBSArchive.SetMultiThreaded(aValue: Boolean); +begin + fMultiThreaded := aValue; + {$IF CompilerVersion < 34.0} + if aValue and not Assigned(Sync) then + Sync := TReadWriteSync.Create; + {$IFEND} +end; + +function TwbBSArchive.FindFileRecordTES3(const aFileName: string; var aFileIdx: Integer): Boolean; +var + h: UInt64; + i: integer; +begin + h := CreateHashTES3(aFileName); + + Result := False; + for i := Low(fFilesTES3) to High(fFilesTES3) do + if fFilesTES3[i].Hash = h then begin + aFileIdx := i; + Result := True; + Exit; + end; +end; + +function TwbBSArchive.FindFileRecordTES4(const aFileName: string; var aFolderIdx, aFileIdx: Integer): Boolean; +var + fdir, fname, name, ext: string; + h: UInt64; + i, j: integer; +begin + SplitDirName(aFileName, fdir, fname); + Result := False; + h := CreateHashTES4(fdir, ''); + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + if h <> fFoldersTES4[i].Hash then + // since table is sorted by hash, we can abort when our hash is lesser + if h < fFoldersTES4[i].Hash then + Exit + else + Continue; + + SplitNameExt(fname, name, ext); + h := CreateHashTES4(name, ext); + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do begin + if h <> fFoldersTES4[i].Files[j].Hash then + if h < fFoldersTES4[i].Files[j].Hash then + Exit + else + Continue; + + Result := True; + aFolderIdx := i; + aFileIdx := j; + Exit; + end; + end; +end; + +function TwbBSArchive.FindFileRecordFO4(const aFileName: string; var aFileIdx: Integer): Boolean; +var + fdir, fname, name, ext: string; + hdir, hfile: Cardinal; + hext: TMagic4; + i: integer; +begin + SplitDirName(aFileName, fdir, fname); + SplitNameExt(fname, name, ext, True); + hdir := CreateHashFO4(fdir); + hfile := CreateHashFO4(name); + hext := String2Magic(LowerCase(ext)); + + Result := False; + for i := Low(fFilesFO4) to High(fFilesFO4) do + if (fFilesFO4[i].DirHash = hdir) and (fFilesFO4[i].NameHash = hfile) and (fFilesFO4[i].Ext = hext) then begin + aFileIdx := i; + Result := True; + Exit; + end; +end; + +function TwbBSArchive.FindFileRecord(const aFileName: string): Pointer; +var + i, j: integer; +begin + Result := nil; + + case fType of + baTES3: + if FindFileRecordTES3(aFileName, i) then Result := @fFilesTES3[i]; + baTES4, baFO3, baSSE: + if FindFileRecordTES4(aFileName, i, j) then Result := @fFoldersTES4[i].Files[j]; + baFO4, baFO4dds, baSF, baSFdds: + if FindFileRecordFO4(aFileName, i) then Result := @fFilesFO4[i]; + end; +end; + +function TwbBSArchive.GetDDSMipChunkNum(var aDDSInfo: TDDSInfo): Integer; +var + w, h: Integer; +begin + w := aDDSInfo.Width; + h := aDDSInfo.Height; + Result := 1; + while (Result < aDDSInfo.MipMaps) and + (Result < fMaxChunkCount) and + (w >= fSingleMipChunkX) and + (h >= fSingleMipChunkY) + do begin + Inc(Result); + w := w div 2; + h := h div 2; + end; +end; + +function TwbBSArchive.CalcDataHash(aData: Pointer; aLen: Cardinal): TPackedDataHash; +var + fMD5: TMD5Alg; +begin + fMD5.Init(@fMD5); + fMD5.Update(@fMD5, aData, aLen); + fMD5.Done(@fMD5, @Result); +end; + +function TwbBSArchive.FindPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer): Boolean; +var + i: Integer; +begin + Result := False; + + if not fShareData then + Exit; + + for i := 0 to Pred(fPackedDataCount) do + if (aSize = fPackedData[i].Size) and CompareMem(@aHash, @fPackedData[i].Hash, SizeOf(aHash)) then begin + case fType of + baTES3: begin + PwbBSFileTES3(aFileRecord).Size := PwbBSFileTES3(fPackedData[i].FileRecord).Size; + PwbBSFileTES3(aFileRecord).Offset := PwbBSFileTES3(fPackedData[i].FileRecord).Offset; + end; + baTES4, baFO3, baSSE: begin + PwbBSFileTES4(aFileRecord).Size := PwbBSFileTES4(fPackedData[i].FileRecord).Size; + PwbBSFileTES4(aFileRecord).Offset := PwbBSFileTES4(fPackedData[i].FileRecord).Offset; + end; + baFO4, baSF: begin + PwbBSFileFO4(aFileRecord).Size := PwbBSFileFO4(fPackedData[i].FileRecord).Size; + PwbBSFileFO4(aFileRecord).PackedSize := PwbBSFileFO4(fPackedData[i].FileRecord).PackedSize; + PwbBSFileFO4(aFileRecord).Offset := PwbBSFileFO4(fPackedData[i].FileRecord).Offset; + end; + baFO4dds, baSFdds: begin + PwbBSTexChunkRec(aFileRecord).Size := PwbBSTexChunkRec(fPackedData[i].FileRecord).Size; + PwbBSTexChunkRec(aFileRecord).PackedSize := PwbBSTexChunkRec(fPackedData[i].FileRecord).PackedSize; + PwbBSTexChunkRec(aFileRecord).Offset := PwbBSTexChunkRec(fPackedData[i].FileRecord).Offset; + end; + end; + Result := True; + Exit; + end; +end; + +procedure TwbBSArchive.AddPackedData(aSize: Cardinal; aHash: TPackedDataHash; aFileRecord: Pointer); +begin + if not fShareData then + Exit; + + if fPackedDataCount = Length(fPackedData) then + if Length(fPackedData) = 0 then + SetLength(fPackedData, 2048) + else + SetLength(fPackedData, Length(fPackedData) * 2); + + fPackedData[fPackedDataCount].Size := aSize; + fPackedData[fPackedDataCount].Hash := aHash; + fPackedData[fPackedDataCount].FileRecord := aFileRecord; + Inc(fPackedDataCount); +end; + +procedure TwbBSArchive.LoadFromFile(const aFileName: string); +var + i, j: Integer; +begin + if fStates * [stReading, stWriting] <> [] then + Close; + + fStream := TwbReadOnlyCachedFileStream.Create(aFileName, fmOpenRead or fmShareDenyWrite); + + // magic + fMagic := Int2Magic(fStream.ReadCardinal); + if fMagic = MAGIC_TES3 then fType := baTES3 else + if fMagic = MAGIC_BSA then fType := baTES4 else + if fMagic = MAGIC_BTDX then fType := baFO4 else + raise Exception.Create('Unknown archive format'); + + // archive version except Morrowind + if fType <> baTES3 then begin + fVersion := fStream.ReadCardinal; + fCompressionType := ctZlib; // default compression type + case fVersion of + HEADER_VERSION_TES4: + fType := baTES4; + HEADER_VERSION_FO3 : + fType := baFO3; + HEADER_VERSION_SSE : + fType := baSSE; + HEADER_VERSION_FO4v1, + HEADER_VERSION_FO4NGv7, + HEADER_VERSION_FO4NGv8 : + fType := baFO4; + HEADER_VERSION_SF2, + HEADER_VERSION_SF3: + fType := baSF; + else + raise Exception.Create('Unknown archive version 0x' + IntToHex(fVersion, 8)); + end; + end; + + case fType of + //-------------------------------------------------- + // Morrowind + baTES3: begin + // read header + fStream.ReadBuffer(fHeaderTES3, SizeOf(fHeaderTES3)); + SetLength(fFilesTES3, fHeaderTES3.FileCount); + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fFilesTES3[i].Size := fStream.ReadCardinal; + fFilesTES3[i].Offset := fStream.ReadCardinal; + end; + // skip name offsets + fStream.Position := fStream.Position + 4 * fHeaderTES3.FileCount; + // read names + for i := Low(fFilesTES3) to High(fFilesTES3) do + fFilesTES3[i].Name := fStream.ReadStringTerm; + // read hashes + for i := Low(fFilesTES3) to High(fFilesTES3) do + fFilesTES3[i].Hash := fStream.ReadUInt64; + // remember binary data offset since stored files offsets are relative + fDataOffset := fStream.Position; + end; + + //-------------------------------------------------- + // Fallout 4, Starfield + baFO4, baSF: begin + // read header + fStream.ReadBuffer(fHeaderFO4, SizeOf(fHeaderFO4)); + // SF header + if fType = baSF then + if fVersion = HEADER_VERSION_SF2 then + fStream.ReadBuffer(fHeaderSFv2, SizeOf(fHeaderSFv2)) + else begin + fStream.ReadBuffer(fHeaderSFv3, SizeOf(fHeaderSFv3)); + if fHeaderSFv3.CompressionMethod = 3 then + fCompressionType := ctLZ4Block; + end; + + // read GNRL files + if fHeaderFO4.Magic = MAGIC_GNRL then begin + SetLength(fFilesFO4, fHeaderFO4.FileCount); + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fFilesFO4[i].NameHash := fStream.ReadCardinal; + fFilesFO4[i].Ext := Int2Magic(fStream.ReadCardinal); + fFilesFO4[i].DirHash := fStream.ReadCardinal; + fFilesFO4[i].Unknown := fStream.ReadCardinal; + fFilesFO4[i].Offset := fStream.ReadInt64; + fFilesFO4[i].PackedSize := fStream.ReadCardinal; + fFilesFO4[i].Size := fStream.ReadCardinal; + fStream.ReadCardinal; // BAADF00D + end; + end + // read DX10 textures + else if fHeaderFO4.Magic = MAGIC_DX10 then begin + if fType = baFO4 then + fType := baFO4dds + else if fType = baSF then + fType := baSFdds; + SetLength(fFilesFO4, fHeaderFO4.FileCount); + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fFilesFO4[i].NameHash := fStream.ReadCardinal; + fFilesFO4[i].Ext := Int2Magic(fStream.ReadCardinal); + fFilesFO4[i].DirHash := fStream.ReadCardinal; + fFilesFO4[i].UnknownTex := fStream.ReadByte; + SetLength(fFilesFO4[i].TexChunks, fStream.ReadByte); + fStream.ReadWord; // skip chunkHeaderSize, always 24 + //fFilesFO4[i].ChunkHeaderSize := fStream.ReadWord; + fFilesFO4[i].Height := fStream.ReadWord; + fFilesFO4[i].Width := fStream.ReadWord; + fFilesFO4[i].NumMips := fStream.ReadByte; + fFilesFO4[i].DXGIFormat := fStream.ReadByte; + fFilesFO4[i].CubeMaps := fStream.ReadWord; + for j := Low(fFilesFO4[i].TexChunks) to High(fFilesFO4[i].TexChunks) do + with fFilesFO4[i].TexChunks[j] do begin + Offset := fStream.ReadInt64; + PackedSize := fStream.ReadCardinal; + Size := fStream.ReadCardinal; + StartMip := fStream.ReadWord; + EndMip := fStream.ReadWord; + fStream.ReadCardinal; // skip BAADF00D + end; + end; + end + else + raise Exception.Create('Unknown BA2 archive type'); + + // read file names + fStream.Position := fHeaderFO4.FileTableOffset; + for i := Low(fFilesFO4) to High(fFilesFO4) do + fFilesFO4[i].Name := StringReplace(fStream.ReadStringLen16, '/', '\', [rfReplaceAll]); + end; + + //-------------------------------------------------- + // Oblivion, Fallout 3, New Vegas, Skyrim, Skyrim SE + baTES4, baFO3, baSSE: begin + if fType = baSSE then + fCompressionType := ctLZ4Frame; + + // read header + fStream.ReadBuffer(fHeaderTES4, SizeOf(fHeaderTES4)); + fStream.Position := fHeaderTES4.FoldersOffset; + + // read folder records + SetLength(fFoldersTES4, fHeaderTES4.FolderCount); + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fFoldersTES4[i].Hash := fStream.ReadUInt64; + fFoldersTES4[i].FileCount := fStream.ReadCardinal; + if fType = baSSE then begin + fFoldersTES4[i].Unk32 := fStream.ReadCardinal; + fFoldersTES4[i].Offset := fStream.ReadInt64; + end else + fFoldersTES4[i].Offset := fStream.ReadCardinal; + end; + + // read folder names and file records + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fFoldersTES4[i].Name := fStream.ReadStringLen; + SetLength(fFoldersTES4[i].Files, fFoldersTES4[i].FileCount); + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do begin + fFoldersTES4[i].Files[j].Hash := fStream.ReadUInt64; + fFoldersTES4[i].Files[j].Size := fStream.ReadCardinal; + fFoldersTES4[i].Files[j].Offset := fStream.ReadCardinal; + end; + end; + + // read file names + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + fFoldersTES4[i].Files[j].Name := fStream.ReadStringTerm; + end; + + end; + + fFileName := aFileName; + Include(fStates, stReading); +end; + + +type + THashPair = record DirHash, FileHash: UInt64; pc: TPackingCompression end; + PHashPair = ^THashPair; + +function HashPairSort(List: TStringList; Index1, Index2: Integer): Integer; +var + h1, h2: PHashPair; +begin + h1 := PHashPair(List.Objects[Index1]); + h2 := PHashPair(List.Objects[Index2]); + if h1.DirHash < h2.DirHash then + Result := -1 + else if h1.DirHash > h2.DirHash then + Result := 1 + else if h1.FileHash < h2.FileHash then + Result := -1 + else if h1.FileHash > h2.FileHash then + Result := 1 + else + Result := 0; +end; + +function AlphabeticalSort(List: TStringList; Index1, Index2: Integer): Integer; +begin + Result := CompareStr(List[Index1], List[Index2]); +end; + +procedure TwbBSArchive.CreateArchive(const aFileName: string; aType: TBSArchiveType; + aFilesList: TStringList = nil); +var + HashPairs: array of THashPair; + h: PHashPair; + hdir: UInt64; + s, fdir, fname, fext, name: string; + i, len, folderidx, fileidx: Integer; + Buffer: TBytes; + ddsinfo: TDDSInfo; +begin + if stReading in fStates then + Close; + + if stWriting in fStates then + raise Exception.Create('Archive is already being created'); + + case aType of + baTES3: begin + fMagic := MAGIC_TES3; + end; + baTES4: begin + fVersion := HEADER_VERSION_TES4; + fMagic := MAGIC_BSA; + fHeaderTES4.Flags := ARCHIVE_PATHNAMES or ARCHIVE_FILENAMES or ARCHIVE_EMBEDNAME or ARCHIVE_XMEM or ARCHIVE_UNKNOWN10; + fHeaderTES4.FileFlags := 0; + fHeaderTES4.FoldersOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4); + fCompressionType := ctZlib; + end; + baFO3: begin + fVersion := HEADER_VERSION_FO3; + fMagic := MAGIC_BSA; + fHeaderTES4.Flags := ARCHIVE_PATHNAMES or ARCHIVE_FILENAMES; + fHeaderTES4.FileFlags := 0; + fHeaderTES4.FoldersOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4); + fCompressionType := ctZlib; + end; + baSSE: begin + fVersion := HEADER_VERSION_SSE; + fMagic := MAGIC_BSA; + fHeaderTES4.Flags := ARCHIVE_PATHNAMES or ARCHIVE_FILENAMES; + fHeaderTES4.FileFlags := 0; + fHeaderTES4.FoldersOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4); + fCompressionType := ctLZ4Frame; + end; + baFO4: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_GNRL; + fVersion := HEADER_VERSION_FO4v1; + fCompressionType := ctZlib; + end; + baFO4dds: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_DX10; + fVersion := HEADER_VERSION_FO4v1; + fCompressionType := ctZlib; + end; + baSF: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_GNRL; + fVersion := HEADER_VERSION_SF2; + fCompressionType := ctZlib; + end; + baSFdds: begin + fMagic := MAGIC_BTDX; + fHeaderFO4.Magic := MAGIC_DX10; + fVersion := HEADER_VERSION_SF3; + fCompressionType := ctLZ4Block; + end; + else + raise Exception.Create('Unsupported archive type'); + end; + + fType := aType; + + if fType in [baTES3] then begin + if not Assigned(aFilesList) or (aFilesList.Count = 0) then + raise Exception.Create('Archive requires predefined files list'); + + // sort files by hashes + SetLength(HashPairs, aFilesList.Count); + for i := 0 to Pred(aFilesList.Count) do begin + h := @HashPairs[i]; + h.FileHash := CreateHashTES3(aFilesList[i]); + aFilesList.Objects[i] := Pointer(h); + end; + aFilesList.CustomSort(HashPairSort); + + // create file records and calculate total names length + SetLength(fFilesTES3, aFilesList.Count); + len := 0; + for i := 0 to Pred(aFilesList.Count) do begin + fFilesTES3[i].Hash := PHashPair(aFilesList.Objects[i]).FileHash; + fFilesTES3[i].Name := LowerCase(aFilesList[i]); + Inc(len, Length(fFilesTES3[i].Name) + 1); // include terminator + end; + + // offset to hash table + fDataOffset := SizeOf(fMagic) + SizeOf(fHeaderTES3) + + 8 * Length(fFilesTES3) + // File sizes/offsets + 4 * Length(fFilesTES3) + // Archive directory/name offsets + len; // Filename records + + // stored as minus 12 (for header size) + fHeaderTES3.HashOffset := fDataOffset - 12; + fHeaderTES3.FileCount := aFilesList.Count; + + // offset to files data + fDataOffset := fDataOffset + 8 * Length(fFilesTES3); // Hash table + + // files are stored alphabetically in the data section in vanilla archives + // not really needed but whatever + aFilesList.CustomSort(AlphabeticalSort); + end + + else if fType in [baTES4, baFO3, baSSE] then begin + if not Assigned(aFilesList) or (aFilesList.Count = 0) then + raise Exception.Create('Archive requires predefined files list'); + + fHeaderTES4.FolderNamesLength := 0; + fHeaderTES4.FileNamesLength := 0; + + // dirs and files must be sorted by their hashes + SetLength(HashPairs, aFilesList.Count); + for i := 0 to Pred(aFilesList.Count) do begin + h := @HashPairs[i]; + if SplitDirName(aFilesList[i], fdir, fname) = 0 then + raise Exception.Create('File is missing the folder part: ' + aFilesList[i]); + + // calculate hashes + h.DirHash := CreateHashTES4(fdir, ''); + SplitNameExt(fname, s, fext); + h.FileHash := CreateHashTES4(s, fext); + h.pc := TPackingCompression(aFilesList.Objects[i]); + aFilesList.Objects[i] := Pointer(h); + + fext := LowerCase(fext); + with fHeaderTES4 do begin + // determine file flags + if fdir.StartsWith('meshes\', True) then FileFlags := FileFlags or FILE_NIF else + if fdir.StartsWith('textures\', True) then FileFlags := FileFlags or FILE_DDS else + if fdir.StartsWith('sound\', True) then FileFlags := FileFlags or FILE_WAV or FILE_MP3 else + if fext = '.nif' then FileFlags := FileFlags or FILE_NIF else + if fext = '.lod' then FileFlags := FileFlags or FILE_NIF else + if fext = '.bto' then FileFlags := FileFlags or FILE_NIF else + if fext = '.btr' then FileFlags := FileFlags or FILE_NIF else + if fext = '.btt' then FileFlags := FileFlags or FILE_NIF else + if fext = '.dtl' then FileFlags := FileFlags or FILE_NIF else + if fext = '.kf' then FileFlags := FileFlags or FILE_NIF else + if fext = '.kfm' then FileFlags := FileFlags or FILE_NIF else + if fext = '.hkx' then FileFlags := FileFlags or FILE_NIF else + if fext = '.dds' then FileFlags := FileFlags or FILE_DDS else + if fext = '.xml' then FileFlags := FileFlags or FILE_XML or FILE_MISC else + if fext = '.wav' then FileFlags := FileFlags or FILE_WAV else + if fext = '.fuz' then FileFlags := FileFlags or FILE_WAV else + if fext = '.lip' then FileFlags := FileFlags or FILE_MP3 else + if fext = '.mp3' then FileFlags := FileFlags or FILE_MP3 else + if fext = '.ogg' then FileFlags := FileFlags or FILE_MP3 else + if fext = '.txt' then FileFlags := FileFlags or FILE_TXT else + if fext = '.htm' then FileFlags := FileFlags or FILE_TXT else + if fext = '.bat' then FileFlags := FileFlags or FILE_TXT else + if fext = '.scc' then FileFlags := FileFlags or FILE_TXT else + if fext = '.spt' then FileFlags := FileFlags or FILE_SPT else + if fext = '.fnt' then FileFlags := FileFlags or FILE_FNT else + if fext = '.tex' then FileFlags := FileFlags or FILE_FNT else + FileFlags := FileFlags or FILE_MISC; + + // determine archive flags + + // packed scripts can't be added to objects in the SSE CK if the archive was packed + // without the "RetainNames" flag (the scripts aren't shown in the script adding window) + if fext = '.pex' then ArchiveFlags := ArchiveFlags or ARCHIVE_RETAINNAME; + end; + end; + + // sort by hashes + aFilesList.CustomSort(HashPairSort); + + // create folder and file records + fHeaderTES4.FileCount := 0; + hdir := 0; + folderidx := -1; + fileidx := 0; + for i := 0 to Pred(aFilesList.Count) do begin + SplitDirName(aFilesList[i], fdir, fname); + h := Pointer(aFilesList.Objects[i]); + // new folder + if h.DirHash <> hdir then begin + Inc(folderidx); + fileidx := 0; + hdir := h.DirHash; + SetLength(fFoldersTES4, folderidx + 1); + fFoldersTES4[folderidx].Hash := h.DirHash; + fFoldersTES4[folderidx].Name := LowerCase(fdir); + // calc folder names length + Inc(fHeaderTES4.FolderNamesLength, Length(fdir) + 1); // + terminator only, length prefix is not counted + end; + SetLength(fFoldersTES4[folderidx].Files, fileidx + 1); + fFoldersTES4[folderidx].Files[fileidx].Hash := h.FileHash; + fFoldersTES4[folderidx].Files[fileidx].Name := LowerCase(fname); + fFoldersTES4[folderidx].Files[fileidx].PackingCompression := h.pc; + Inc(fileidx); + Inc(fFoldersTES4[folderidx].FileCount); + Inc(fHeaderTES4.FileCount); + // calculate file names length + Inc(fHeaderTES4.FileNamesLength, Length(fname) + 1); // + terminator + end; + fHeaderTES4.FolderCount := Length(fFoldersTES4); + + // calculate folders offsets + // at the end fDataOffset will hold the total size of header, folder and file records + // in other words the start of files data + fDataOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderTES4) + 16 * Length(fFoldersTES4); + // SSE folder record is 8 bytes larger + if fType = baSSE then + Inc(fDataOffset, 8 * Length(fFoldersTES4)); + // Offsets are stored including this value + Inc(fDataOffset, fHeaderTES4.FileNamesLength); + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fFoldersTES4[i].Offset := fDataOffset; + // add folder name length + Inc(fDataOffset, Length(fFoldersTES4[i].Name) + 2); // + length prefix + terminator + // add file records length + Inc(fDataOffset, 16 * Length(fFoldersTES4[i].Files)); + end; + + // final flags detection + + // misc file flag is not in Skyrim SE + if fType = baSSE then + fHeaderTES4.FileFlags := fHeaderTES4.FileFlags and not FILE_MISC; + + // embedded names in texture only archives + // except Skyrim SE: crashing engine bug if texture is uncompressed and file name is embedded + if (fHeaderTES4.FileFlags = FILE_DDS) and (fType <> baSSE) then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_EMBEDNAME; + + // startupstr flag in archives with meshes + if fHeaderTES4.FileFlags and FILE_NIF <> 0 then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_STARTUPSTR; + + // retainname flag in archives with sounds + if fHeaderTES4.FileFlags and FILE_WAV <> 0 then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_RETAINNAME; + + // txt, xml and fnt file flags are exclusive for Oblivion + if fType <> baTES4 then + fHeaderTES4.FileFlags := fHeaderTES4.FileFlags and not (FILE_XML or FILE_TXT or FILE_FNT); + + // set compression flag if needed + if fCompress then + fHeaderTES4.Flags := fHeaderTES4.Flags or ARCHIVE_COMPRESS; + end + + else if fType in [baFO4, baFO4dds, baSF, baSFdds] then begin + if not Assigned(aFilesList) or (aFilesList.Count = 0) then + raise Exception.Create('Archive requires predefined files list'); + + fHeaderFO4.FileCount := aFilesList.Count; + SetLength(fFilesFO4, aFilesList.Count); + for i := 0 to Pred(aFilesList.Count) do begin + if SplitDirName(aFilesList[i], fdir, fname) = 0 then + raise Exception.Create('File is missing the folder part: ' + aFileName); + + SplitNameExt(fname, name, fext, True); + // archive2.exe uses / + fFilesFO4[i].Name := StringReplace(aFilesList[i], '\', '/', [rfReplaceAll]); + fFilesFO4[i].DirHash := CreateHashFO4(fdir); + fFilesFO4[i].NameHash := CreateHashFO4(name); + fFilesFO4[i].Ext := String2Magic(LowerCase(fext)); + fFilesFO4[i].Unknown := iFileFO4Unknown; + fFilesFO4[i].PackingCompression := TPackingCompression(aFilesList.Objects[i]); + end; + + fDataOffset := SizeOf(fMagic) + SizeOf(fVersion) + SizeOf(fHeaderFO4); + if fType = baSF then + Inc(fDataOffset, SizeOf(fHeaderSFv2)) + else if fType = baSFdds then + Inc(fDataOffset, SizeOf(fHeaderSFv3)); + + // file records have fixed length in general archive + if fType in [baFO4, baSF] then + fDataOffset := fDataOffset + 36 * Length(fFilesFO4) + + // variable file record length depending on DDS chunks number + else if fType in [baFO4dds, baSFdds] then begin + if not Assigned(fDDSInfoProc) then + raise Exception.Create('DDS archive requires DDS file information callback'); + + for i := 0 to Pred(aFilesList.Count) do begin + fDDSInfoProc(Self, aFilesList[i], ddsinfo, Self.fDDSInfoProcContext); + fDataOffset := fDataOffset + 24 {size of file record} + 24 {size of each texchunk} * GetDDSMipChunkNum(ddsinfo); + end; + end; + + end; + + fStream := TwbWriteCachedFileStream.Create(aFileName, fmCreate); + fFileName := aFileName; + Include(fStates, stWriting); + + // reserve space for the header + SetLength(Buffer, fDataOffset); + fStream.Write(Buffer[0], Length(Buffer)); +end; + +procedure TwbBSArchive.Save; +var + i, j: integer; +begin + if not (stWriting in fStates) then + raise Exception.Create('Archive is not in writing mode'); + + case fType of + baTES3: begin + for i := Low(fFilesTES3) to High(fFilesTES3) do + if fFilesTES3[i].Offset = 0 then + raise Exception.Create('Archived file has no data: ' + fFilesTES3[i].Name); + + // write header + fStream.Position := 0; + // magic, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fHeaderTES3, SizeOf(fHeaderTES3)); + // file sizes/offsets + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fStream.WriteCardinal(fFilesTES3[i].Size); + fStream.WriteCardinal(fFilesTES3[i].Offset - fDataOffset); // offsets are relative + end; + // Archive directory/name offsets + j := 0; + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fStream.WriteCardinal(j); + Inc(j, Length(fFilesTES3[i].Name) + 1); // including terminator + end; + // Filename records + for i := Low(fFilesTES3) to High(fFilesTES3) do + fStream.WriteStringTerm(fFilesTES3[i].Name); + // Hash table + for i := Low(fFilesTES3) to High(fFilesTES3) do begin + fStream.WriteCardinal(fFilesTES3[i].Hash shr 32); + fStream.WriteCardinal(fFilesTES3[i].Hash and $FFFFFFFF); + end; + end; + + baTES4, baFO3, baSSE: begin + // check that all files from files table have saved data + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + if fFoldersTES4[i].Files[j].Offset = 0 then + raise Exception.Create('Archived file has no data: ' + fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name); + + // write header + fStream.Position := 0; + // magic, version, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fVersion, SizeOf(fVersion)); + fStream.Write(fHeaderTES4, SizeOf(fHeaderTES4)); + // folder records + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fStream.WriteUInt64(fFoldersTES4[i].Hash); + fStream.WriteCardinal(fFoldersTES4[i].FileCount); + if fType = baSSE then begin + fStream.WriteCardinal(fFoldersTES4[i].Unk32); + fStream.WriteInt64(fFoldersTES4[i].Offset); + end else + fStream.WriteCardinal(fFoldersTES4[i].Offset); + end; + // file records + for i := Low(fFoldersTES4) to High(fFoldersTES4) do begin + fStream.WriteStringLen(fFoldersTES4[i].Name); + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do begin + fStream.WriteUInt64(fFoldersTES4[i].Files[j].Hash); + fStream.WriteCardinal(fFoldersTES4[i].Files[j].Size); + fStream.WriteCardinal(fFoldersTES4[i].Files[j].Offset); + end; + end; + // file names + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + fStream.WriteStringTerm(fFoldersTES4[i].Files[j].Name); + end; + + baFO4, baSF: begin + for i := Low(fFilesFO4) to High(fFilesFO4) do + if fFilesFO4[i].Offset = 0 then + raise Exception.Create('Archived file has no data: ' + fFilesFO4[i].Name); + + // file names table + fHeaderFO4.FileTableOffset := fStream.Position; + for i := Low(fFilesFO4) to High(fFilesFO4) do + fStream.WriteStringLen16(fFilesFO4[i].Name); + // write header + fStream.Position := 0; + // magic, version, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fVersion, SizeOf(fVersion)); + fStream.Write(fHeaderFO4, SizeOf(fHeaderFO4)); + // additional SF header + if fType = baSF then begin + fHeaderSFv2.Unknown1 := 1; + fHeaderSFv2.Unknown2 := 0; + fStream.Write(fHeaderSFv2, SizeOf(fHeaderSFv2)); + end; + // file records + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fStream.WriteCardinal(fFilesFO4[i].NameHash); + fStream.WriteBuffer(fFilesFO4[i].Ext[0], SizeOf(fFilesFO4[i].Ext)); + fStream.WriteCardinal(fFilesFO4[i].DirHash); + fStream.WriteCardinal(fFilesFO4[i].Unknown); + fStream.WriteInt64(fFilesFO4[i].Offset); + fStream.WriteCardinal(fFilesFO4[i].PackedSize); + fStream.WriteCardinal(fFilesFO4[i].Size); + fStream.WriteCardinal(iFileFO4Tail); + end; + end; + + baFO4dds, baSFdds: begin + for i := Low(fFilesFO4) to High(fFilesFO4) do + if Length(fFilesFO4[i].TexChunks) = 0 then + raise Exception.Create('Archived file has no data: ' + fFilesFO4[i].Name); + + // file names table + fHeaderFO4.FileTableOffset := fStream.Position; + for i := Low(fFilesFO4) to High(fFilesFO4) do + fStream.WriteStringLen16(fFilesFO4[i].Name); + // write header + fStream.Position := 0; + // magic, version, header record + fStream.Write(fMagic, SizeOf(fMagic)); + fStream.Write(fVersion, SizeOf(fVersion)); + fStream.Write(fHeaderFO4, SizeOf(fHeaderFO4)); + // additional SF header + if fType = baSFdds then begin + fHeaderSFv3.Unknown1 := 1; + fHeaderSFv3.Unknown2 := 0; + fHeaderSFv3.CompressionMethod := 3; // lz4 + fStream.Write(fHeaderSFv3, SizeOf(fHeaderSFv3)); + end; + // file records + for i := Low(fFilesFO4) to High(fFilesFO4) do begin + fStream.WriteCardinal(fFilesFO4[i].NameHash); + fStream.WriteBuffer(fFilesFO4[i].Ext[0], SizeOf(fFilesFO4[i].Ext)); + fStream.WriteCardinal(fFilesFO4[i].DirHash); + fStream.WriteByte(fFilesFO4[i].UnknownTex); + fStream.WriteByte(Length(fFilesFO4[i].TexChunks)); + fStream.WriteWord(24); // fixed chunk header size + fStream.WriteWord(fFilesFO4[i].Height); + fStream.WriteWord(fFilesFO4[i].Width); + fStream.WriteByte(fFilesFO4[i].NumMips); + fStream.WriteByte(fFilesFO4[i].DXGIFormat); + fStream.WriteWord(fFilesFO4[i].CubeMaps); + for j := Low(fFilesFO4[i].TexChunks) to High(fFilesFO4[i].TexChunks) do + with fFilesFO4[i].TexChunks[j] do begin + fStream.WriteUInt64(Offset); + fStream.WriteCardinal(PackedSize); + fStream.WriteCardinal(Size); + fStream.WriteWord(StartMip); + fStream.WriteWord(EndMip); + fStream.WriteCardinal(iFileFO4Tail); + end; + end; + end; + + end; + + FreeAndNil(fStream); + Exclude(fStates, stWriting); + Close; +end; + +function TwbBSArchive.GetCreatedArchiveSize: Int64; +begin + if (stWriting in fStates) and Assigned(fStream) then + Result := fStream.Position + else + Result := 0; +end; + +procedure TwbBSArchive.AddFileDisk(const aFilePath, aSourcePath: string); +var + fname: string; + i: integer; + buffer: PByte; + stream: TFileStream; +begin + if not (stWriting in fStates) then + raise Exception.Create('Archive is not in writing mode'); + + stream := TFileStream.Create(aSourcePath, fmOpenRead + fmShareDenyNone); + try + // Modified: Make sure memory is zeroed when allocated + buffer := AllocMem(stream.Size); + try + stream.Read(buffer^, stream.Size); + AddFileData(aFilePath, stream.Size, buffer); + finally + if Assigned(buffer) then + FreeMem(buffer); + end; + finally + stream.Free; + end; +end; + +procedure TwbBSArchive.AddFileDiskRoot(const aRootDir, aSourcePath: string); +var + fname: string; + i: integer; +begin + i := Length(aRootDir); + if (i > 1) and (aRootDir[Length(aRootDir)] <> '\') then + Inc(i); + + fname := Copy(aSourcePath, i + 1, Length(aSourcePath)); + + AddFileDisk(fname, aSourcePath); +end; + +procedure TwbBSArchive.SyncBeginWrite; +begin + if fMultiThreaded then + Sync.BeginWrite; +end; + +procedure TwbBSArchive.SyncEndWrite; +begin + if fMultiThreaded then + Sync.EndWrite; +end; + +procedure TwbBSArchive.CompressStream(aSrc, aDst: TStream); +begin + case fCompressionType of + ctZlib: ZCompressStream(aSrc, aDst); + ctLZ4Frame: lz4CompressStream(aSrc, aDst); + ctLZ4Block: lz4BlockCompressStream(aSrc, aDst); + else + raise Exception.Create('Archive compression type is undefined'); + end; +end; + +procedure TwbBSArchive.DecompressBuf(aSrc: Pointer; aSrcSize: Integer; aDst: Pointer; aDstSize: Integer); +begin + case fCompressionType of + ctZlib: try + DecompressToUserBuf(aSrc, aSrcSize, aDst, aDstSize); + except + // ignore zlib's Buffer error since it happens in vanilla "Fallout - Misc.bsa" + // Bethesda probably used old buggy zlib version when packing it + on E: Exception do if E.Message <> 'Buffer error' then raise; + end; + ctLZ4Frame: lz4DecompressToUserBuf(aSrc, aSrcSize, aDst, aDstSize); + ctLZ4Block: lz4BlockDecompressToUserBuf(aSrc, aSrcSize, aDst, aDstSize); + end; +end; + +procedure TwbBSArchive.PackData(aFileRecord: Pointer; const aFileName: string; + aDataHash: TPackedDataHash; aData: PByte; aSize: Integer; + aCompress: Boolean; aDoCompress: Boolean = False); +var + zStream: TBytesStream; + msData: TPreallocatedMemoryStream; + DataSize: Integer; + Position: Int64; +begin + DataSize := aSize; + zStream := nil; + msData := nil; + + if FindPackedData(DataSize, aDataHash, aFileRecord) then + Exit; + + try + if aCompress then begin + // compressing in parallel when multithreaded + SyncEndWrite; + try + zStream := TBytesStream.Create; + msData := TPreallocatedMemoryStream.Create(aData, aSize); + CompressStream(msData, zStream); + + // leave as compressed if compression reduced the size + // by at least let's say 32 bytes + // or data is forced to be compressed + if aDoCompress or (zStream.Size + 32 < aSize) then begin + aData := @zStream.Bytes[0]; + aSize := zStream.Size; + end else + aCompress := False; + finally + SyncBeginWrite; + end; + end; + + // let's try to find existing data again if multithreaded + // maybe some other thread has written the same data while we've been busy compressing + // zStream exists if we've really spent time compressing + if fMultiThreaded and Assigned(zStream) then + if FindPackedData(DataSize, aDataHash, aFileRecord) then + Exit; + + Position := fStream.Position; + + // embedded name for Fallout 3/NV/Skyrim/Skyrim SE + if (fType in [baFO3, baSSE]) and (fHeaderTES4.Flags and ARCHIVE_EMBEDNAME <> 0) then + fStream.WriteStringLen(aFileName, False); + + // if compressed then write uncompressed size first for Oblivion/Fallout 3/NV/Skyrim/Skyrim SE + if (fType in [baTES4, baFO3, baSSE]) and aCompress then + fStream.WriteCardinal(DataSize); + + fStream.Write(aData^, aSize); + + // updating file record + case fType of + baTES3: with PwbBSFileTES3(aFileRecord)^ do begin + Offset := Position; + Size := DataSize; + end; + baTES4, baFO3, baSSE: with PwbBSFileTES4(aFileRecord)^ do begin + Offset := Position; + Size := fStream.Position - Offset; + // compress flag in Size inverts compression status from the header + // set it if archive's compression doesn't match file's compression + if Self.fCompress xor aCompress then + Size := Size or FILE_SIZE_COMPRESS; + end; + baFO4, baSF: with PwbBSFileFO4(aFileRecord)^ do begin + Offset := Position; + Size := DataSize; + if aCompress then + PackedSize := aSize; + end; + baFO4dds, baSFdds: with PwbBSTexChunkRec(aFileRecord)^ do begin + Offset := Position; + Size := DataSize; + if aCompress then + PackedSize := aSize; + end; + end; + + AddPackedData(DataSize, aDataHash, aFileRecord); + + finally + if Assigned(zStream) then + zStream.Free; + if Assigned(msData) then + msData.Free; + end; +end; + +procedure TwbBSArchive.AddFileData(const aFileName: string; const aSize: Cardinal; const aData: PByte); +var + i, j, Off, MipSize, BitsPerPixel: integer; + DataHash: TPackedDataHash; + DDSHeader: PDDSHeader; + DDSHeaderDX10: PDDSHeaderDX10; + DDSInfo: TDDSInfo; +begin + if not (stWriting in fStates) then + raise Exception.Create('Archive is not in writing mode'); + + // dds mipmaps have their own partial hash calculation down below + if fShareData and not (fType in [baFO4dds, baSFdds]) then + DataHash := CalcDataHash(@aData[0], aSize); + + SyncBeginWrite; + try + case fType of + baTES3: begin + if not FindFileRecordTES3(aFileName, i) then + raise Exception.Create('File not found in files table: ' + aFileName); + + PackData(@fFilesTES3[i], aFileName, DataHash, @aData[0], aSize, False); + end; + + baTES4, baFO3, baSSE: begin + if not FindFileRecordTES4(aFileName, i, j) then + raise Exception.Create('File not found in files table: ' + aFileName); + + PackData( + @fFoldersTES4[i].Files[j], fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name, + DataHash, @aData[0], aSize, fFoldersTES4[i].Files[j].Compress(Self) + ); + end; + + baFO4, baSF: begin + if not FindFileRecordFO4(aFileName, i) then + raise Exception.Create('File not found in files table: ' + aFileName); + + fFilesFO4[i].Offset := fStream.Position; + fFilesFO4[i].Size := aSize; + + PackData( + @fFilesFO4[i], fFilesFO4[i].Name, + DataHash, @aData[0], aSize, fFilesFO4[i].Compress(Self) + ); + end; + + baFO4dds, baSFdds: begin + if not FindFileRecordFO4(aFileName, i) then + raise Exception.Create('File not found in files table: ' + aFileName); + + fFilesFO4[i].UnknownTex := 0; + // DDS file parameters + DDSHeader := @aData[0]; + Off := SizeOf(DDSHeader^); // offset to image data + fFilesFO4[i].Width := DDSHeader.dwWidth; + fFilesFO4[i].Height := DDSHeader.dwHeight; + fFilesFO4[i].NumMips := DDSHeader.dwMipMapCount; + // no mipmaps is equal to a single one + if fFilesFO4[i].NumMips = 0 then + fFilesFO4[i].NumMips := 1; + + // DXGI detection + if DDSHeader.ddspf.dwFourCC = MAGIC_DXT1 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC1_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_DXT3 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC2_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_DXT5 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC3_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_ATI1 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC4_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC4U then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC4_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC4S then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC4_SNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_ATI2 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC5_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC5U then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC5_UNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_BC5S then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_BC5_SNORM) + else if DDSHeader.ddspf.dwFourCC = MAGIC_DX10 then begin + DDSHeaderDX10 := @aData[Off]; + Off := Off + SizeOf(DDSHeaderDX10^); + fFilesFO4[i].DXGIFormat := Byte(DDSHeaderDX10.dxgiFormat); + end + else begin + if DDSHeader.ddspf.dwRGBBitCount = 32 then + if DDSHeader.ddspf.dwFlags and DDPF_ALPHAPIXELS = 0 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B8G8R8X8_UNORM) + else if DDSHeader.ddspf.dwRBitMask = $000000FF then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_R8G8B8A8_UNORM) + else + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B8G8R8A8_UNORM) + + else if DDSHeader.ddspf.dwRGBBitCount = 16 then + if (DDSHeader.ddspf.dwRBitMask = $F800) and (DDSHeader.ddspf.dwGBitMask = $07E0) and + (DDSHeader.ddspf.dwBBitMask = $001F) and (DDSHeader.ddspf.dwABitMask = $0000) then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B5G6R5_UNORM) + else if (DDSHeader.ddspf.dwRBitMask = $7C00) and (DDSHeader.ddspf.dwGBitMask = $03E0) and + (DDSHeader.ddspf.dwBBitMask = $001F) and (DDSHeader.ddspf.dwABitMask = $8000) then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_B5G5R5A1_UNORM) + else + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_R8G8_UNORM) + + else if DDSHeader.ddspf.dwRGBBitCount = 8 then + if DDSHeader.ddspf.dwFlags and DDPF_ALPHA <> 0 then + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_A8_UNORM) + else + fFilesFO4[i].DXGIFormat := Byte(DXGI_FORMAT_R8_UNORM) + + else + raise Exception.Create('Unsupported uncompressed DDS format'); + end; + + // MipMap size detection + case TDXGI(fFilesFO4[i].DXGIFormat) of + DXGI_FORMAT_BC1_UNORM, DXGI_FORMAT_BC1_UNORM_SRGB, + DXGI_FORMAT_BC4_UNORM, DXGI_FORMAT_BC4_SNORM: + BitsPerPixel := 4; + DXGI_FORMAT_BC2_UNORM, DXGI_FORMAT_BC2_UNORM_SRGB, + DXGI_FORMAT_BC3_UNORM, DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT_BC5_UNORM, DXGI_FORMAT_BC5_SNORM, + DXGI_FORMAT_BC6H_SF16, DXGI_FORMAT_BC6H_UF16, + DXGI_FORMAT_BC7_UNORM, DXGI_FORMAT_BC7_UNORM_SRGB, + DXGI_FORMAT_A8_UNORM, + DXGI_FORMAT_R8_SINT, DXGI_FORMAT_R8_SNORM, + DXGI_FORMAT_R8_UINT, DXGI_FORMAT_R8_UNORM: + BitsPerPixel := 8; + DXGI_FORMAT_B5G6R5_UNORM, DXGI_FORMAT_B5G5R5A1_UNORM, + DXGI_FORMAT_R8G8_SINT, DXGI_FORMAT_R8G8_UINT, DXGI_FORMAT_R8G8_UNORM: + BitsPerPixel := 16; + DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, + DXGI_FORMAT_B8G8R8X8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, + DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_SINT, + DXGI_FORMAT_R8G8B8A8_UINT, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + BitsPerPixel := 32; + else + raise Exception.Create('Unsupported DDS format'); + end; + MipSize := (fFilesFO4[i].Width * fFilesFO4[i].Height * BitsPerPixel) shr 3; + + // cubemaps detection + fFilesFO4[i].CubeMaps := $800; + if DDSHeader.dwCaps2 and DDSCAPS2_CUBEMAP <> 0 then + fFilesFO4[i].CubeMaps := fFilesFO4[i].CubeMaps or 1; + + // number of chunks to store in file record + DDSInfo.Width := fFilesFO4[i].Width; + DDSInfo.Height := fFilesFO4[i].Height; + DDSInfo.MipMaps := fFilesFO4[i].NumMips; + SetLength(fFilesFO4[i].TexChunks, GetDDSMipChunkNum(DDSInfo)); + + // storing chunks + for j := Low(fFilesFO4[i].TexChunks) to High(fFilesFO4[i].TexChunks) do begin + fFilesFO4[i].TexChunks[j].StartMip := j; + if j < High(fFilesFO4[i].TexChunks) then + fFilesFO4[i].TexChunks[j].EndMip := j + else begin + // last chunk stores all remaining mipmaps + fFilesFO4[i].TexChunks[j].EndMip := Pred(fFilesFO4[i].NumMips); + MipSize := Integer(aSize) - Off; + end; + + DataHash := CalcDataHash(@aData[Off], MipSize); + + PackData( + @fFilesFO4[i].TexChunks[j], fFilesFO4[i].Name, + DataHash, @aData[Off], MipSize, + fFilesFO4[i].Compress(Self), fFilesFO4[i].Compress(Self) // force compression + ); + Inc(Off, MipSize); + MipSize := MipSize div 4; + end; + end; + + end; + finally + SyncEndWrite; + end; +end; + +function TwbBSArchive.ExtractFileData(aFileRecord: Pointer): TwbBSResultBuffer; +var + FileTES3: PwbBSFileTES3; + FileTES4: PwbBSFileTES4; + FileFO4: PwbBSFileFO4; + DDSHeader: PDDSHeader; + DDSHeaderDX10: PDDSHeaderDX10; + i, size, TexSize: integer; + bCompressed: Boolean; + Buffer: TBytes; +begin + if not (stReading in fStates) then + raise Exception.Create('Archive is not loaded'); + + if aFileRecord = nil then + Exit; + + SyncBeginWrite; + try + case fType of + baTES3: begin + FileTES3 := aFileRecord; + fStream.Position := fDataOffset + FileTES3.Offset; + Result.size := FileTES3.Size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(FileTES3.Size); + end; + + baTES4, baFO3, baSSE: begin + FileTES4 := aFileRecord; + fStream.Position := FileTES4.Offset; + size := FileTES4.Size; + bCompressed := size and FILE_SIZE_COMPRESS <> 0; + if bCompressed then + size := size and not FILE_SIZE_COMPRESS; + if fHeaderTES4.Flags and ARCHIVE_COMPRESS <> 0 then + bCompressed := not bCompressed; + + // skip embedded file name + length prefix + if (fType in [baFO3, baSSE]) and (fHeaderTES4.Flags and ARCHIVE_EMBEDNAME <> 0) then + size := size - (Length(fStream.ReadStringLen(False)) + 1); + + if bCompressed then begin + // reading uncompressed size + Result.size := fStream.ReadCardinal; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + dec(size, SizeOf(Cardinal)); + if (Result.size > 0) and (size > 0) then begin + SetLength(Buffer, size); + fStream.ReadBuffer(Buffer[0], Length(Buffer)); + SyncEndWrite; + try + DecompressBuf(@Buffer[0], Length(Buffer), @Result.data[0], Result.size); + finally + SyncBeginWrite; + end; + end; + end + else begin + Result.size := size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + if size > 0 then + fStream.ReadBuffer(Result.data[0], Result.size); + end; + end; + + baFO4, baSF: begin + FileFO4 := aFileRecord; + fStream.Position := FileFO4.Offset; + if FileFO4.PackedSize <> 0 then begin + SetLength(Buffer, FileFO4.PackedSize); + fStream.ReadBuffer(Buffer[0], Length(Buffer)); + Result.size := FileFO4.Size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + SyncEndWrite; + try + DecompressBuf(@Buffer[0], Length(Buffer), @Result.data[0], Result.size); + finally + SyncBeginWrite; + end; + end + else begin + Result.size := FileFO4.Size; + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + fStream.ReadBuffer(Result.data[0], Result.size); + end; + end; + + baFO4dds, baSFdds: begin + FileFO4 := aFileRecord; + + TexSize := SizeOf(TDDSHeader); + for i := Low(FileFO4.TexChunks) to High(FileFO4.TexChunks) do + Inc(TexSize, FileFO4.TexChunks[i].Size); + Result.size := Texsize; + + // Modified: Make sure memory is zeroed when allocated + Result.data := AllocMem(Result.size); + + DDSHeader := @Result.data[0]; + DDSHeader.Magic := MAGIC_DDS; + DDSHeader.dwSize := SizeOf(TDDSHeader) - SizeOf(TMagic4); + DDSHeader.dwWidth := FileFO4.Width; + DDSHeader.dwHeight := FileFO4.Height; + DDSHeader.dwFlags := DDSD_CAPS or DDSD_PIXELFORMAT or + DDSD_WIDTH or DDSD_HEIGHT or DDSD_MIPMAPCOUNT; + DDSHeader.dwCaps := DDSCAPS_TEXTURE; + DDSHeader.dwMipMapCount := FileFO4.NumMips; + if DDSHeader.dwMipMapCount > 1 then + DDSHeader.dwCaps := DDSHeader.dwCaps or DDSCAPS_MIPMAP or DDSCAPS_COMPLEX; + DDSHeader.dwDepth := 1; + + DDSHeaderDX10 := @Result.data[SizeOf(TDDSHeader)]; + DDSHeaderDX10.resourceDimension := DDS_DIMENSION_TEXTURE2D; + DDSHeaderDX10.arraySize := 1; + + if FileFO4.CubeMaps = 2049 then begin + // Archive2.exe creates invalid textures like this + //DDSHeader.dwCaps := DDSHeader.dwCaps or DDSCAPS2_CUBEMAP or DDSCAPS_COMPLEX + // or DDSCAPS2_POSITIVEX or DDSCAPS2_NEGATIVEX + // or DDSCAPS2_POSITIVEY or DDSCAPS2_NEGATIVEY + // or DDSCAPS2_POSITIVEZ or DDSCAPS2_NEGATIVEZ; + // This is the correct way + DDSHeader.dwCaps := DDSHeader.dwCaps or DDSCAPS_COMPLEX; + DDSHeader.dwCaps2 := DDSCAPS2_CUBEMAP + or DDSCAPS2_POSITIVEX or DDSCAPS2_NEGATIVEX + or DDSCAPS2_POSITIVEY or DDSCAPS2_NEGATIVEY + or DDSCAPS2_POSITIVEZ or DDSCAPS2_NEGATIVEZ; + DDSHeaderDX10.miscFlags := DDS_RESOURCE_MISC_TEXTURECUBE; + end; + DDSHeader.ddspf.dwSize := SizeOf(DDSHeader.ddspf); + + case TDXGI(FileFO4.DXGIFormat) of + DXGI_FORMAT_BC1_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DXT1; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC2_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DXT3; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC3_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DXT5; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC4_SNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC4S; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC4_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC4U; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC5_SNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC5S; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC5_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_BC5U; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_BC1_UNORM_SRGB: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height div 2; + end; + DXGI_FORMAT_BC2_UNORM_SRGB, DXGI_FORMAT_BC3_UNORM_SRGB, + DXGI_FORMAT_BC6H_UF16, DXGI_FORMAT_BC6H_SF16, + DXGI_FORMAT_BC7_UNORM, DXGI_FORMAT_BC7_UNORM_SRGB: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_LINEARSIZE; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * FileFO4.Height; + end; + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, + DXGI_FORMAT_R8G8B8A8_SINT, DXGI_FORMAT_R8G8B8A8_UINT, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_R8G8_SINT, DXGI_FORMAT_R8G8_UINT: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_R8_SINT, DXGI_FORMAT_R8_SNORM, DXGI_FORMAT_R8_UINT: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_FOURCC; + DDSHeader.ddspf.dwFourCC := MAGIC_DX10; + DDSHeaderDX10.dxgiFormat := Integer(FileFO4.DXGIFormat); + DDSHeader.dwPitchOrLinearSize := FileFO4.Width; + end; + DXGI_FORMAT_R8G8B8A8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB or DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 32; + DDSHeader.ddspf.dwRBitMask := $000000FF; + DDSHeader.ddspf.dwGBitMask := $0000FF00; + DDSHeader.ddspf.dwBBitMask := $00FF0000; + DDSHeader.ddspf.dwABitMask := $FF000000; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_B8G8R8A8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB or DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 32; + DDSHeader.ddspf.dwRBitMask := $00FF0000; + DDSHeader.ddspf.dwGBitMask := $0000FF00; + DDSHeader.ddspf.dwBBitMask := $000000FF; + DDSHeader.ddspf.dwABitMask := $FF000000; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_B8G8R8X8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB; + DDSHeader.ddspf.dwRGBBitCount := 32; + DDSHeader.ddspf.dwRBitMask := $00FF0000; + DDSHeader.ddspf.dwGBitMask := $0000FF00; + DDSHeader.ddspf.dwBBitMask := $000000FF; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 4; + end; + DXGI_FORMAT_B5G6R5_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB; + DDSHeader.ddspf.dwRGBBitCount := 16; + DDSHeader.ddspf.dwRBitMask := $0000F800; + DDSHeader.ddspf.dwGBitMask := $000007E0; + DDSHeader.ddspf.dwBBitMask := $0000001F; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_B5G5R5A1_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_RGB or DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 16; + DDSHeader.ddspf.dwRBitMask := $00007C00; + DDSHeader.ddspf.dwGBitMask := $000003E0; + DDSHeader.ddspf.dwBBitMask := $0000001F; + DDSHeader.ddspf.dwABitMask := $00008000; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_R8G8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_LUMINANCE OR DDPF_ALPHAPIXELS; + DDSHeader.ddspf.dwRGBBitCount := 16; + DDSHeader.ddspf.dwRBitMask := $000000FF; + DDSHeader.ddspf.dwABitMask := $0000FF00; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width * 2; + end; + DXGI_FORMAT_A8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_ALPHA; + DDSHeader.ddspf.dwRGBBitCount := 8; + DDSHeader.ddspf.dwABitMask := $000000FF; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width; + end; + DXGI_FORMAT_R8_UNORM: begin + DDSHeader.dwFlags := DDSHeader.dwFlags or DDSD_PITCH; + DDSHeader.ddspf.dwFlags := DDPF_LUMINANCE; + DDSHeader.ddspf.dwRGBBitCount := 8; + DDSHeader.ddspf.dwRBitMask := $000000FF; + DDSHeader.dwPitchOrLinearSize := FileFO4.Width; + end; + end; + + TexSize := SizeOf(TDDSHeader); + if DDSHeader.ddspf.dwFourCC = MAGIC_DX10 then begin + ReallocMem(Result.data, Result.size + SizeOf(TDDSHeaderDX10)); + Inc(TexSize, SizeOf(TDDSHeaderDX10)); + Result.size := TexSize; + end; + + // append chunks + for i := Low(FileFO4.TexChunks) to High(FileFO4.TexChunks) do with FileFO4.TexChunks[i] do begin + fStream.Position := Offset; + // compressed chunk + if PackedSize <> 0 then begin + SetLength(Buffer, PackedSize); + fStream.ReadBuffer(Buffer[0], Length(Buffer)); + SyncEndWrite; + try + DecompressBuf(@Buffer[0], Length(Buffer), @Result.data[TexSize], Size); + finally + SyncBeginWrite; + end; + end + // uncompressed chunk + else + fStream.ReadBuffer(Result.data[TexSize], Size); + + Inc(TexSize, Size); + Result.size := TexSize; + end; + end + + else + raise Exception.Create('Extraction is not supported for this archive'); + end; + finally + SyncEndWrite; + end; +end; + +function TwbBSArchive.ExtractFileData(const aFileName: string): TwbBSResultBuffer; +var + FileRecord: Pointer; +begin + if not (stReading in fStates) then + raise Exception.Create('Archive is not loaded'); + + FileRecord := FindFileRecord(aFileName); + + if not Assigned(FileRecord) then + raise Exception.Create('File not found in archive'); + + Result := ExtractFileData(FileRecord); +end; + +// Addded: For use in non-Borland C/C++ +procedure TwbBSArchive.ReleaseFileData(fileDataResult: TwbBSResultBuffer); +begin + FreeMem(fileDataResult.data); + fileDataResult.size := 0; +end; + +procedure TwbBSArchive.ExtractFile(const aFileName, aSaveAs: string); +var + fs: TFileStream; + fileData: TwbBSResultBuffer; +begin + if not (stReading in fStates) then + raise Exception.Create('Archive is not loaded'); + + fs := TFileStream.Create(aSaveAs, fmCreate); + try + fileData := ExtractFileData(aFileName); + fs.Write(fileData.data[0], fileData.size); + finally + ReleaseFileData(fileData); + fs.Free; + end; +end; + +procedure TwbBSArchive.IterateFiles(aProc: TBSFileIterationProc; aData: Pointer = nil; + aSingleThreaded: Boolean = False); +var + i, j: Integer; +begin + if not Assigned(aProc) then + Exit; + + if fMultiThreaded and not aSingleThreaded then + case fType of + baTES3: + TParallel.&For(Low(fFilesTES3), High(fFilesTES3), procedure(i: Integer; LoopState: TParallel.TLoopState) begin + if aProc(Self, fFilesTES3[i].Name, @fFilesTES3[i], nil, aData) then + LoopState.Stop; + end); + baTES4, baFO3, baSSE: + TParallel.&For(Low(fFoldersTES4), High(fFoldersTES4), procedure(i: Integer; OuterLoopState: TParallel.TLoopState) begin + TParallel.&For(Low(fFoldersTES4[i].Files), High(fFoldersTES4[i].Files), procedure(j: Integer; InnerLoopState: TParallel.TLoopState) begin + if aProc(Self, fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name, @fFoldersTES4[i].Files[j], @fFoldersTES4[i], aData) then begin + OuterLoopState.Stop; + InnerLoopState.Stop; + end; + end); + end); + baFO4, baFO4dds, baSF, baSFdds: + TParallel.&For(Low(fFilesFO4), High(fFilesFO4), procedure(i: Integer; LoopState: TParallel.TLoopState) begin + if aProc(Self, fFilesFO4[i].Name, @fFilesFO4[i], nil, aData) then + LoopState.Stop; + end); + end + else + case fType of + baTES3: + for i := Low(fFilesTES3) to High(fFilesTES3) do + if aProc(Self, fFilesTES3[i].Name, @fFilesTES3[i], nil, aData) then + Break; + baTES4, baFO3, baSSE: + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + for j := Low(fFoldersTES4[i].Files) to High(fFoldersTES4[i].Files) do + if aProc(Self, fFoldersTES4[i].Name + '\' + fFoldersTES4[i].Files[j].Name, @fFoldersTES4[i].Files[j], @fFoldersTES4[i], aData) then + Break; + baFO4, baFO4dds, baSF, baSFdds: + for i := Low(fFilesFO4) to High(fFilesFO4) do + if aProc(Self, fFilesFO4[i].Name, @fFilesFO4[i], nil, aData) then + Break; + end; +end; + +{procedure TwbBSArchive.IterateFolders(aProc: TBSFileIterationProc); +var + i: Integer; +begin + if not Assigned(aProc) then + Exit; + + if fType in [baTES4, baFO3, baSSE] then + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + aProc(Self, fFoldersTES4[i].Name, nil, @fFoldersTES4[i]); +end;} + +function TwbBSArchive.FileExists(const aFileName: string): Boolean; +begin + Result := Assigned(FindFileRecord(aFileName)); +end; + +procedure TwbBSArchive.Close; +begin + if Assigned(fStream) then + FreeAndNil(fStream); + + if stWriting in fStates then + System.SysUtils.DeleteFile(fFileName); + + fStates := []; + fType := baNone; + fFileName := ''; + fDataOffset := 0; + FillChar(fHeaderTES3, SizeOf(fHeaderTES3), 0); + SetLength(fFilesTES3, 0); + FillChar(fHeaderTES4, SizeOf(fHeaderTES4), 0); + SetLength(fFoldersTES4, 0); + FillChar(fHeaderFO4, SizeOf(fHeaderFO4), 0); + SetLength(fFilesFO4, 0); + + if fShareData then begin + SetLength(fPackedData, 0); + fPackedDataCount := 0; + end; +end; + +procedure TwbBSArchive.ResourceDict(const aDict: TwbResourceDict; aFolder: string); +var + Folder : string; + i, j : Integer; +begin + if not Assigned(aDict) then + Exit; + + Folder := ExcludeTrailingPathDelimiter(aFolder); + case fType of + baTES3: + for i := Low(fFilesTES3) to High(fFilesTES3) do + with fFilesTES3[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aDict.TryAdd(Name, wbNothing); + baTES4, baFO3, baSSE: + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + with fFoldersTES4[i] do begin + if (Folder = '') or Name.StartsWith(Folder, True) then + for j := Low(Files) to High(Files) do begin + var lName := Name + '\' + Files[j].Name; + aDict.TryAdd(lName, wbNothing); + end; + end; + baFO4, baFO4dds, baSF, baSFdds: + for i := Low(fFilesFO4) to High(fFilesFO4) do + with fFilesFO4[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aDict.TryAdd(Name, wbNothing); + end; +end; + +procedure TwbBSArchive.ResourceList(const aList: TStrings; aFolder: string = ''); +var + Folder : string; + i, j : Integer; +begin + if not Assigned(aList) then + Exit; + + Folder := ExcludeTrailingPathDelimiter(aFolder); + case fType of + baTES3: + for i := Low(fFilesTES3) to High(fFilesTES3) do + with fFilesTES3[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aList.Add(Name); + baTES4, baFO3, baSSE: + for i := Low(fFoldersTES4) to High(fFoldersTES4) do + with fFoldersTES4[i] do begin + if (Folder = '') or Name.StartsWith(Folder, True) then + for j := Low(Files) to High(Files) do + aList.Add(Name + '\' + Files[j].Name); + end; + baFO4, baFO4dds, baSF, baSFdds: + for i := Low(fFilesFO4) to High(fFilesFO4) do + with fFilesFO4[i] do + if (Folder = '') or Name.StartsWith(Folder, True) then + aList.Add(Name); + end; + +end; + +end. diff --git a/libs/libbsarch/examples/libbsarch-visualstudio-test.cpp b/libs/libbsarch/examples/libbsarch-visualstudio-test.cpp new file mode 100644 index 0000000..1183ee9 --- /dev/null +++ b/libs/libbsarch/examples/libbsarch-visualstudio-test.cpp @@ -0,0 +1,190 @@ +#include +#include + +#include + +// #pragma comment(lib, "libbsarch") + +static const std::wstring separators(L"\\/"); + +std::wstring dirname(const std::wstring &path) +{ + size_t slash_pos = path.find_last_of(separators); + return path.substr(0, slash_pos); +} + +std::wstring basename(const std::wstring &path) +{ + size_t slash_pos = path.find_last_of(separators); + return path.substr(slash_pos + 1); +} + +bool mkdirp(const std::wstring &directory, bool basedir = true) +{ + DWORD attributes = ::GetFileAttributesW(directory.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) + { + std::size_t slash_pos = directory.find_last_of(separators); + if (slash_pos != std::wstring::npos) + { + mkdirp(directory.substr(0, slash_pos)); + } + return ::CreateDirectoryW(directory.c_str(), nullptr); + } + else + { + return true; + } +} + +void dds_info(bsa_archive_t archive, const wchar_t *file_path, bsa_dds_info_t *dds_info, void *context) +{ + // Mock result + dds_info->width = 2048; + dds_info->height = 2048; + dds_info->mipmaps = 12; + + printf("ba2 dds_info callback file: %ls, context: %s\n", file_path, (char *) context); +} + +int main() +{ + bsa_result_message_t result = {0}; + bsa_archive_t archive = bsa_create(); + + { + result = bsa_load_from_file(archive, L"test_read.bsa"); + if (result.code < 0) + printf("ba1 %ls\n", result.text); + + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_get_resource_list(archive, entries, L""); + + for (size_t index = 0; index < bsa_entry_list_count(entries); index++) + { + wchar_t filename[2048]; + wchar_t dest_filename[2048] = L"test_ba1\\"; + + bsa_entry_list_get(entries, index, 2048, filename); + wcscat_s(dest_filename, 2048, filename); + + printf("ba1 file: %ls %ls\n", filename, dest_filename); + + if (mkdirp(dirname(dest_filename))) + { + result = bsa_extract_file(archive, filename, dest_filename); + if (result.code < 0) + printf("ba1 %ls\n", result.text); + } + else + { + printf("ba1 could not create: %ls\n", dirname(dest_filename).c_str()); + } + } + + bsa_entry_list_free(entries); + + bsa_close(archive); + } + + { + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_entry_list_add(entries, L"textures\\grass\\test.dds"); + bsa_entry_list_add(entries, L"textures\\grass\\test2.dds"); + + bsa_create_archive(archive, L"test_write.bsa", baSSE, entries); + bsa_add_file_from_disk_root(archive, L"test_ba1\\", L"test_ba1\\textures\\grass\\test.dds"); + result = bsa_add_file_from_disk(archive, L"textures\\grass\\test2.dds", L"test_ba1\\textures\\grass\\test.dds"); + if (result.code < 0) + printf("ba1 %ls\n", result.text); + bsa_save(archive); + bsa_close(archive); + + bsa_entry_list_free(entries); + } + + bsa_file_dds_info_callback_set(archive, dds_info, (void *) "I was shared"); + + { + result = bsa_load_from_file(archive, L"test_read.ba2"); + if (result.code < 0) + printf("ba2 %ls\n", result.text); + + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_get_resource_list(archive, entries, L""); + + for (size_t index = 0; index < bsa_entry_list_count(entries); index++) + { + wchar_t filename[2048]; + wchar_t dest_filename[2048] = L"test_ba2\\"; + + bsa_entry_list_get(entries, index, 2048, filename); + wcscat_s(dest_filename, 2048, filename); + + printf("ba2 file: %ls %ls\n", filename, dest_filename); + + if (mkdirp(dirname(dest_filename))) + { + result = bsa_extract_file(archive, filename, dest_filename); + if (result.code < 0) + printf("ba2 %ls\n", result.text); + } + else + { + printf("ba2 could not create: %ls\n", dirname(dest_filename).c_str()); + } + } + + bsa_entry_list_free(entries); + + bsa_close(archive); + } + + { + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_entry_list_add(entries, L"textures\\grass\\test.dds"); + bsa_entry_list_add(entries, L"textures\\grass\\test2.dds"); + + bsa_create_archive(archive, L"test_write.ba2", baFO4dds, entries); + bsa_add_file_from_disk_root(archive, L"test_ba2\\", L"test_ba2\\textures\\grass\\test.dds"); + result = bsa_add_file_from_disk(archive, L"textures\\grass\\test2.dds", L"test_ba2\\textures\\grass\\test.dds"); + if (result.code < 0) + printf("ba2 %ls\n", result.text); + bsa_save(archive); + bsa_close(archive); + + bsa_entry_list_free(entries); + } + + /* + { + result = bsa_load_from_file(archive, L"test_large.ba2"); + if (result.code < 0) + printf("ba2 large %ls\n", result.text); + + bsa_entry_list_t entries = bsa_entry_list_create(); + bsa_get_resource_list(archive, entries, L""); + + for (size_t index = 0; index < bsa_entry_list_count(entries); index++) { + wchar_t filename[2048]; + wchar_t dest_filename[2048] = L"test_ba2_large\\"; + + bsa_entry_list_get(entries, index, 2048, filename); + wcscat_s(dest_filename, 2048, filename); + + printf("ba2 file large: %ls %ls\n", filename, dest_filename); + + if (mkdirp(dirname(dest_filename))) { + result = bsa_extract_file(archive, filename, dest_filename); + if (result.code < 0) + printf("ba2 large %ls\n", result.text); + } + else { + printf("ba2 large could not create: %ls\n", dirname(dest_filename).c_str()); + } + } + } + */ + + bsa_free(archive); +} diff --git a/libs/libbsarch/examples/libbsarch-visualstudio-test.sln b/libs/libbsarch/examples/libbsarch-visualstudio-test.sln new file mode 100644 index 0000000..8f105e4 --- /dev/null +++ b/libs/libbsarch/examples/libbsarch-visualstudio-test.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.156 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libbsarch-visualstudio-test", "libbsarch-visualstudio-test.vcxproj", "{8F73F848-939D-4862-B58B-26572CF20A5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x64.ActiveCfg = Debug|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x64.Build.0 = Debug|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x86.ActiveCfg = Debug|Win32 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Debug|x86.Build.0 = Debug|Win32 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x64.ActiveCfg = Release|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x64.Build.0 = Release|x64 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x86.ActiveCfg = Release|Win32 + {8F73F848-939D-4862-B58B-26572CF20A5F}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F05F0FBF-790B-4443-B2EC-E7033074D9E0} + EndGlobalSection +EndGlobal diff --git a/libs/libbsarch/examples/test_read.ba2 b/libs/libbsarch/examples/test_read.ba2 new file mode 100644 index 0000000..16162ca Binary files /dev/null and b/libs/libbsarch/examples/test_read.ba2 differ diff --git a/libs/libbsarch/examples/test_read.bsa b/libs/libbsarch/examples/test_read.bsa new file mode 100644 index 0000000..e493003 Binary files /dev/null and b/libs/libbsarch/examples/test_read.bsa differ diff --git a/libs/libbsarch/include/libbsarch/base_types.hpp b/libs/libbsarch/include/libbsarch/base_types.hpp new file mode 100644 index 0000000..2baf4be --- /dev/null +++ b/libs/libbsarch/include/libbsarch/base_types.hpp @@ -0,0 +1,66 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#include "libbsarch.h" +#include "utils/convertible_string.hpp" +#include + +namespace libbsarch { +struct memory_blob : public bsa_result_buffer_t +{ + memory_blob(uint32_t buffer_size, bsa_buffer_t buffer_data, bsa_archive_t parent) + : bsa_result_buffer_t{buffer_size, buffer_data} + , parent_(parent) + {} + + memory_blob(bsa_result_buffer_t buffer, bsa_archive_t parent) + : bsa_result_buffer_t(buffer) + , parent_(parent) + {} + + bsa_archive_t parent_; + + memory_blob(memory_blob const &) = default; + ~memory_blob() { bsa_file_data_free(parent_, *this); } +}; + +struct disk_blob +{ + disk_blob(const convertible_string &root_dir, const convertible_string &absolute_path) + : path_in_archive(absolute_path) + , path_on_disk(absolute_path) + { + path_in_archive.remove_substring(root_dir); + } + + disk_blob(const convertible_string &path_in_archive, + const convertible_string &absolute_path, + [[maybe_unused]] bool decoy_parameter) + : path_in_archive(path_in_archive) + , path_on_disk(absolute_path) + {} + + convertible_string path_in_archive; + convertible_string path_on_disk; +}; + +struct bs_packed_file +{ + bs_packed_file(const disk_blob &blob) + : path_in_archive(blob.path_in_archive) + , data(blob.path_on_disk) + {} + + bs_packed_file(convertible_string path_in_archive, memory_blob blob) + : path_in_archive(std::move(path_in_archive)) + , data(blob) + {} + + convertible_string path_in_archive; + std::variant data; +}; + +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/bs_archive.h b/libs/libbsarch/include/libbsarch/bs_archive.h new file mode 100644 index 0000000..1954e99 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/bs_archive.h @@ -0,0 +1,100 @@ +#pragma once + +#include "bs_archive_entries.h" +#include "libbsarch.hpp" + +namespace libbsarch { +class bs_archive +{ +public: + bs_archive(); + bs_archive(const bsa_archive_type_t &type); + virtual ~bs_archive(); + + /* Properties */ + convertible_string get_filename(); + bsa_archive_type_t get_type(); + uint32_t get_version(); + convertible_string get_format_name(); + uint32_t get_file_count(); + + uint32_t get_archive_flags(); + void set_archive_flags(uint32_t flags); + + uint32_t get_file_flags(); + void set_file_flags(uint32_t flags); + + void set_compressed(bool value); + bool get_compressed(); + + void set_share_data(bool value); + bool get_share_data(); + + /* Input-Output */ + virtual void load_from_disk(const convertible_string &archive_path); + virtual void create(const convertible_string &archiveName, const bs_archive_entries &entries); + virtual void create(const convertible_string &archiveName, + const bs_archive_entries &entries, + const bsa_archive_type_t type); + void save() const; + + /* Add to archive */ + virtual void add_file_from_disk(const disk_blob &blob); + virtual void add_file_from_memory(const convertible_string &path_in_archive, const memory_blob &memory_data); + + /* Extract */ + memory_blob extract_to_memory(bsa_file_record_t record); + memory_blob extract_to_memory(const convertible_string &filename); + void extract_to_disk(const convertible_string &filename, const convertible_string &save_as) const; + + /* Selectors */ + bsa_file_record_t find_file_record(const convertible_string &filename); + std::vector list_files() const; + + /* Other */ + + /*! \brief Sets the DDS callback. It is required to use Fallout 4 DDS archives. You do not have + * to manage the lifetime of the provided context + * \example + void dds_callback([[maybe_unused]] bsa_archive_t archive, + const wchar_t *file_path, + bsa_dds_info_t *dds_info, + void *context) + { + const auto &path = *static_cast(context) + '/' + convertible_string(file_path); + + auto image = std::make_unique(); + DirectX::TexMetadata info; + + const auto hr = LoadFromDDSFile(PREPARE_PATH_LIBBSARCH(path), DirectX::DDS_FLAGS_BAD_DXTN_TAILS, &info, *image); + if (FAILED(hr)) + { + //Do not throw here. Exceptions will be ignored + return; + } + + dds_info->width = info.width; + dds_info->height = info.height; + dds_info->mipmaps = info.mipLevels; + } + */ + template + void set_dds_callback(bsa_file_dds_info_proc_t dds_function, const context_type &context) + { + debug_log() << "Setting DDS callback for archive: " << archive_ << "\nCallback adress: " << &dds_function; + + auto heap_context = new context_type(context); + callback_contexts_.emplace_back(heap_context); + bsa_file_dds_info_callback_set(archive_, dds_function, callback_contexts_.back()); + } + + bsa_archive_t get_archive() const; + +protected: + bsa_archive_t archive_; + bsa_archive_type_t type_; + +private: + std::vector callback_contexts_; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/bs_archive_auto.hpp b/libs/libbsarch/include/libbsarch/bs_archive_auto.hpp new file mode 100644 index 0000000..c288f68 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/bs_archive_auto.hpp @@ -0,0 +1,39 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#include "bs_archive.h" + +namespace libbsarch { +/*! + * \brief The bs_archive_auto class is a convenience class. It manages both bs_archive and bs_archive_entries. + * It is recommended when packing from disk/extracting to disk + * Otherwise, if using memory, the use of these two separate classes is recommended + */ +class bs_archive_auto : public bs_archive +{ +protected: + bs_archive_entries entries_; + +public: + bs_archive_auto() = default; + bs_archive_auto(const bsa_archive_type_t &type); + + void add_file_from_disk(const disk_blob &blob) override; + void add_file_from_disk(const std::vector &blobs); + void add_file_from_memory(const convertible_string &path_in_archive, const memory_blob &memory_data) override; + void add_file(const bs_packed_file &file); + + void extract_all(const convertible_string &directory, bool overwrite_current_files); + + void load_from_disk(const convertible_string &archive_path) override; + void save_to_disk(const convertible_string &archive_path); + +private: + using bs_archive::create; //Use save_to_disk instead + using bs_archive::save; //Use save_to_disk instead + std::vector files_; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/bs_archive_entries.h b/libs/libbsarch/include/libbsarch/bs_archive_entries.h new file mode 100644 index 0000000..7df2f40 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/bs_archive_entries.h @@ -0,0 +1,24 @@ +#pragma once + +#include "libbsarch.hpp" +#include + +namespace libbsarch { +class bs_archive_entries +{ +public: + bs_archive_entries(); + explicit bs_archive_entries(const std::vector &entries); + explicit bs_archive_entries(const bsa_entry_list_t &entries); + ~bs_archive_entries(); + + void add(const convertible_string &filepath); + uint32_t count(); + std::vector list(); + + bsa_entry_list_t getEntries() const; + +protected: + bsa_entry_list_t _entries; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/libbsarch.h b/libs/libbsarch/include/libbsarch/libbsarch.h new file mode 100644 index 0000000..bda7ca0 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/libbsarch.h @@ -0,0 +1,157 @@ +#pragma once + +#include + +#include + +#include + +#ifdef _WIN32 + #ifdef BSARCH_DLL_EXPORT + #define BSARCH_DLL_API(ReturnType) extern "C" __declspec(dllexport) ReturnType __stdcall + #else + #define BSARCH_DLL_API(ReturnType) extern "C" __declspec(dllimport) ReturnType __stdcall + #endif +#else + #ifdef BSARCH_DLL_EXPORT + #define BSARCH_DLL_API(ReturnType) extern "C" __attribute__((visibility("default"))) ReturnType + #else + #define BSARCH_DLL_API(ReturnType) extern "C" ReturnType + #endif +#endif + +#ifdef __GNUC__ +#define PACKED(datastructure) datastructure __attribute__((__packed__)) +#else +#define PACKED(datastructure) __pragma(pack(push, 1)) datastructure __pragma(pack(pop)) +#endif + +typedef enum bsa_result_code_e +{ + BSA_RESULT_NONE = 0, + BSA_RESULT_EXCEPTION = -1 +} bsa_result_code_t; + +typedef void *bsa_buffer_t; +typedef void *bsa_archive_t; +typedef void *bsa_entry_list_t; +typedef void *bsa_file_record_t; +typedef void *bsa_folder_record_t; + +typedef struct bsa_dds_info_s +{ + uint32_t width, height, mipmaps; +} bsa_dds_info_t; + +PACKED(struct bsa_dds_header_s { + uint32_t magic; // DDS_MAGIC + DirectX::DDS_HEADER header; +}); + +typedef struct bsa_dds_header_s bsa_dds_header_t; + +PACKED(struct bsa_result_message_s { + int8_t code; // bsa_result_code_t + wchar_t text[1024]; +}); + +typedef struct bsa_result_message_s bsa_result_message_t; + +PACKED(struct bsa_result_buffer_s { + uint32_t size; + bsa_buffer_t data; +}); + +typedef struct bsa_result_buffer_s bsa_result_buffer_t; + +PACKED(struct bsa_result_message_buffer_s { + bsa_result_buffer_t buffer; + bsa_result_message_t message; +}); + +typedef struct bsa_result_message_buffer_s bsa_result_message_buffer_t; + +typedef enum bsa_archive_state_e +{ + stReading, + stWriting +} bsa_archive_state_t; + +typedef enum bsa_archive_type_e +{ + baNone, + baTES3, + baTES4, + baFO3, + baSSE, + baFO4, + baFO4dds, + baSF, + baSFdds +} bsa_archive_type_t; + +typedef void (*bsa_file_dds_info_proc_t)(bsa_archive_t archive, + const wchar_t *file_path, + bsa_dds_info_t *dds_info, + void *context); +typedef bool (*bsa_file_iteration_proc_t)(bsa_archive_t archive, + const wchar_t *file_path, + bsa_file_record_t file_record, + bsa_folder_record_t folder_record, + void *context); + +BSARCH_DLL_API(bsa_entry_list_t) bsa_entry_list_create(); +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_free(bsa_entry_list_t entry_list); +BSARCH_DLL_API(uint32_t) bsa_entry_list_count(bsa_entry_list_t entry_list); +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_add(bsa_entry_list_t entry_list, const wchar_t *entry_string); +BSARCH_DLL_API(uint32_t) +bsa_entry_list_get(bsa_entry_list_t entry_list, uint32_t index, uint32_t string_buffer_size, wchar_t *string_buffer); + +BSARCH_DLL_API(bsa_archive_t) bsa_create(); +BSARCH_DLL_API(bsa_result_message_t) bsa_free(bsa_archive_t archive); +BSARCH_DLL_API(bsa_result_message_t) bsa_load_from_file(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_create_archive(bsa_archive_t archive, + const wchar_t *file_path, + bsa_archive_type_t _archivetype, + bsa_entry_list_t entry_list); +BSARCH_DLL_API(bsa_result_message_t) bsa_save(bsa_archive_t archive); +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *source_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk_root(bsa_archive_t archive, const wchar_t *root_dir, const wchar_t *source_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_memory(bsa_archive_t archive, const wchar_t *file_path, uint32_t size, bsa_buffer_t data); +BSARCH_DLL_API(bsa_file_record_t) bsa_find_file_record(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_record(bsa_archive_t archive, bsa_file_record_t file_record); +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_filename(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_t) bsa_file_data_free(bsa_archive_t archive, bsa_result_buffer_t file_data_result); +BSARCH_DLL_API(bsa_result_message_t) +bsa_extract_file(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *save_as); +BSARCH_DLL_API(bsa_result_message_t) +bsa_iterate_files(bsa_archive_t archive, bsa_file_iteration_proc_t file_iteration_proc, void *context); +BSARCH_DLL_API(bool) bsa_file_exists(bsa_archive_t archive, const wchar_t *file_path); +BSARCH_DLL_API(bsa_result_message_t) +bsa_get_resource_list(bsa_archive_t archive, bsa_entry_list_t entry_result_list, const wchar_t *folder); +BSARCH_DLL_API(bsa_result_message_t) bsa_close(bsa_archive_t archive); + +BSARCH_DLL_API(uint32_t) +bsa_filename_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer); +BSARCH_DLL_API(bsa_archive_type_t) bsa_archive_type_get(bsa_archive_t archive); +BSARCH_DLL_API(uint32_t) bsa_version_get(bsa_archive_t archive); +BSARCH_DLL_API(uint32_t) +bsa_format_name_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer); +BSARCH_DLL_API(uint32_t) bsa_file_count_get(bsa_archive_t archive); +BSARCH_DLL_API(uint32_t) bsa_archive_flags_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_archive_flags_set(bsa_archive_t archive, uint32_t flags); +BSARCH_DLL_API(uint32_t) bsa_file_flags_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_file_flags_set(bsa_archive_t archive, uint32_t flags); +BSARCH_DLL_API(bool) bsa_compress_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_compress_set(bsa_archive_t archive, bool flags); +BSARCH_DLL_API(bool) bsa_share_data_get(bsa_archive_t archive); +BSARCH_DLL_API(void) bsa_share_data_set(bsa_archive_t archive, bool flags); + +BSARCH_DLL_API(void) +bsa_file_dds_info_callback_set(bsa_archive_t archive, bsa_file_dds_info_proc_t file_dds_info_proc, void *context); diff --git a/libs/libbsarch/include/libbsarch/libbsarch.hpp b/libs/libbsarch/include/libbsarch/libbsarch.hpp new file mode 100644 index 0000000..2a6acce --- /dev/null +++ b/libs/libbsarch/include/libbsarch/libbsarch.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "base_types.hpp" +#include "utils/convertible_ostream.hpp" +#include "utils/convertible_string.hpp" + +#include + +namespace libbsarch { +constexpr bool enable_debug_log = true; +[[maybe_unused]] constexpr int max_string_buffer_size = 1024; + +inline convertible_ostream &debug_log() +{ + static convertible_ostream ostr; + if constexpr (enable_debug_log) + return ostr << "[libbsarch] " << __FUNCTION__ << ' '; +} + +inline void checkResult(const bsa_result_message_s &result) +{ + if (result.code == BSA_RESULT_EXCEPTION) + { + // Copy from packed struct to aligned local buffer to avoid UB + wchar_t aligned_text[1024]; + std::memcpy(aligned_text, result.text, sizeof(aligned_text)); + const std::string error = to_string(std::wstring(aligned_text)); + debug_log() << error; + throw std::runtime_error(error); + } +} + +inline void checkResult(const bsa_result_message_buffer_s &result) +{ + checkResult(result.message); +} +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp b/libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp new file mode 100644 index 0000000..80babb0 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/utils/convertible_ostream.hpp @@ -0,0 +1,37 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#include "convertible_string.hpp" +#include + +namespace libbsarch { +class convertible_ostream +{ +public: + convertible_ostream &operator<<(const std::string &rh); + convertible_ostream &operator<<(const char *rh); + convertible_ostream &operator<<(const char rh); + convertible_ostream &operator<<(const std::wstring &rh); + convertible_ostream &operator<<(const void *rh); + convertible_ostream &operator<<(const convertible_string &rh); + + template::value, number>::type> + convertible_ostream &operator<<(const number &num) + { + std::cout << num; + return *this; + } + + operator std::ostream &(); + operator std::wostream &(); + +#ifdef LIBBSARCH_QT_SUPPORT + operator QDebug(); + convertible_ostream &operator<<(const QString &rh); +#endif +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/utils/convertible_string.hpp b/libs/libbsarch/include/libbsarch/utils/convertible_string.hpp new file mode 100644 index 0000000..e6b996d --- /dev/null +++ b/libs/libbsarch/include/libbsarch/utils/convertible_string.hpp @@ -0,0 +1,50 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +//See https://stackoverflow.com/questions/4804298/how-to-convert-wstring-into-string + +#ifdef LIBBSARCH_QT_SUPPORT +#include +#endif + +#include "string_convert.hpp" + +namespace libbsarch { + +template +class convertible_string_converter; + +class convertible_string +{ +public: + // default ctor + convertible_string() = default; + + /* conversion ctors */ + convertible_string(std::string value, bool to_native_path = true); + convertible_string(const char *const val_array, bool to_native_path = true); + convertible_string(const std::wstring &wvalue, bool to_native_path = true); + convertible_string(const wchar_t *const wval_array, bool to_native_path = true); + + /* assignment operators */ + convertible_string &operator=(const std::string &value); + convertible_string &operator=(const std::wstring &wvalue); + convertible_string &operator=(const wchar_t *wvalue); + + /* implicit conversion operators */ + operator std::string() const; + operator std::wstring() const; + operator const wchar_t *() const; + + /* Util */ + bool remove_substring(const convertible_string &sub_str); + convertible_string &to_native_path(); + +private: + std::string str_; + bool auto_convert_to_native_path = true; +}; +} // namespace libbsarch diff --git a/libs/libbsarch/include/libbsarch/utils/string_convert.hpp b/libs/libbsarch/include/libbsarch/utils/string_convert.hpp new file mode 100644 index 0000000..6086fe1 --- /dev/null +++ b/libs/libbsarch/include/libbsarch/utils/string_convert.hpp @@ -0,0 +1,18 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#pragma once + +#ifdef LIBBSARCH_QT_SUPPORT +#include +#endif + +#include +#include +#include + +namespace libbsarch { +std::string to_string(const std::wstring &str); +std::wstring to_wstring(const std::string &str); +} // namespace libbsarch diff --git a/libs/libbsarch/src/bs_archive.cpp b/libs/libbsarch/src/bs_archive.cpp new file mode 100644 index 0000000..b50ef5b --- /dev/null +++ b/libs/libbsarch/src/bs_archive.cpp @@ -0,0 +1,185 @@ +#include "bs_archive.h" + +namespace libbsarch { + +bs_archive::bs_archive() + : archive_(bsa_create()) +{} + +bs_archive::bs_archive(const bsa_archive_type_t &type) + : archive_(bsa_create()) + , type_(type) +{} + +bs_archive::~bs_archive() +{ + bsa_close(archive_); + bsa_free(archive_); +} + +/* Properties */ +convertible_string bs_archive::get_filename() +{ + wchar_t name[max_string_buffer_size]; + bsa_filename_get(archive_, max_string_buffer_size, name); + return name; +} + +bsa_archive_type_t bs_archive::get_type() +{ + return bsa_archive_type_get(archive_); +} + +uint32_t bs_archive::get_version() +{ + return bsa_version_get(archive_); +} + +convertible_string bs_archive::get_format_name() +{ + wchar_t format_name[max_string_buffer_size]; + bsa_format_name_get(archive_, max_string_buffer_size, format_name); + return format_name; +} + +uint32_t bs_archive::get_file_count() +{ + return bsa_file_count_get(archive_); +} + +uint32_t bs_archive::get_archive_flags() +{ + return bsa_archive_flags_get(archive_); +} + +void bs_archive::set_archive_flags(uint32_t flags) +{ + bsa_archive_flags_set(archive_, flags); +} + +uint32_t bs_archive::get_file_flags() +{ + return bsa_file_flags_get(archive_); +} + +void bs_archive::set_file_flags(uint32_t flags) +{ + bsa_file_flags_set(archive_, flags); +} + +void bs_archive::set_compressed(bool value) +{ + bsa_compress_set(archive_, value); +} + +void bs_archive::set_share_data(bool value) +{ + bsa_share_data_set(archive_, value); +} + +bool bs_archive::get_compressed() +{ + return bsa_compress_get(archive_); +} + +bool bs_archive::get_share_data() +{ + return bsa_share_data_get(archive_); +} + +void bs_archive::load_from_disk(const convertible_string &archive_path) +{ + debug_log() << "Opening archive: " << archive_path; + const std::wstring str = archive_path; + const auto &result = bsa_load_from_file(archive_, str.c_str()); + libbsarch::checkResult(result); +} + +void bs_archive::create(const convertible_string &archiveName, const bs_archive_entries &entries) +{ + create(archiveName, entries, type_); +} + +void bs_archive::create(const convertible_string &archiveName, + const bs_archive_entries &entries, + const bsa_archive_type_t type) +{ + debug_log() << "Creating archive. Archive name: " << archiveName << '\n' + << "type: " << type_ << '\n' + << "entries: " << entries.getEntries(); + bsa_create_archive(archive_, archiveName, type, entries.getEntries()); +} + +void bs_archive::save() const +{ + debug_log() << "Saving archive: " << archive_; + + const auto &result = bsa_save(archive_); + + libbsarch::checkResult(result); +} + +void bs_archive::add_file_from_disk(const disk_blob &blob) +{ + debug_log() << "Adding file from disk root. Path in archive: " << blob.path_in_archive << '\n' + << "Filepath: " << blob.path_on_disk << '\n' + << "Archive: " << archive_; + + const auto &result = bsa_add_file_from_disk(archive_, blob.path_in_archive, blob.path_on_disk); + + libbsarch::checkResult(result); +} + +void bs_archive::add_file_from_memory(const convertible_string &path_in_archive, + const memory_blob &memory_data) //NOTE UNTESTED +{ + debug_log() << "Adding file from memory. Filename: " << path_in_archive << '\n' + << "Data size: " << memory_data.size; + const auto &result = bsa_add_file_from_memory(archive_, path_in_archive, memory_data.size, memory_data.data); + libbsarch::checkResult(result); +} + +bsa_file_record_t bs_archive::find_file_record(const convertible_string &filename) +{ + debug_log() << "Finding file record of: " << filename; + const auto &result = bsa_find_file_record(archive_, filename); + return result; +} + +memory_blob bs_archive::extract_to_memory(bsa_file_record_t record) +{ + const auto &result = bsa_extract_file_data_by_record(archive_, record); + libbsarch::checkResult(result); + return memory_blob(result.buffer, archive_); +} + +memory_blob bs_archive::extract_to_memory(const convertible_string &filename) +{ + const auto &result = bsa_extract_file_data_by_filename(archive_, filename); + libbsarch::checkResult(result); + return memory_blob(result.buffer, archive_); +} + +void bs_archive::extract_to_disk(const convertible_string &filename, const convertible_string &save_as) const +{ + debug_log() << "Extracting: " << filename << " saved as " << save_as; + const auto &result = bsa_extract_file(archive_, filename, save_as); + + libbsarch::checkResult(result); +} + +std::vector bs_archive::list_files() const +{ + bsa_entry_list_t list = bsa_entry_list_create(); + const auto &result = bsa_get_resource_list(archive_, list, L""); + + libbsarch::checkResult(result); + + return bs_archive_entries(list).list(); +} + +bsa_archive_t bs_archive::get_archive() const +{ + return archive_; +} +} // namespace libbsarch diff --git a/libs/libbsarch/src/bs_archive_auto.cpp b/libs/libbsarch/src/bs_archive_auto.cpp new file mode 100644 index 0000000..1ad8e3e --- /dev/null +++ b/libs/libbsarch/src/bs_archive_auto.cpp @@ -0,0 +1,87 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "bs_archive_auto.hpp" +#include +namespace libbsarch { +bs_archive_auto::bs_archive_auto(const bsa_archive_type_t &type) + : bs_archive(type) +{} + +void bs_archive_auto::add_file_from_disk(const disk_blob &blob) +{ + files_.emplace_back(blob); +} + +void bs_archive_auto::add_file_from_disk(const std::vector &blobs) +{ + files_.reserve(blobs.size()); + files_.insert(files_.end(), blobs.cbegin(), blobs.cend()); +} + +void bs_archive_auto::add_file_from_memory(const convertible_string &path_in_archive, const memory_blob &memory_data) +{ + files_.emplace_back(bs_packed_file(path_in_archive, memory_data)); +} + +void bs_archive_auto::add_file(const bs_packed_file &file) +{ + files_.emplace_back(file); +} + +void bs_archive_auto::extract_all(const convertible_string &directory, bool overwrite_current_files) +{ + namespace fs = std::filesystem; + for (const auto &file : list_files()) + { + convertible_string path_string = std::string(directory) + "/" + std::string(file); + fs::path current_file(path_string); + fs::path current_path = fs::path(current_file).remove_filename(); + + fs::create_directories(current_path); + if (fs::exists(current_file) && overwrite_current_files) + { + fs::remove(current_file); + extract_to_disk(file, current_file.string()); + } + else if (!fs::exists(current_file)) + extract_to_disk(file, current_file.string()); + } +} + +void bs_archive_auto::load_from_disk(const convertible_string &archive_path) +{ + bs_archive::load_from_disk(archive_path); + for (const auto &file : list_files()) + entries_.add(file); +} + +void bs_archive_auto::save_to_disk(const convertible_string &archive_path) +{ + for (const auto &file : files_) + entries_.add(file.path_in_archive); + + bs_archive::create(archive_path, entries_); + + for (const auto &file : files_) + { + switch (file.data.index()) + { + case 0: { + const auto &blob = std::get(file.data); + bs_archive::add_file_from_memory(file.path_in_archive, blob); + break; + } + case 1: { + const auto &file_path = std::get(file.data); + const auto &blob = disk_blob(file.path_in_archive, file_path, bool()); + bs_archive::add_file_from_disk(blob); + } + } + } + bs_archive::save(); +} + +} // namespace libbsarch diff --git a/libs/libbsarch/src/bs_archive_entries.cpp b/libs/libbsarch/src/bs_archive_entries.cpp new file mode 100644 index 0000000..4dc9c6d --- /dev/null +++ b/libs/libbsarch/src/bs_archive_entries.cpp @@ -0,0 +1,52 @@ +#include "bs_archive_entries.h" + +namespace libbsarch { +bs_archive_entries::bs_archive_entries() + : _entries(bsa_entry_list_create()) +{} + +bs_archive_entries::bs_archive_entries(const std::vector &entries) + : _entries(bsa_entry_list_create()) +{ + for (const auto &entry : entries) + add(entry); +} + +bs_archive_entries::bs_archive_entries(const bsa_entry_list_t &entries) + : _entries(entries) +{} + +bs_archive_entries::~bs_archive_entries() +{ + bsa_entry_list_free(_entries); +} + +void bs_archive_entries::add(const convertible_string &filepath) +{ + debug_log() << "Adding to entries: " << filepath; + const auto &result = bsa_entry_list_add(_entries, filepath); + libbsarch::checkResult(result); +} + +uint32_t bs_archive_entries::count() +{ + return bsa_entry_list_count(_entries); +} + +std::vector bs_archive_entries::list() +{ + std::vector list; + for (uint32_t i = 0; i < count(); ++i) + { + wchar_t buffer[max_string_buffer_size]; + bsa_entry_list_get(_entries, i, max_string_buffer_size, buffer); + list.push_back(buffer); + } + return list; +} + +bsa_entry_list_t bs_archive_entries::getEntries() const +{ + return _entries; +} +} // namespace libbsarch diff --git a/libs/libbsarch/src/libbsarch.cpp b/libs/libbsarch/src/libbsarch.cpp new file mode 100644 index 0000000..52ded9f --- /dev/null +++ b/libs/libbsarch/src/libbsarch.cpp @@ -0,0 +1,206 @@ +#ifdef BSARCH_DLL_EXPORT +#include "libbsarch.h" + +namespace libbsarch { + +BSARCH_DLL_API(bsa_entry_list_t) bsa_entry_list_create() +{ + return nullptr; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_free(bsa_entry_list_t entry_list) +{ + return {0}; +} + +BSARCH_DLL_API(uint32_t) bsa_entry_list_count(bsa_entry_list_t entry_list) +{ + return 0; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_entry_list_add(bsa_entry_list_t entry_list, const wchar_t *entry_string) +{ + return {0}; +} + +BSARCH_DLL_API(uint32_t) +bsa_entry_list_get(bsa_entry_list_t entry_list, uint32_t index, uint32_t string_buffer_size, wchar_t *string_buffer) +{ + return 0; +} + +BSARCH_DLL_API(bsa_archive_t) bsa_create() +{ + return nullptr; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_free(bsa_archive_t archive) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_load_from_file(bsa_archive_t archive, const wchar_t *file_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_create_archive(bsa_archive_t archive, + const wchar_t *file_path, + bsa_archive_type_t archive_type, + bsa_entry_list_t entry_list) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_save(bsa_archive_t archive) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *source_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_disk_root(bsa_archive_t archive, const wchar_t *root_dir, const wchar_t *source_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_add_file_from_memory(bsa_archive_t archive, const wchar_t *file_path, uint32_t size, bsa_buffer_t data) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_file_record_t) bsa_find_file_record(bsa_archive_t archive, const wchar_t *file_path) +{ + return nullptr; +} + +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_record(bsa_archive_t archive, bsa_file_record_t file_record) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_buffer_t) +bsa_extract_file_data_by_filename(bsa_archive_t archive, const wchar_t *file_path) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_file_data_free(bsa_archive_t archive, bsa_result_buffer_t file_data_result) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_extract_file(bsa_archive_t archive, const wchar_t *file_path, const wchar_t *save_as) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_iterate_files(bsa_archive_t archive, bsa_file_iteration_proc_t file_iteration_proc, void *context) +{ + return {0}; +} + +BSARCH_DLL_API(bool) bsa_file_exists(bsa_archive_t archive, const wchar_t *file_path) +{ + return false; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_get_resource_list(bsa_archive_t archive, bsa_entry_list_t entry_result_list, const wchar_t *folder) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) +bsa_resolve_hash(bsa_archive_t archive, uint64_t hash, bsa_entry_list_t entry_result_list) +{ + return {0}; +} + +BSARCH_DLL_API(bsa_result_message_t) bsa_close(bsa_archive_t archive) +{ + return {0}; +} + +BSARCH_DLL_API(uint32_t) bsa_filename_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer) +{ + return 0; +} + +BSARCH_DLL_API(bsa_archive_type_t) bsa_archive_type_get(bsa_archive_t archive) +{ + return bsa_archive_type_t::baNone; +} + +BSARCH_DLL_API(uint32_t) bsa_version_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(uint32_t) bsa_format_name_get(bsa_archive_t archive, uint32_t string_buffer_size, wchar_t *string_buffer) +{ + return 0; +} + +BSARCH_DLL_API(uint32_t) bsa_file_count_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(uint32_t) bsa_archive_flags_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(void) bsa_archive_flags_set(bsa_archive_t archive, uint32_t flags) +{ + return; +} + +BSARCH_DLL_API(uint32_t) bsa_file_flags_get(bsa_archive_t archive) +{ + return 0; +} + +BSARCH_DLL_API(void) bsa_file_flags_set(bsa_archive_t archive, uint32_t flags) +{ + return; +} + +BSARCH_DLL_API(bool) bsa_compress_get(bsa_archive_t archive) +{ + return false; +} + +BSARCH_DLL_API(void) bsa_compress_set(bsa_archive_t archive, bool flags) +{ + return; +} + +BSARCH_DLL_API(bool) bsa_share_data_get(bsa_archive_t archive) +{ + return false; +} + +BSARCH_DLL_API(void) bsa_share_data_set(bsa_archive_t archive, bool flags) +{ + return; +} + +BSARCH_DLL_API(void) +bsa_file_dds_info_callback_set(bsa_archive_t archive, bsa_file_dds_info_proc_t file_dds_info_proc, void *context) +{ + return; +} +} // namespace libbsarch +#endif diff --git a/libs/libbsarch/src/libbsarch.def b/libs/libbsarch/src/libbsarch.def new file mode 100644 index 0000000..d6386b0 --- /dev/null +++ b/libs/libbsarch/src/libbsarch.def @@ -0,0 +1,38 @@ +;DESCRIPTION "BSArch DLL" +EXPORTS + bsa_entry_list_create + bsa_entry_list_free + bsa_entry_list_count + bsa_entry_list_add + bsa_entry_list_get + bsa_create + bsa_free + bsa_load_from_file + bsa_create_archive + bsa_save + bsa_add_file_from_disk + bsa_add_file_from_disk_root + bsa_add_file_from_memory + bsa_find_file_record + bsa_extract_file_data_by_record + bsa_extract_file_data_by_filename + bsa_file_data_free + bsa_extract_file + bsa_iterate_files + bsa_file_exists + bsa_get_resource_list + bsa_close + bsa_filename_get + bsa_archive_type_get + bsa_version_get + bsa_format_name_get + bsa_file_count_get + bsa_archive_flags_get + bsa_archive_flags_set + bsa_file_flags_get + bsa_file_flags_set + bsa_compress_get + bsa_compress_set + bsa_share_data_get + bsa_share_data_set + bsa_file_dds_info_callback_set diff --git a/libs/libbsarch/src/utils/convertible_ostream.cpp b/libs/libbsarch/src/utils/convertible_ostream.cpp new file mode 100644 index 0000000..d5ac238 --- /dev/null +++ b/libs/libbsarch/src/utils/convertible_ostream.cpp @@ -0,0 +1,58 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#include "convertible_ostream.hpp" + +namespace libbsarch { +convertible_ostream &convertible_ostream::operator<<(const std::string &rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const char *rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const char rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const std::wstring &rh) +{ + std::wcout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const void *rh) +{ + std::cout << rh; + return *this; +} +convertible_ostream &convertible_ostream::operator<<(const convertible_string &rh) +{ + return *this << std::string(rh); +} + +convertible_ostream::operator std::ostream &() +{ + return std::cout; +} +convertible_ostream::operator std::wostream &() +{ + return std::wcout; +} + +#ifdef LIBBSARCH_QT_SUPPORT +convertible_ostream::operator QDebug() +{ + return qDebug(); +} +convertible_ostream &convertible_ostream::operator<<(const QString &rh) +{ + qDebug() << rh; + return *this; +} +#endif +} // namespace libbsarch diff --git a/libs/libbsarch/src/utils/convertible_string.cpp b/libs/libbsarch/src/utils/convertible_string.cpp new file mode 100644 index 0000000..c965202 --- /dev/null +++ b/libs/libbsarch/src/utils/convertible_string.cpp @@ -0,0 +1,108 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "convertible_string.hpp" +#include +#include + +namespace libbsarch { +/* conversion ctors */ +convertible_string::convertible_string(std::string value, bool to_native_path) + : str_(std::move(value)) + , auto_convert_to_native_path(to_native_path) +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} + +convertible_string::convertible_string(const char *const val_array, bool to_native_path) + : str_(std::string(val_array)) + , auto_convert_to_native_path(to_native_path) + +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} +convertible_string::convertible_string(const std::wstring &wvalue, bool to_native_path) + : str_(to_string(wvalue)) + , auto_convert_to_native_path(to_native_path) + +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} +convertible_string::convertible_string(wchar_t const *wval_array, bool to_native_path) + : str_(to_string(std::wstring(wval_array))) + , auto_convert_to_native_path(to_native_path) +{ + if (auto_convert_to_native_path) + this->to_native_path(); +} + +/* assignment operators */ +convertible_string &convertible_string::operator=(const std::string &value) +{ + str_ = value; + if (auto_convert_to_native_path) + this->to_native_path(); + return *this; +} +convertible_string &convertible_string::operator=(const std::wstring &wvalue) +{ + str_ = to_string(wvalue); + if (auto_convert_to_native_path) + this->to_native_path(); + return *this; +} + +convertible_string &convertible_string::operator=(const wchar_t *wvalue) +{ + str_ = to_string(std::wstring(wvalue)); + if (auto_convert_to_native_path) + this->to_native_path(); + return *this; +} + +/* implicit conversion operators */ +convertible_string::operator std::string() const +{ + return str_; +} +convertible_string::operator std::wstring() const +{ + return to_wstring(str_); +} +convertible_string::operator const wchar_t *() const +{ + const std::wstring wstr = to_wstring(str_); + wchar_t *wchar_array = new wchar_t[wstr.length() + 1]; +#ifdef _WIN32 + wcscpy_s(wchar_array, wstr.length() + 1, wstr.c_str()); +#else + std::wmemcpy(wchar_array, wstr.c_str(), wstr.length() + 1); +#endif + return wchar_array; +} + +/* Util */ +bool convertible_string::remove_substring(const convertible_string &sub_str) +{ + size_t pos = str_.find(sub_str); + if (pos != std::string::npos) + { + str_.erase(pos, std::string(sub_str).length()); + return true; + } + return false; +} + +convertible_string &convertible_string::to_native_path() +{ + std::filesystem::path path(str_); + str_ = path.make_preferred().string(); + return *this; +} + +} // namespace libbsarch diff --git a/libs/libbsarch/src/utils/string_convert.cpp b/libs/libbsarch/src/utils/string_convert.cpp new file mode 100644 index 0000000..1e8ce9e --- /dev/null +++ b/libs/libbsarch/src/utils/string_convert.cpp @@ -0,0 +1,39 @@ +/* Copyright (C) 2019 G'k + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#include "string_convert.hpp" + +#include +#include + +namespace libbsarch { + +std::string to_string(const std::wstring &str) +{ + if (str.empty()) return {}; + std::mbstate_t state{}; + const wchar_t* src = str.data(); + std::size_t len = std::wcsrtombs(nullptr, &src, 0, &state); + if (len == static_cast(-1)) return {}; + std::string result(len, '\0'); + src = str.data(); + state = std::mbstate_t{}; + std::wcsrtombs(result.data(), &src, len + 1, &state); + return result; +} + +std::wstring to_wstring(const std::string &str) +{ + if (str.empty()) return {}; + std::mbstate_t state{}; + const char* src = str.data(); + std::size_t len = std::mbsrtowcs(nullptr, &src, 0, &state); + if (len == static_cast(-1)) return {}; + std::wstring result(len, L'\0'); + src = str.data(); + state = std::mbstate_t{}; + std::mbsrtowcs(result.data(), &src, len + 1, &state); + return result; +} +} // namespace libbsarch diff --git a/libs/libbsarch/vcpkg.json b/libs/libbsarch/vcpkg.json new file mode 100644 index 0000000..3cfe0a0 --- /dev/null +++ b/libs/libbsarch/vcpkg.json @@ -0,0 +1,18 @@ +{ + "dependencies": ["mo2-dds-header"], + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "69db61b22147b14ad66fd05cf798d855f6d68c12", + "packages": ["mo2-*"] + } + ] + } +} -- cgit v1.3.1