diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..426d76d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,398 @@
+## 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/main/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/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# 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/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.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
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# 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 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# 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/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..774a589
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Ac_K
+
+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/README.md b/README.md
new file mode 100644
index 0000000..d76e640
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# TTGamesExplorerRebirth
+ TTGames modding tool
diff --git a/distribution/misc/Logo.png b/distribution/misc/Logo.png
new file mode 100644
index 0000000..9040ad3
Binary files /dev/null and b/distribution/misc/Logo.png differ
diff --git a/distribution/misc/Screen01.png b/distribution/misc/Screen01.png
new file mode 100644
index 0000000..eea11ab
Binary files /dev/null and b/distribution/misc/Screen01.png differ
diff --git a/distribution/misc/Screen02.png b/distribution/misc/Screen02.png
new file mode 100644
index 0000000..055fa8f
Binary files /dev/null and b/distribution/misc/Screen02.png differ
diff --git a/distribution/misc/Screen03.png b/distribution/misc/Screen03.png
new file mode 100644
index 0000000..9e470a2
Binary files /dev/null and b/distribution/misc/Screen03.png differ
diff --git a/distribution/misc/Screen04.png b/distribution/misc/Screen04.png
new file mode 100644
index 0000000..36e58f1
Binary files /dev/null and b/distribution/misc/Screen04.png differ
diff --git a/distribution/misc/Screen05.png b/distribution/misc/Screen05.png
new file mode 100644
index 0000000..651bc5b
Binary files /dev/null and b/distribution/misc/Screen05.png differ
diff --git a/distribution/misc/Screen06.png b/distribution/misc/Screen06.png
new file mode 100644
index 0000000..66fee08
Binary files /dev/null and b/distribution/misc/Screen06.png differ
diff --git a/src/TTGamesExplorerRebirth.sln b/src/TTGamesExplorerRebirth.sln
new file mode 100644
index 0000000..3520571
--- /dev/null
+++ b/src/TTGamesExplorerRebirth.sln
@@ -0,0 +1,163 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34511.84
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TTGamesExplorerRebirthCmd", "TTGamesExplorerRebirthCmd\TTGamesExplorerRebirthCmd.csproj", "{7371710E-E05A-4758-94A1-5CE01DD6BC29}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TTGamesExplorerRebirthLib", "TTGamesExplorerRebirthLib\TTGamesExplorerRebirthLib.csproj", "{6DE3EAD3-AF28-4585-9E6F-3B8B61773741}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TTGamesExplorerRebirthUI", "TTGamesExplorerRebirthUI\TTGamesExplorerRebirthUI.csproj", "{43ED4579-F874-4497-AA9D-B20CF3C9A59B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|ARM = Debug|ARM
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Main|Any CPU = Main|Any CPU
+ Main|ARM = Main|ARM
+ Main|ARM64 = Main|ARM64
+ Main|x64 = Main|x64
+ Main|x86 = Main|x86
+ Main-Debug|Any CPU = Main-Debug|Any CPU
+ Main-Debug|ARM = Main-Debug|ARM
+ Main-Debug|ARM64 = Main-Debug|ARM64
+ Main-Debug|x64 = Main-Debug|x64
+ Main-Debug|x86 = Main-Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|ARM = Release|ARM
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x64.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x86.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|Any CPU.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|Any CPU.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM64.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM64.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x64.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x64.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x86.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x86.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM64.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x64.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x64.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x86.ActiveCfg = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x86.Build.0 = Debug|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM64.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x64.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x64.Build.0 = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x86.ActiveCfg = Release|Any CPU
+ {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x86.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x64.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x86.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|Any CPU.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|Any CPU.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM64.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM64.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x64.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x64.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x86.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x86.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM64.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x64.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x64.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x86.ActiveCfg = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x86.Build.0 = Debug|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM64.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x64.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x64.Build.0 = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x86.ActiveCfg = Release|Any CPU
+ {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x86.Build.0 = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x64.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x86.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|Any CPU.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|Any CPU.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM64.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM64.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x64.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x64.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x86.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x86.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|Any CPU.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM64.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x64.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x64.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x86.ActiveCfg = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x86.Build.0 = Debug|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM.ActiveCfg = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM.Build.0 = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM64.Build.0 = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x64.ActiveCfg = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x64.Build.0 = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x86.ActiveCfg = Release|Any CPU
+ {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {3F7065D4-94E1-42E5-A498-3F8C4DE41FCE}
+ EndGlobalSection
+EndGlobal
diff --git a/src/TTGamesExplorerRebirthCmd/Program.cs b/src/TTGamesExplorerRebirthCmd/Program.cs
new file mode 100644
index 0000000..f7e9319
--- /dev/null
+++ b/src/TTGamesExplorerRebirthCmd/Program.cs
@@ -0,0 +1,21 @@
+namespace TTGamesExplorerRebirthCmd
+{
+ internal class Program
+ {
+ static void Main()
+ {
+ Console.WriteLine("TTGames Explorer Rebirth");
+ Console.WriteLine("------------------------\n");
+
+ /*
+
+ This is used for testing format class easily before writing the UI part.
+ TODO: Maybe use this project do to a CLI project.
+
+ */
+
+ Console.WriteLine("Done!");
+ Console.ReadKey();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TTGamesExplorerRebirthCmd/TTGamesExplorerRebirthCmd.csproj b/src/TTGamesExplorerRebirthCmd/TTGamesExplorerRebirthCmd.csproj
new file mode 100644
index 0000000..f805b40
--- /dev/null
+++ b/src/TTGamesExplorerRebirthCmd/TTGamesExplorerRebirthCmd.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ disable
+
+
+
+
+
+
+
diff --git a/src/TTGamesExplorerRebirthLib/Compression/UnLZ2K.cs b/src/TTGamesExplorerRebirthLib/Compression/UnLZ2K.cs
new file mode 100644
index 0000000..33b9f13
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Compression/UnLZ2K.cs
@@ -0,0 +1,558 @@
+namespace TTGamesExplorerRebirthLib.Compression
+{
+ public static class UnLZ2K
+ {
+ private const uint ChunkSizeConst = 0x2000;
+
+ private static byte[] _srcBuffer;
+ private static byte[] _chunkBuffer;
+ private static byte[] _smallByteBuffer;
+ private static byte[] _largeByteBuffer;
+ private static short[] _smallWordBuffer;
+ private static short[] _largeWordBuffer;
+ private static short[] _parallelBuffer0;
+ private static short[] _parallelBuffer1;
+
+ private static uint _bitStream;
+ private static byte _previousBitAlign;
+ private static byte _lastByteRead;
+ private static uint _srcOffset;
+ private static int _literalsToCopy;
+ private static short _chunksWithCurrentSetup;
+ private static uint _readOffset;
+
+ ///
+ /// Give source byte buffer and destination byte buffer, process to LZ2K decompression.
+ /// This algorithm is mostly used by TTGames.
+ ///
+ ///
+ /// Based from RE of Lego Worlds by Ac_K.
+ /// Based on unlz2K by Trevor Natiuk:
+ /// https://github.com/pianistrevor/unlz2k
+ ///
+ ///
+ /// Byte array representing the source data to be decompressed.
+ ///
+ ///
+ /// Byte array representing the decompressed destination data.
+ ///
+ public static void Decompress(byte[] srcBuffer, byte[] dstBuffer)
+ {
+ if (dstBuffer.Length == 0)
+ {
+ return;
+ }
+
+ _srcBuffer = srcBuffer;
+ _chunkBuffer = new byte[ChunkSizeConst];
+ _smallByteBuffer = new byte[20];
+ _largeByteBuffer = new byte[510];
+ _smallWordBuffer = new short[256];
+ _largeWordBuffer = new short[4096];
+ _parallelBuffer0 = new short[1024];
+ _parallelBuffer1 = new short[1024];
+
+ _bitStream = 0;
+ _previousBitAlign = 0;
+ _lastByteRead = 0;
+ _srcOffset = 0;
+ _literalsToCopy = 0;
+ _chunksWithCurrentSetup = 0;
+ _readOffset = 0;
+
+ uint bytesLeft = (uint)dstBuffer.Length;
+
+ using MemoryStream dstStream = new(dstBuffer);
+ using BinaryWriter dstWriter = new(dstStream);
+
+ LoadIntoBitstream(0x20);
+
+ while (bytesLeft != 0)
+ {
+ uint chunkSize = bytesLeft < ChunkSizeConst ? bytesLeft : ChunkSizeConst;
+
+ DecodeChunk(chunkSize);
+
+ byte[] tempBuffer = new byte[chunkSize];
+
+ Array.Copy(_chunkBuffer, tempBuffer, chunkSize);
+
+ dstWriter.Write(tempBuffer);
+
+ bytesLeft -= chunkSize;
+ }
+
+ dstBuffer = dstStream.ToArray();
+ }
+
+ private static void LoadIntoBitstream(byte bits)
+ {
+ _bitStream <<= bits;
+
+ if (bits > _previousBitAlign)
+ {
+ do
+ {
+ bits -= _previousBitAlign;
+ _bitStream |= (uint)_lastByteRead << bits;
+ _lastByteRead = (byte)((_srcOffset == (uint)_srcBuffer.Length) ? 0 : _srcBuffer[_srcOffset++]);
+ _previousBitAlign = 8;
+ } while (bits > _previousBitAlign);
+ }
+
+ _previousBitAlign -= bits;
+ _bitStream |= (uint)_lastByteRead >> _previousBitAlign;
+ _bitStream &= 0xFFFFFFFF;
+ }
+
+ private static void DecodeChunk(uint chunkSize)
+ {
+ uint dstOffset = 0;
+
+ --_literalsToCopy;
+ if (_literalsToCopy >= 0)
+ {
+ while (_literalsToCopy >= 0)
+ {
+ _chunkBuffer[dstOffset++] = _chunkBuffer[_readOffset++];
+ _readOffset &= 0x1FFF;
+
+ if (dstOffset == chunkSize)
+ {
+ return;
+ }
+
+ --_literalsToCopy;
+ }
+ }
+
+ while (dstOffset < chunkSize)
+ {
+ short decodedBitStream = DecodeBitStream();
+ if (decodedBitStream <= 255)
+ {
+ _chunkBuffer[dstOffset++] = (byte)decodedBitStream;
+
+ if (dstOffset == chunkSize)
+ {
+ return;
+ }
+ }
+ else
+ {
+ _literalsToCopy = DecodeBitStreamForLiterals();
+ _readOffset = (uint)(dstOffset - _literalsToCopy - 1) & 0x1FFF;
+ _literalsToCopy = decodedBitStream - 254;
+
+ while (_literalsToCopy >= 0)
+ {
+ _chunkBuffer[dstOffset++] = _chunkBuffer[_readOffset++];
+ _readOffset &= 0x1FFF;
+
+ if (dstOffset == chunkSize)
+ {
+ return;
+ }
+
+ --_literalsToCopy;
+ }
+ }
+ }
+
+ if (dstOffset > chunkSize)
+ {
+ throw new IndexOutOfRangeException();
+ }
+ }
+
+ private static short DecodeBitStream()
+ {
+ if (_chunksWithCurrentSetup == 0)
+ {
+ _chunksWithCurrentSetup = (short)(_bitStream >> 16);
+
+ LoadIntoBitstream(0x10);
+ FillSmallDicts(19, 5, 3);
+ FillLargeDicts();
+ FillSmallDicts(14, 4, -1);
+ }
+
+ _chunksWithCurrentSetup--;
+
+ short tmpValue = _largeWordBuffer[_bitStream >> 20];
+ if (tmpValue >= _largeByteBuffer.Length)
+ {
+ uint mask = 0x80000;
+
+ while (tmpValue >= _largeByteBuffer.Length)
+ {
+ tmpValue = ((_bitStream & mask) == 0) ? _parallelBuffer0[tmpValue] : _parallelBuffer1[tmpValue];
+ mask >>= 1;
+ }
+ }
+
+ LoadIntoBitstream(_largeByteBuffer[tmpValue]);
+
+ return tmpValue;
+ }
+
+ private static int DecodeBitStreamForLiterals()
+ {
+ short tmpValue = _smallWordBuffer[_bitStream >> 24];
+ if (tmpValue >= 14)
+ {
+ uint mask = 0x800000;
+
+ while (tmpValue >= 14)
+ {
+ tmpValue = ((_bitStream & mask) == 0) ? _parallelBuffer0[tmpValue] : _parallelBuffer1[tmpValue];
+ mask >>= 1;
+ }
+ }
+
+ LoadIntoBitstream(_smallByteBuffer[tmpValue]);
+
+ if (tmpValue == 0)
+ {
+ return 0;
+ }
+
+ if (tmpValue == 1)
+ {
+ return 2;
+ }
+
+ tmpValue--;
+
+ uint tmpBitStream = _bitStream >> (32 - tmpValue);
+
+ LoadIntoBitstream((byte)tmpValue);
+
+ return (int)(tmpBitStream + (1 << tmpValue));
+ }
+
+ private static void FillSmallDicts(byte length, byte bits, sbyte specialInd)
+ {
+ byte tmpValue1 = (byte)(_bitStream >> (32 - bits)); // NOTE: bits is never 0.
+
+ LoadIntoBitstream(bits);
+
+ if (tmpValue1 != 0)
+ {
+ byte tmpValue2 = 0;
+
+ if (tmpValue1 > 0)
+ {
+ while (tmpValue2 < tmpValue1)
+ {
+ uint tmpBitStream = (byte)(_bitStream >> 29);
+ bits = 3;
+
+ if (tmpBitStream == 7)
+ {
+ uint mask = 0x10000000;
+
+ if ((_bitStream & mask) == 0)
+ {
+ bits = 4;
+ }
+ else
+ {
+ byte counter = 0;
+
+ while ((_bitStream & mask) != 0)
+ {
+ mask >>= 1;
+ counter += 1;
+ }
+
+ bits = (byte)(counter + 4);
+ tmpBitStream += counter;
+ }
+ }
+
+ LoadIntoBitstream(bits);
+
+ _smallByteBuffer[tmpValue2] = (byte)tmpBitStream;
+
+ tmpValue2++;
+
+ if (tmpValue2 == specialInd)
+ {
+ sbyte specialLength = (sbyte)(_bitStream >> 30);
+
+ LoadIntoBitstream(2);
+
+ if (specialLength >= 1)
+ {
+ Array.Clear(_smallByteBuffer, tmpValue2, specialLength);
+
+ tmpValue2 += (byte)specialLength;
+ }
+ }
+ }
+ }
+
+ if (tmpValue2 < length)
+ {
+ Array.Clear(_smallByteBuffer, tmpValue2, length - tmpValue2);
+ }
+
+ FillWordsUsingBytes(length, _smallByteBuffer, 8, _smallWordBuffer);
+
+ return;
+ }
+
+ tmpValue1 = (byte)(_bitStream >> (32 - bits));
+
+ LoadIntoBitstream(bits);
+
+ if (length > 0)
+ {
+ Array.Clear(_smallByteBuffer, 0, length);
+ }
+
+ for (int i = 0; i < _smallWordBuffer.Length; i++)
+ {
+ _smallWordBuffer[i] = tmpValue1;
+ }
+ }
+
+ private static void FillLargeDicts()
+ {
+ short tmpValue1 = (short)(_bitStream >> 23);
+
+ LoadIntoBitstream(9);
+
+ if (tmpValue1 == 0)
+ {
+ short tmpValue2 = (short)(_bitStream >> 23);
+
+ LoadIntoBitstream(9);
+
+ Array.Clear(_largeByteBuffer, 0, _largeByteBuffer.Length);
+
+ for (int i = 0; i < _largeWordBuffer.Length; i++)
+ {
+ _largeWordBuffer[i] = tmpValue2;
+ }
+
+ return;
+ }
+
+ ushort bytes = 0;
+ if (tmpValue1 < 0)
+ {
+ // NOTE: Does this ever happen?
+
+ Array.Clear(_largeByteBuffer, 0, _largeByteBuffer.Length);
+
+ FillWordsUsingBytes((short)_largeByteBuffer.Length, _largeByteBuffer, 12, _largeWordBuffer);
+
+ return;
+ }
+
+ while (bytes < tmpValue1)
+ {
+ ushort tmpLength = (ushort)(_bitStream >> 24);
+ short tmpValue2 = _smallWordBuffer[tmpLength];
+
+ if (tmpValue2 >= 19)
+ {
+ uint mask = 0x800000;
+
+ do
+ {
+ tmpValue2 = ((_bitStream & mask) == 0) ? _parallelBuffer0[tmpValue2] : _parallelBuffer1[tmpValue2];
+ mask >>= 1;
+ } while (tmpValue2 >= 19);
+ }
+
+ LoadIntoBitstream(_smallByteBuffer[tmpValue2]);
+
+ if (tmpValue2 > 2)
+ {
+ tmpValue2 -= 2;
+ _largeByteBuffer[bytes++] = (byte)tmpValue2;
+ }
+ else
+ {
+ if (tmpValue2 == 0)
+ {
+ tmpLength = 1;
+ }
+ else if (tmpValue2 == 1)
+ {
+ tmpValue2 = (short)(_bitStream >> 28);
+
+ LoadIntoBitstream(4);
+
+ tmpLength = (ushort)(tmpValue2 + 3);
+ }
+ else
+ {
+ tmpValue2 = (short)(_bitStream >> 23);
+
+ LoadIntoBitstream(9);
+
+ tmpLength = (ushort)(tmpValue2 + 20);
+ }
+
+ if (tmpLength > 0)
+ {
+ Array.Clear(_largeByteBuffer, bytes, tmpLength);
+
+ bytes += tmpLength;
+ }
+ }
+ }
+
+ if (bytes < _largeByteBuffer.Length)
+ {
+ Array.Clear(_largeByteBuffer, bytes, _largeByteBuffer.Length - bytes);
+ }
+
+ FillWordsUsingBytes((short)_largeByteBuffer.Length, _largeByteBuffer, 12, _largeWordBuffer);
+
+ return;
+ }
+
+ private static void FillWordsUsingBytes(short bytesLength, byte[] bytesBuffer, short pivot, short[] wordsBuffer)
+ {
+ ushort[] srcBuffer = new ushort[17];
+ ushort[] destBuffer = new ushort[18];
+
+ for (int i = 0; i < bytesLength; i++)
+ {
+ srcBuffer[bytesBuffer[i]]++;
+ }
+
+ sbyte shift = 14;
+ sbyte ind = 1;
+ ushort low, high;
+
+ while (ind <= 16)
+ {
+ low = srcBuffer[ind];
+ high = srcBuffer[ind + 1];
+ low <<= shift + 1;
+ high <<= shift;
+ low += destBuffer[ind];
+ ind += 4;
+ high += low;
+ high &= 0xFFFF;
+
+ destBuffer[ind - 3] = low;
+ destBuffer[ind - 2] = high;
+
+ low = (ushort)(srcBuffer[ind - 2] << (shift - 1));
+ low += high;
+ high = (ushort)(srcBuffer[ind - 1] << (shift - 2));
+ high += low;
+
+ destBuffer[ind - 1] = low;
+ destBuffer[ind] = high;
+
+ shift -= 4;
+ }
+
+ if (destBuffer[17] != 0)
+ {
+ throw new Exception("Wrong table.");
+ }
+
+ shift = (sbyte)(pivot - 1);
+
+ int tmpValue = 16 - pivot;
+ int tmpValueCopy = tmpValue;
+
+ for (int i = 1; i <= pivot; ++i)
+ {
+ destBuffer[i] >>= tmpValue;
+ srcBuffer[i] = (ushort)(1 << shift--);
+ }
+
+ tmpValue--;
+
+ for (int i = pivot + 1; i <= 16; ++i)
+ {
+ srcBuffer[i] = (ushort)(1 << tmpValue--);
+ }
+
+ ushort comp1 = destBuffer[pivot + 1];
+ comp1 >>= 16 - pivot;
+
+ if (comp1 != 0)
+ {
+ ushort comp2 = (ushort)(1 << pivot);
+
+ if (comp1 != comp2)
+ {
+ for (int i = comp1; i < comp2; i++)
+ {
+ wordsBuffer[i] = 0;
+ }
+ }
+ }
+
+ if (bytesLength <= 0)
+ {
+ return;
+ }
+
+ shift = (sbyte)(15 - pivot);
+
+ ushort mask = (ushort)(1 << shift);
+ short tmpValue2 = bytesLength;
+
+ for (int i = 0; i < bytesLength; i++)
+ {
+ byte tmpByte = bytesBuffer[i];
+ if (tmpByte != 0)
+ {
+ ushort destValue = destBuffer[tmpByte];
+ ushort srcValue = srcBuffer[tmpByte];
+
+ srcValue += destValue;
+
+ if (tmpByte > pivot)
+ {
+ short[] tmpBuffer = wordsBuffer;
+ short tmpOffset = (short)(destValue >> tmpValueCopy);
+ byte newLength = (byte)(tmpByte - pivot);
+
+ if (newLength != 0)
+ {
+ while (newLength != 0)
+ {
+ if (tmpBuffer[tmpOffset] == 0)
+ {
+ _parallelBuffer0[tmpValue2] = 0;
+ _parallelBuffer1[tmpValue2] = 0;
+
+ tmpBuffer[tmpOffset] = tmpValue2++;
+ }
+
+ tmpOffset = tmpBuffer[tmpOffset];
+ tmpBuffer = ((destValue & mask) == 0) ? _parallelBuffer0 : _parallelBuffer1;
+ destValue += destValue;
+ newLength--;
+ }
+ }
+
+ tmpBuffer[tmpOffset] = (short)i;
+ }
+ else if (destValue < srcValue)
+ {
+ for (int j = destValue; j < srcValue; j++)
+ {
+ wordsBuffer[j] = (short)i;
+ }
+ }
+
+ destBuffer[tmpByte] = srcValue;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TTGamesExplorerRebirthLib/Encryption/RC4.cs b/src/TTGamesExplorerRebirthLib/Encryption/RC4.cs
new file mode 100644
index 0000000..73ef079
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Encryption/RC4.cs
@@ -0,0 +1,95 @@
+namespace TTGamesExplorerRebirthLib.Encryption
+{
+ public static class RC4
+ {
+ ///
+ /// Give data and an encryption key, apply RC4 cryptography. RC4 is symmetric,
+ /// which means this single method will work for encrypting and decrypting.
+ ///
+ ///
+ /// Original implementation by Christopher Whitley:
+ /// https://github.com/manbeardgames/RC4
+ ///
+ /// https://en.wikipedia.org/wiki/RC4
+ ///
+ ///
+ /// Byte array representing the data to be encrypted/decrypted.
+ ///
+ ///
+ /// Byte array representing the key to use.
+ ///
+ ///
+ /// Byte array representing the encrypted/decrypted data.
+ ///
+ public static byte[] Crypt(byte[] data, byte[] key)
+ {
+ // Key Scheduling Algorithm Phase:
+ // KSA Phase Step 1: First, the entries of S are set equal to the values of 0 to 255 in ascending order.
+ int[] s = new int[256];
+ for (int _ = 0; _ < 256; _++)
+ {
+ s[_] = _;
+ }
+
+ // KSA Phase Step 2a: Next, a temporary vector T is created.
+ int[] t = new int[256];
+
+ // KSA Phase Step 2b: If the length of the key k is 256 bytes, then k is assigned to T.
+ if (key.Length == 256)
+ {
+ Buffer.BlockCopy(key, 0, t, 0, key.Length);
+ }
+ else
+ {
+ // Otherwise, for a key with a given length, copy the elements of
+ // the key into vector T, repeating for as many times as neccessary to fill T.
+ for (int _ = 0; _ < 256; _++)
+ {
+ t[_] = key[_ % key.Length];
+ }
+ }
+
+ // KSA Phase Step 3: We use T to produce the initial permutation of S...
+ int i, j = 0;
+ for (i = 0; i < 256; i++)
+ {
+ // Increment j by the sum of S[i] and T[i], however keeping it within the
+ // range of 0 to 255 using mod (%) division.
+ j = (j + s[i] + t[i]) % 256;
+
+ // Swap the values of S[i] and S[j]
+ (s[j], s[i]) = (s[i], s[j]);
+ }
+
+ // Pseudo random generation algorithm (Stream Generation):
+ // Once the vector S is initialized from above in the Key Scheduling Algorithm Phase,
+ // the input key is no longer used. In this phase, for the length of the data, we...
+ i = j = 0;
+ byte[] result = new byte[data.Length];
+ for (int iteration = 0; iteration < data.Length; iteration++)
+ {
+ // PRGA Phase Step 1. Continously increment i from 0 to 255, starting it back
+ // at 0 once we go beyond 255 (this is done with mod (%) division.
+ i = (i + 1) % 256;
+
+ // PRGA Phase Step 2. Lookup the i'th element of S and add it to j, keeping the
+ // result within the range of 0 to 255 using mod (%) division.
+ j = (j + s[i]) % 256;
+
+ // PRGA Phase Step 3. Swap the values of S[i] and S[j].
+ (s[j], s[i]) = (s[i], s[j]);
+
+ // PRGA Phase Step 4. Use the result of the sum of S[i] and S[j], mod (%) by 256,
+ // to get the index of S that handls the value of the stream value K.
+ int k = s[(s[i] + s[j]) % 256];
+
+ // PRGA Phase Step 5. Use bitwise exclusive OR (^) with the next byte in the data to
+ // produce the next byte of the resulting ciphertext (when
+ // encrypting) or plaintext (when decrypting).
+ result[iteration] = Convert.ToByte(data[iteration] ^ k);
+ }
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TTGamesExplorerRebirthLib/Formats/CBX.cs b/src/TTGamesExplorerRebirthLib/Formats/CBX.cs
new file mode 100644
index 0000000..c5acd79
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/CBX.cs
@@ -0,0 +1,486 @@
+using TTGamesExplorerRebirthLib.Helper;
+
+namespace TTGamesExplorerRebirthLib.Formats
+{
+ ///
+ /// Give cbx file data, convert it to wav format.
+ ///
+ ///
+ /// Based on CBXDecoder (unlicensed) by Connor Harrison:
+ /// https://github.com/connorh315/CBXDecoder
+ ///
+ /// Code optimized by Ac_K.
+ ///
+ public class CBX
+ {
+ private const string MagicBox = "!B0X";
+ private const int ChunkSize = 0x1B0;
+
+ private readonly float[] _exponentTable = new float[64];
+
+ private readonly float[] _floatTable = [
+ 0.000000f, -0.996776f, -0.990327f, -0.983879f, -0.977431f, -0.970982f, -0.964534f, -0.958085f,
+ -0.951637f, -0.930754f, -0.904960f, -0.879167f, -0.853373f, -0.827579f, -0.801786f, -0.775992f,
+ -0.750198f, -0.724405f, -0.698611f, -0.670635f, -0.619048f, -0.567460f, -0.515873f, -0.464286f,
+ -0.412698f, -0.361111f, -0.309524f, -0.257937f, -0.206349f, -0.154762f, -0.103175f, -0.051587f,
+ 0.000000f, 0.051587f, 0.103175f, 0.154762f, 0.206349f, 0.257937f, 0.309524f, 0.361111f,
+ 0.412698f, 0.464286f, 0.515873f, 0.567460f, 0.619048f, 0.670635f, 0.698611f, 0.724405f,
+ 0.750198f, 0.775992f, 0.801786f, 0.827579f, 0.853373f, 0.879167f, 0.904960f, 0.930754f,
+ 0.951637f, 0.958085f, 0.964534f, 0.970982f, 0.977431f, 0.983879f, 0.990327f, 0.996776f,
+ ];
+
+ private readonly byte[] _smallTable = [
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x15,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x19,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x16,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x00,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x15,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x1A,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x16,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12,
+ 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x02,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1B,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x01,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1C,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x03,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1B,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x01,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1C,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18,
+ 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x03,
+ ];
+
+ private readonly int[] _wordTable = [
+ 1, 8, 0, 1, 7, 0, 0, 8, 0, 0, 7, 0, 0, 2, 0, 0,
+ 2, -1082130432, 0, 2, 1065353216, 0, 3, -1082130432, 0, 3, 1065353216, 1, 4, -1073741824, 1, 4,
+ 1073741824, 1, 3, -1073741824, 1, 3, 1073741824, 1, 5, -1069547520, 1, 5, 1077936128, 1, 4, -1069547520,
+ 1, 4, 1077936128, 1, 6, -1065353216, 1, 6, 1082130432, 1, 5, -1065353216, 1, 5, 1082130432, 1,
+ 7, -1063256064, 1, 7, 1084227584, 1, 6, -1063256064, 1, 6, 1084227584, 1, 8, -1061158912, 1, 8,
+ 1086324736, 1, 7, -1061158912, 1, 7, 1086324736, 885592027, 1243806083, 1050253722, 20, 1065353216, 1065353216, 1036831949, 1061997773, 1117782016,
+ 1117782016, 1031127695, 1050253722, 1045220557, 1058642330, 1061997773, 1065353216, 23896536, 0, 4194304, 0, 149718852, 149718852, 0, 27225808, 27225816,
+ 1, 0, -1082130432, 1077936128, -1069547520, 1065353216, 1077936128, -1061158912, 1077936128, 0, -1069547520, 1077936128, 0, 0, 1065353216, 0,
+ 0, 0, 24903876, 0,
+ ];
+
+ private int _offset = 0;
+ private int _activeByte = 0;
+ private int _comparison = 8;
+
+ private byte[] _chunkBuffer;
+
+ private readonly float[] _firstLookup = new float[12];
+ private readonly float[] _lookup = new float[12];
+ private readonly float[] _decodeTable = new float[1000];
+ private readonly float[] _collection = new float[120];
+ private readonly float[] _data = new float[348];
+ private readonly float[] _espResult = new float[12];
+ private readonly float[] _otherResult = new float[12];
+
+ public byte[] WaveBuffer { get; private set; }
+
+ public CBX(string filePath)
+ {
+ Convert(File.ReadAllBytes(filePath));
+ }
+
+ public CBX(byte[] buffer)
+ {
+ Convert(buffer);
+ }
+
+ private void Convert(byte[] buffer)
+ {
+ // Initialize.
+
+ for (int i = 0; i < _exponentTable.Length; ++i)
+ {
+ _exponentTable[i] = (float)(59.9246f * Math.Pow(1.068f, i + 1));
+ }
+
+ // Read Header.
+
+ using MemoryStream stream = new(buffer);
+ using BinaryReader reader = new(stream);
+
+ if (reader.ReadUInt32AsString() != MagicBox)
+ {
+ throw new InvalidDataException($"{stream.Position:x8}");
+ }
+
+ int outputSize = reader.ReadInt32();
+ int sampleRate = reader.ReadInt32();
+
+ // Convert the file to WAV.
+
+ _chunkBuffer = reader.ReadBytes((int)(stream.Length - stream.Position));
+ _activeByte = _offset + 1 > _chunkBuffer.Length ? (byte)0 : _chunkBuffer[_offset++];
+
+ int chunkOffset = 0;
+ ushort[] chunkData = new ushort[outputSize];
+
+ while (_offset < _chunkBuffer.Length)
+ {
+ GenerateDecodeTable();
+
+ for (int i = 0; i < ChunkSize; i++)
+ {
+ uint value = BitConverter.SingleToUInt32Bits(_decodeTable[i]) & 0x1FFFF;
+ if (value - 0x8000 < 0x10000)
+ {
+ value = (uint)(0x8000 - (value < 0x10000 ? 1 : 0));
+ }
+
+ chunkData[chunkOffset + i] = (ushort)value;
+
+ _decodeTable[i] = 0.0f;
+ }
+
+ chunkOffset += ChunkSize;
+ }
+
+ // Write WAV header.
+
+ using MemoryStream outputStream = new();
+ using BinaryWriter writer = new(outputStream);
+
+ writer.WriteStringWithoutPrefixedSize("RIFF");
+ writer.Write(2 * chunkOffset + 32);
+ writer.WriteStringWithoutPrefixedSize("WAVE");
+ writer.WriteStringWithoutPrefixedSize("fmt ");
+ writer.Write(16);
+ writer.Write((short)1);
+ writer.Write((short)1);
+ writer.Write(sampleRate);
+ writer.Write(sampleRate * 2);
+ writer.Write((short)2);
+ writer.Write((short)16);
+ writer.WriteStringWithoutPrefixedSize("data");
+ writer.Write(2 * chunkOffset);
+
+ // Write WAV data.
+
+ for (int i = 0; i < chunkOffset; i++)
+ {
+ writer.Write(chunkData[i]);
+ }
+
+ WaveBuffer = outputStream.ToArray();
+ }
+
+ private void RefreshActiveByte(int value)
+ {
+ _comparison -= value;
+ _activeByte >>= value;
+
+ if (_comparison >= 8)
+ {
+ return;
+ }
+
+ _activeByte |= (_offset + 1 > _chunkBuffer.Length ? (byte)0 : _chunkBuffer[_offset++]) << _comparison;
+ _comparison += 8;
+ }
+
+ private void GenerateDecodeTable()
+ {
+ bool unknownFlag = (_activeByte & 63) < 24;
+
+ for (int i = 0; i < 4; i++)
+ {
+ _lookup[i] = (float)((_floatTable[_activeByte & 63] - _data[i]) * 0.25);
+
+ RefreshActiveByte(6);
+ }
+
+ for (int i = 4; i < 12; i++)
+ {
+ _lookup[i] = (float)((_floatTable[(_activeByte & 31) + 16] - _data[i]) * 0.25);
+
+ RefreshActiveByte(5);
+ }
+
+ int unk0 = 0;
+
+ for (int i = 216; i < 648; i += 108)
+ {
+ int unk1 = _activeByte & byte.MaxValue;
+
+ RefreshActiveByte(8);
+
+ int offset = i - unk1;
+ float unk2 = (_activeByte & 15) * 0.06666667f;
+
+ RefreshActiveByte(4);
+
+ float unk3 = _exponentTable[_activeByte & 63];
+
+ RefreshActiveByte(6);
+
+ int unk4 = _activeByte & 1;
+
+ RefreshActiveByte(1);
+
+ int unk5 = _activeByte & 1;
+
+ RefreshActiveByte(1);
+
+ int collectionOffsetBase = 8;
+
+ if (!unknownFlag)
+ {
+ int unk6 = 0;
+
+ do
+ {
+ switch (_activeByte & 3)
+ {
+ case 0:
+ case 2:
+ {
+ _collection[collectionOffsetBase - 1 + unk4 + unk6] = 0.0f;
+
+ RefreshActiveByte(1);
+ break;
+ }
+ case 1:
+ {
+ _collection[collectionOffsetBase - 1 + unk4 + unk6] = -2f;
+
+ RefreshActiveByte(2);
+ break;
+ }
+ case 3:
+ {
+ _collection[collectionOffsetBase - 1 + unk4 + unk6] = 2f;
+
+ RefreshActiveByte(2);
+ break;
+ }
+ }
+
+ unk6 += 2;
+ }
+ while (unk6 < 108);
+ }
+ else
+ {
+ int unk7 = 0;
+ int unk8 = 0;
+
+ do
+ {
+ byte unk9 = _smallTable[(_activeByte & byte.MaxValue) + (unk7 << 8)];
+ unk7 = _wordTable[unk9 * 3];
+
+ RefreshActiveByte(_wordTable[(unk9 * 3) + 1]);
+
+ if (unk9 < 4)
+ {
+ if (unk9 < 2)
+ {
+ int unk10 = 7;
+
+ while (true)
+ {
+ int unk11 = _activeByte & 1;
+
+ RefreshActiveByte(1);
+
+ if (unk11 == 1)
+ {
+ unk10++;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ int unk12 = _activeByte & 1;
+
+ RefreshActiveByte(1);
+
+ _collection[collectionOffsetBase - 1 + unk4 + unk8] = unk12 != 1 ? -unk10 : unk10;
+
+ unk8 += 2;
+ }
+ else
+ {
+ int unk13 = (_activeByte & 63) + 7;
+
+ RefreshActiveByte(6);
+
+ if (unk13 * 2 + unk8 > 108)
+ {
+ unk13 = (108 - unk8) / 2;
+ }
+
+ if (unk13 > 0)
+ {
+ do
+ {
+ _collection[collectionOffsetBase - 1 + unk4 + unk8] = 0.0f;
+ unk8 += 2;
+ unk13--;
+ }
+ while (unk13 != 0);
+ }
+ }
+ }
+ else
+ {
+ _collection[collectionOffsetBase - 1 + unk4 + unk8] = BitConverter.Int32BitsToSingle(_wordTable[(unk9 * 3) + 2]);
+ unk8 += 2;
+ }
+ }
+ while (unk8 <= 107);
+ }
+
+ if (unk5 == 0)
+ {
+ for (int j = collectionOffsetBase + 1 - unk4; j < 107 + collectionOffsetBase + 1 - unk4; j += 2)
+ {
+ _collection[j - 1] = (_collection[j - 2] + _collection[j]) * 0.597385942935944f - (_collection[j - 4] + _collection[j + 2]) * 0.114591561257839f + (_collection[j - 6] + _collection[j + 4]) * 0.0180326793342829f;
+ }
+
+ unk3 *= 0.5f;
+ }
+ else
+ {
+ for (int j = 0; j < 54; ++j)
+ {
+ _collection[collectionOffsetBase - unk4 + j * 2] = 0.0f;
+ }
+ }
+
+ int unk14 = offset + 1;
+ int collectionOffset = collectionOffsetBase;
+
+ for (int j = 0; j < 12; ++j)
+ {
+ for (int k = 0; k < 9; k++)
+ {
+ _decodeTable[unk0 + k] = _collection[collectionOffset + (k - 1)] * unk3 + (unk14 + (k - 1) >= 324 ? _decodeTable[unk14 + 27 - (352 - k)] : _data[unk14 + 27 + (k - 4)]) * unk2;
+ }
+
+ collectionOffset += 9;
+ unk14 += 9;
+ unk0 += 9;
+ }
+ }
+
+ for (int i = 0; i < 324; i++)
+ {
+ _data[24 + i] = _decodeTable[108 + i];
+ }
+
+ int unk15 = 0;
+ for (int i = 0; i < 4; i++)
+ {
+ for (int j = 0; j != 12; j += 6)
+ {
+ for (int k = 0; k < 6; k++)
+ {
+ _data[j + k] += _lookup[j + k];
+ }
+ }
+
+ int unk16 = i != 3 ? 1 : 33;
+
+ for (int j = 11; j > 0; j--)
+ {
+ _firstLookup[j] = _data[j - 1];
+ }
+
+ _firstLookup[0] = 1.0f;
+
+ for (int j = 0; j < 12; j++)
+ {
+ float unk17 = -_data[11] * _firstLookup[11] - _data[10] * _firstLookup[10];
+
+ for (int k = 10; k > 0; k--)
+ {
+ _firstLookup[k + 1] = _firstLookup[k] + _data[k] * unk17;
+ unk17 -= _data[k - 1] * _firstLookup[k - 1];
+ }
+
+ _firstLookup[1] = _firstLookup[0] + _data[0] * unk17;
+ _firstLookup[0] = unk17;
+ _espResult[j] = unk17;
+
+ int unk18 = 0;
+ if (j > 3)
+ {
+ int unk19 = (j - 4 >> 2) + 1;
+ unk18 = unk19 * 4;
+ int unk20 = 0;
+ do
+ {
+ unk19--;
+ unk17 = unk17 - _espResult[j - 1 - unk20 * 4] * _otherResult[unk20 * 4] - _espResult[j - 2 - unk20 * 4] * _otherResult[unk20 * 4 + 1] - _espResult[j - 3 - unk20 * 4] * _otherResult[unk20 * 4 + 2] - _espResult[j - 4 - unk20 * 4] * _otherResult[unk20 * 4 + 3];
+ unk20++;
+ }
+ while (unk19 != 0);
+ }
+
+ if (j > unk18)
+ {
+ do
+ {
+ unk17 -= _espResult[j - unk18 - 1] * _otherResult[unk18];
+ ++unk18;
+ }
+ while (j > unk18);
+ }
+
+ _otherResult[j] = unk17;
+ }
+
+ if (unk16 <= 0)
+ {
+ return;
+ }
+
+ do
+ {
+ List listIndex = [23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12];
+
+ int checkIndex(int i)
+ {
+ if (i == listIndex.Count)
+ {
+ i = 0;
+ }
+
+ return i % listIndex.Count;
+ }
+
+ for (int k = 0; k < 12; k++)
+ {
+
+ _data[listIndex[checkIndex(k + 12)]] = (_data[listIndex[checkIndex(k + 11)]] * _otherResult[0] + _decodeTable[unk15 + k] + _data[listIndex[checkIndex(k + 10)]] * _otherResult[1] + _data[listIndex[checkIndex(k + 9)]] * _otherResult[2] + _data[listIndex[checkIndex(k + 8)]] * _otherResult[3] + _data[listIndex[checkIndex(k + 7)]] * _otherResult[4] + _data[listIndex[checkIndex(k + 6)]] * _otherResult[5] + _data[listIndex[checkIndex(k + 5)]] * _otherResult[6] + _data[listIndex[checkIndex(k + 4)]] * _otherResult[7] + _data[listIndex[checkIndex(k + 3)]] * _otherResult[8] + _data[listIndex[checkIndex(k + 2)]] * _otherResult[9] + _data[listIndex[checkIndex(k + 1)]] * _otherResult[10] + _data[listIndex[checkIndex(k)]] * _otherResult[11]);
+ _decodeTable[unk15 + k] = _data[listIndex[checkIndex(k + 12)]] + 12582912f;
+ }
+
+ unk15 += 12;
+ --unk16;
+ }
+ while (unk16 != 0);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DAT/CompressionFormat.cs b/src/TTGamesExplorerRebirthLib/Formats/DAT/CompressionFormat.cs
new file mode 100644
index 0000000..224a5fb
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DAT/CompressionFormat.cs
@@ -0,0 +1,10 @@
+namespace TTGamesExplorerRebirthLib.Formats.DAT
+{
+ public enum CompressionFormat
+ {
+ None,
+ LZ2K,
+ ZIPX,
+ Deflate_v1,
+ }
+}
\ No newline at end of file
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DAT/DATArchive.cs b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATArchive.cs
new file mode 100644
index 0000000..20cd2c1
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATArchive.cs
@@ -0,0 +1,454 @@
+using TTGamesExplorerRebirthLib.Hash;
+using TTGamesExplorerRebirthLib.Helper;
+
+namespace TTGamesExplorerRebirthLib.Formats.DAT
+{
+ ///
+ /// Give DAT file path and get a list of contained files through "Files" field.
+ ///
+ /// Files can be extracted using "ExtractFile" method.
+ ///
+ ///
+ /// Based on QuickBMS script by Luigi Auriemma:
+ /// https://aluigi.altervista.org/quickbms.htm
+ ///
+ /// Based on my own research (Ac_K).
+ ///
+ /// Games supported:
+ /// - LEGO The Lord of the Rings
+ /// - LEGO Star Wars - The Complete Saga
+ /// - LEGO Worlds
+ ///
+ ///
+ public class DATArchive
+ {
+ public const string MagicCC40TAD = ".CC40TAD";
+
+ public List Files = [];
+
+ public string ArchiveFilePath;
+
+ private struct TempFile
+ {
+ public ulong Offset;
+ public uint Size;
+ public uint CompressedSize;
+ public CompressionFormat Compression;
+ }
+
+ // TODO: Improve the deserializer.
+ public DATArchive(string archiveFilePath)
+ {
+ ArchiveFilePath = archiveFilePath;
+
+ using FileStream stream = new(archiveFilePath, FileMode.Open, FileAccess.Read);
+ using BinaryReader reader = new(stream);
+
+ // Read DAT header.
+
+ uint infoTableOffset = reader.ReadUInt32();
+
+ if ((infoTableOffset & 0x80000000) != 0)
+ {
+ infoTableOffset ^= 0xFFFFFFFF;
+ infoTableOffset <<= 8;
+ infoTableOffset += 0x100;
+ }
+
+ uint infoTableSize = reader.ReadUInt32();
+
+ stream.Seek(infoTableOffset, SeekOrigin.Begin);
+
+ uint versionType1 = reader.ReadUInt32();
+ uint versionType2 = reader.ReadUInt32();
+
+ stream.Seek(infoTableOffset, SeekOrigin.Begin);
+
+ // TODO: Improve this check.
+ if (versionType1.ToConvertedString() == "4CC." ||
+ versionType1.ToConvertedString() == ".CC4" ||
+ versionType2.ToConvertedString() == "4CC." ||
+ versionType2.ToConvertedString() == ".CC4")
+ {
+ // Read DAT info table.
+
+ uint datInfoTableSize = reader.ReadUInt32();
+
+ if (reader.ReadUInt64AsString() != MagicCC40TAD)
+ {
+ throw new InvalidDataException($"{stream.Position:x8}");
+ }
+
+ int formatByteOrder1 = reader.ReadInt32BigEndian(); // -11
+ uint formatVersion = reader.ReadUInt32BigEndian();
+ uint filesCount1 = reader.ReadUInt32BigEndian();
+ uint namesCount = reader.ReadUInt32BigEndian();
+ uint namesSize = reader.ReadUInt32BigEndian();
+ ulong namesOffset = (ulong)stream.Position;
+
+ stream.Seek(namesSize, SeekOrigin.Current);
+
+ ushort unknown2 = reader.ReadUInt16BigEndian(); // 0x1000
+ ushort unknown3 = reader.ReadUInt16BigEndian(); // -1
+
+ // Read file names table.
+
+ uint lastNameWorkaround = namesCount - 1;
+ ushort myId = 0;
+
+ string[] namesTemp = new string[namesCount];
+ string[] namesList = new string[filesCount1];
+
+ for (int i = 0; i < namesCount; i++)
+ {
+ uint nameOffset = reader.ReadUInt32BigEndian();
+ ushort folderId = reader.ReadUInt16BigEndian();
+ ushort unknown1Id = reader.ReadUInt16BigEndian();
+ ushort unknown2Id = reader.ReadUInt16BigEndian();
+ ushort fileId = reader.ReadUInt16BigEndian();
+
+ if (nameOffset != 0xFFFFFFFF)
+ {
+ long latestOffset = stream.Position;
+
+ stream.Seek((long)namesOffset + nameOffset, SeekOrigin.Begin);
+
+ string fileName = stream.ReadNullTerminatedString();
+
+ stream.Seek(latestOffset, SeekOrigin.Begin);
+
+ // TODO: Find a proper solution.
+ if (i == lastNameWorkaround)
+ {
+ fileId = myId;
+ }
+
+ fileName = $"{namesTemp[folderId]}\\{fileName}";
+
+ if (fileId != 0)
+ {
+ namesList[myId] = fileName;
+ myId++;
+ }
+ else
+ {
+ namesTemp[i] = fileName;
+ }
+ }
+ }
+
+ // Read files info table.
+
+ int formatByteOrder2 = reader.ReadInt32BigEndian(); // -11 / Version ?
+ uint filesCount2 = reader.ReadUInt32BigEndian();
+
+ if (filesCount2 != filesCount1)
+ {
+ throw new IndexOutOfRangeException($"{stream.Position:x8}");
+ }
+
+ TempFile[] tempFilesList = new TempFile[filesCount2];
+
+ for (int i = 0; i < filesCount2; i++)
+ {
+ tempFilesList[i].Offset = (formatByteOrder2 <= -11) ? reader.ReadUInt64BigEndian() : reader.ReadUInt32BigEndian();
+
+ uint compressedSize = reader.ReadUInt32BigEndian();
+ uint size = reader.ReadUInt32BigEndian();
+
+ // TODO: Rework this once more format versions will be added.
+ uint compressed;
+ if (formatByteOrder2 <= -11)
+ {
+ compressed = size;
+ size &= 0x7FFFFFFF;
+ compressed >>= 31;
+ }
+ else
+ {
+ compressed = reader.ReadByte();
+
+ ushort zero = reader.ReadUInt16();
+
+ tempFilesList[i].Offset |= (uint)(reader.ReadByte() << 8);
+ }
+
+ tempFilesList[i].Size = size;
+ tempFilesList[i].CompressedSize = compressedSize;
+ tempFilesList[i].Compression = GetFileCompressionFormat(stream, reader, tempFilesList[i].Offset);
+ }
+
+ // Read FNV1a table.
+
+ uint[] fnv1aFilesList = new uint[filesCount2];
+ for (int i = 0; i < filesCount2; i++)
+ {
+ fnv1aFilesList[i] = reader.ReadUInt32BigEndian();
+ }
+
+ // Compute FNV1a of generated names path.
+
+ uint[] fnv1aNamesList = GenerateFnv1aNamesList(namesList, filesCount1);
+
+ // Merge all infos.
+
+ AddToFilesList(filesCount2, fnv1aFilesList, fnv1aNamesList, namesList, tempFilesList);
+ }
+ else
+ {
+ // Read DAT info table.
+
+ int formatByteOrder1 = reader.ReadInt32(); // LSTTCC > -3 - LLOTR > -5 / Version ?
+ uint filesCount1 = reader.ReadUInt32();
+
+ infoTableOffset = (uint)stream.Position;
+ uint nameInfoOffset = infoTableOffset + filesCount1 * 0x10;
+
+ stream.Seek(nameInfoOffset, SeekOrigin.Begin);
+
+ uint namesCount = reader.ReadUInt32();
+ nameInfoOffset = (uint)stream.Position;
+ uint nameFieldSize = (uint)((formatByteOrder1 <= -5) ? 12 : 8);
+ uint namesOffset = nameInfoOffset + (namesCount * nameFieldSize);
+
+ stream.Seek(namesOffset, SeekOrigin.Begin);
+
+ uint namesCrcOffset = reader.ReadUInt32();
+ namesOffset = (uint)stream.Position;
+ namesCrcOffset += (uint)stream.Position;
+
+ stream.Seek(namesCrcOffset, SeekOrigin.Begin);
+
+ ulong fnv1aTableOffset = (ulong)stream.Position;
+
+ // Read FNV1a filenames table.
+
+ uint[] fnv1aFilesList = new uint[filesCount1];
+
+ for (int i = 0; i < fnv1aFilesList.Length; i++)
+ {
+ fnv1aFilesList[i] = reader.ReadUInt32();
+ }
+
+ // Read files infos.
+
+ if (formatByteOrder1 <= -2)
+ {
+ reader.ReadInt32();
+ reader.ReadInt32();
+
+ // TODO: We should be at EOF.
+ if (stream.Position != stream.Length)
+ {
+ throw new IndexOutOfRangeException($"{stream.Position:x8}");
+ }
+ }
+
+ uint nameIndex = 0;
+ string fullName = "";
+ string fullPath = "";
+ string[] namesList = new string[filesCount1];
+ string[] tempArray = new string[ushort.MaxValue]; // TODO: Find a better way to handle that.
+
+ for (int i = 0; i < filesCount1; i++)
+ {
+ short next = 1;
+ string name = "";
+
+ do
+ {
+ stream.Seek(nameInfoOffset, SeekOrigin.Begin);
+
+ next = reader.ReadInt16();
+ short prev = reader.ReadInt16();
+ int offset = reader.ReadInt32();
+
+ if (formatByteOrder1 <= -5) // if (nameFieldSize >= 12)
+ {
+ reader.ReadUInt32();
+ }
+
+ nameInfoOffset = (uint)stream.Position;
+
+ if (offset > 0)
+ {
+ offset += (int)namesOffset;
+
+ stream.Seek(offset, SeekOrigin.Begin);
+
+ name = stream.ReadNullTerminatedString();
+ }
+
+ // NOTE: Used only for LEGO the game if you don't use the hdr file.
+ if (name.Length > 0)
+ {
+ if (name[0] >= 0xf0)
+ {
+ name = "";
+ }
+ }
+
+ if (prev != 0)
+ {
+ fullPath = tempArray[prev];
+ }
+
+ tempArray[(int)nameIndex] = fullPath;
+
+ if (next > 0) // Folder
+ {
+ string tempName = tempArray[prev];
+
+ if (tempName != "") // NOTE: Long story to avoid things like 2foldername that gives problems.
+ {
+ string oldName = @"\"; // Do not use "/"
+ oldName += tempName;
+ oldName += @"\"; // Do not use "/"
+ }
+
+ if (name != "")
+ {
+ fullPath += name;
+ fullPath += @"\"; // Do not use "/"
+ }
+ }
+
+ nameIndex += 1;
+ } while (next > 0);
+
+ fullName = fullPath;
+ fullName += name;
+
+ namesList[i] = $"\\{fullName.ToLowerInvariant()}";
+ }
+
+ TempFile[] tempFilesList = new TempFile[filesCount1];
+
+ for (int i = 0; i < filesCount1; i++)
+ {
+ stream.Seek(infoTableOffset + i * 0x10, SeekOrigin.Begin);
+
+ uint offsetFile = reader.ReadUInt32();
+ uint compressedSize = reader.ReadUInt32();
+ uint size = reader.ReadUInt32();
+ byte[] packed = reader.ReadBytes(3);
+ byte offsetFile2 = reader.ReadByte();
+
+ if (formatByteOrder1 != -1)
+ {
+ offsetFile <<= 8;
+ }
+
+ offsetFile += offsetFile2;
+
+ tempFilesList[i].Offset = offsetFile;
+ tempFilesList[i].Size = size;
+ tempFilesList[i].CompressedSize = compressedSize;
+ tempFilesList[i].Compression = GetFileCompressionFormat(stream, reader, tempFilesList[i].Offset);
+ }
+
+ // Compute FNV1a of generated names path.
+
+ uint[] fnv1aNamesList = GenerateFnv1aNamesList(namesList, filesCount1);
+
+ // Merge all infos.
+
+ AddToFilesList(filesCount1, fnv1aFilesList, fnv1aNamesList, namesList, tempFilesList);
+ }
+ }
+
+ private static uint[] GenerateFnv1aNamesList(string[] namesList, uint filesCount)
+ {
+ uint[] fnv1aNamesList = new uint[filesCount];
+
+ for (int i = 0; i < fnv1aNamesList.Length; i++)
+ {
+ fnv1aNamesList[i] = Fnv.Fnv1a_32_TTGames(namesList[i][1..]);
+ }
+
+ return fnv1aNamesList;
+ }
+
+ private static CompressionFormat GetFileCompressionFormat(FileStream stream, BinaryReader reader, ulong offset)
+ {
+ long oldPosition = stream.Position;
+
+ stream.Seek((long)offset, SeekOrigin.Begin);
+
+ // TODO: Found a better way to know if files are compressed/encrypted.
+ // The byte packed in the size field ("compressed" var) is only available for LZ2K/ZIPX.
+
+ CompressionFormat compressionFormat = reader.ReadUInt32AsString() switch
+ {
+ "LZ2K" => CompressionFormat.LZ2K,
+ "ZIPX" => CompressionFormat.ZIPX,
+ "Defl" => CompressionFormat.Deflate_v1,
+ _ => CompressionFormat.None,
+ };
+
+ stream.Seek(oldPosition, SeekOrigin.Begin);
+
+ return compressionFormat;
+ }
+
+ private void AddToFilesList(uint filesCount, uint[] fnv1aFilesList, uint[] fnv1aNamesList, string[] namesList, TempFile[] tempFilesList)
+ {
+ for (int i = 0; i < filesCount; i++)
+ {
+ int j = Array.IndexOf(fnv1aFilesList, fnv1aNamesList[i]);
+
+ Files.Add(new DATFile(namesList[i], tempFilesList[j].Offset, tempFilesList[j].Size, tempFilesList[j].CompressedSize, tempFilesList[j].Compression));
+ }
+ }
+
+ ///
+ /// Extract DATFile from the DAT archive.
+ ///
+ ///
+ /// DATFile object representing the file to be extracted.
+ ///
+ ///
+ /// Bool to decrypt/decompress the file while it's extracted or not.
+ ///
+ ///
+ /// Byte array representing the file data.
+ ///
+ public byte[] ExtractFile(DATFile file, bool plainData = false)
+ {
+ using FileStream stream = new(ArchiveFilePath, FileMode.Open, FileAccess.Read);
+
+ stream.Seek((long)file.Offset, SeekOrigin.Begin);
+
+ uint size = file.Compression == CompressionFormat.None ? file.Size : file.CompressedSize;
+ byte[] fileData = new byte[size];
+
+ stream.Read(fileData, 0, (int)size);
+
+ if (plainData)
+ {
+ switch (file.Compression)
+ {
+ case CompressionFormat.LZ2K:
+ {
+ fileData = LZ2K.Decompress(fileData);
+ break;
+ }
+
+ case CompressionFormat.ZIPX:
+ {
+ fileData = ZIPX.Decrypt(fileData);
+ break;
+ }
+
+ case CompressionFormat.Deflate_v1:
+ {
+ fileData = Deflatev1.Decompress(fileData);
+ break;
+ }
+ }
+ }
+
+ return fileData;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DAT/DATFile.cs b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATFile.cs
new file mode 100644
index 0000000..71b7377
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATFile.cs
@@ -0,0 +1,36 @@
+namespace TTGamesExplorerRebirthLib.Formats.DAT
+{
+ public class DATFile
+ {
+ public string Path;
+ public ulong Offset;
+ public uint Size;
+ public uint CompressedSize;
+ public CompressionFormat Compression;
+
+ public DATFile(string path, ulong offset, uint size, uint compressedSize, CompressionFormat compression)
+ {
+ Path = path;
+ Offset = offset;
+ Size = size;
+ Compression = compression;
+
+ if (compression != CompressionFormat.None)
+ {
+ CompressedSize = compressedSize;
+ }
+ }
+
+ public override string ToString()
+ {
+ string value = $"Path: {Path}\n\tOffset: 0x{Offset:X8}\n\tSize: 0x{Size:X8}\n";
+
+ if (Compression != CompressionFormat.None)
+ {
+ value += $"\tCompressedSize: 0x{CompressedSize:X8} ({Compression})\n";
+ }
+
+ return value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnDecoderExtensions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnDecoderExtensions.cs
new file mode 100644
index 0000000..d9d8ff2
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnDecoderExtensions.cs
@@ -0,0 +1,250 @@
+using CommunityToolkit.HighPerformance;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using System.Runtime.InteropServices;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.ImageSharp
+{
+ public static class BCnDecoderExtensions
+ {
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ ///
+ /// The stream containing the encoded image.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The format the encoded data is in.
+ /// The decoded Rgba32 image.
+ public static Image DecodeRawToImageRgba32(this BcDecoder decoder, Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ return ColorMemoryToImage(decoder.DecodeRaw2D(inputStream, pixelWidth, pixelHeight, format));
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ ///
+ /// The array containing the encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The format the encoded data is in.
+ /// The decoded Rgba32 image.
+ public static Image DecodeRawToImageRgba32(this BcDecoder decoder, byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ return ColorMemoryToImage(decoder.DecodeRaw2D(input, pixelWidth, pixelHeight, format));
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode the main image from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream that contains either ktx or dds file.
+ /// The decoded Rgba32 image.
+ public static Image DecodeToImageRgba32(this BcDecoder decoder, Stream inputStream)
+ {
+ return ColorMemoryToImage(decoder.Decode2D(inputStream));
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream that contains either ktx or dds file.
+ /// An array of decoded Rgba32 images.
+ public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, Stream inputStream)
+ {
+ var decoded = decoder.DecodeAllMipMaps2D(inputStream);
+ var output = new Image[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ output[i] = ColorMemoryToImage(decoded[i]);
+ }
+ return output;
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The decoded Rgba32 image.
+ public static Image DecodeToImageRgba32(this BcDecoder decoder, KtxFile file)
+ {
+ return ColorMemoryToImage(decoder.Decode2D(file));
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// An array of decoded Rgba32 images.
+ public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, KtxFile file)
+ {
+ var decoded = decoder.DecodeAllMipMaps2D(file);
+ var output = new Image[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ output[i] = ColorMemoryToImage(decoded[i]);
+ }
+ return output;
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The decoded Rgba32 image.
+ public static Image DecodeToImageRgba32(this BcDecoder decoder, DdsFile file)
+ {
+ return ColorMemoryToImage(decoder.Decode2D(file));
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// An array of decoded Rgba32 images.
+ public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, DdsFile file)
+ {
+ var decoded = decoder.DecodeAllMipMaps2D(file);
+ var output = new Image[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ output[i] = ColorMemoryToImage(decoded[i]);
+ }
+ return output;
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ ///
+ /// The stream containing the encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The cancellation token for this asynchronous operation.
+ /// The decoded Rgba32 image.
+ public static async Task> DecodeRawToImageRgba32Async(this BcDecoder decoder, Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default)
+ {
+ return ColorMemoryToImage(await decoder.DecodeRaw2DAsync(inputStream, pixelWidth, pixelHeight, format, token));
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ ///
+ /// The array containing the encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The cancellation token for this asynchronous operation.
+ /// The decoded Rgba32 image.
+ public static async Task> DecodeRawToImageRgba32Async(this BcDecoder decoder, byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default)
+ {
+ return ColorMemoryToImage(await decoder.DecodeRaw2DAsync(input, pixelWidth, pixelHeight, format, token));
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode the main image from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream that contains either ktx or dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The decoded Rgba32 image.
+ public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, Stream inputStream, CancellationToken token = default)
+ {
+ return ColorMemoryToImage(await decoder.Decode2DAsync(inputStream, token));
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream that contains either ktx or dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// An array of decoded Rgba32 images.
+ public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, Stream inputStream, CancellationToken token = default)
+ {
+ var decoded = await decoder.DecodeAllMipMaps2DAsync(inputStream, token);
+ var output = new Image[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ output[i] = ColorMemoryToImage(decoded[i]);
+ }
+ return output;
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The decoded Rgba32 image.
+ public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, KtxFile file, CancellationToken token = default)
+ {
+ return ColorMemoryToImage(await decoder.Decode2DAsync(file, token));
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// An array of decoded Rgba32 images.
+ public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, KtxFile file, CancellationToken token = default)
+ {
+ var decoded = await decoder.DecodeAllMipMaps2DAsync(file, token);
+ var output = new Image[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ output[i] = ColorMemoryToImage(decoded[i]);
+ }
+ return output;
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The decoded Rgba32 image.
+ public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, DdsFile file, CancellationToken token = default)
+ {
+ return ColorMemoryToImage(await decoder.Decode2DAsync(file, token));
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// An array of decoded Rgba32 images.
+ public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, DdsFile file, CancellationToken token = default)
+ {
+ var decoded = await decoder.DecodeAllMipMaps2DAsync(file, token);
+ var output = new Image[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ output[i] = ColorMemoryToImage(decoded[i]);
+ }
+ return output;
+ }
+
+ private static Image ColorMemoryToImage(Memory2D colors)
+ {
+ var output = new Image(colors.Width, colors.Height);
+ for (var y = 0; y < colors.Height; y++)
+ {
+ var yPixels = output.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
+ var yColors = colors.Span.GetRowSpan(y);
+
+ MemoryMarshal.Cast(yColors).CopyTo(yPixels);
+ }
+ return output;
+ }
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnEncoderExtensions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnEncoderExtensions.cs
new file mode 100644
index 0000000..5b03a90
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnEncoderExtensions.cs
@@ -0,0 +1,315 @@
+using CommunityToolkit.HighPerformance;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.PixelFormats;
+using System.Runtime.InteropServices;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.ImageSharp
+{
+ public static class BCnEncoderExtensions
+ {
+ ///
+ /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream.
+ ///
+ /// The image to encode.
+ /// The stream to write the encoded image to.
+ public static void EncodeToStream(this BcEncoder encoder, Image inputImage, Stream outputStream)
+ {
+ encoder.EncodeToStream(ImageToMemory2D(inputImage), outputStream);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Ktx file.
+ ///
+ /// The image to encode.
+ /// The Ktx file containing the encoded image.
+ public static KtxFile EncodeToKtx(this BcEncoder encoder, Image inputImage)
+ {
+ return encoder.EncodeToKtx(ImageToMemory2D(inputImage));
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Dds file.
+ ///
+ /// The image to encode.
+ /// The Dds file containing the encoded image.
+ public static DdsFile EncodeToDds(this BcEncoder encoder, Image inputImage)
+ {
+ return encoder.EncodeToDds(ImageToMemory2D(inputImage));
+ }
+
+ ///
+ /// Encodes all mipmap levels into an array of byte buffers. This data does not contain any file headers, just the raw encoded data.
+ ///
+ /// The image to encode.
+ /// A list of raw encoded data.
+ public static byte[][] EncodeToRawBytes(this BcEncoder encoder, Image inputImage)
+ {
+ return encoder.EncodeToRawBytes(ImageToMemory2D(inputImage));
+ }
+
+ ///
+ /// Encodes a single mip level of the input image to a byte buffer. This data does not contain any file headers, just the raw encoded data.
+ ///
+ /// The image to encode.
+ /// The mipmap to encode.
+ /// The width of the mipmap.
+ /// The height of the mipmap.
+ /// The raw encoded data.
+ public static byte[] EncodeToRawBytes(this BcEncoder encoder, Image inputImage, int mipLevel, out int mipWidth, out int mipHeight)
+ {
+ return encoder.EncodeToRawBytes(ImageToMemory2D(inputImage), mipLevel, out mipWidth, out mipHeight);
+ }
+
+ ///
+ /// Encodes all cubemap faces and mipmap levels into either a ktx or a dds file and writes it to the output stream.
+ /// Order is +X, -X, +Y, -Y, +Z, -Z
+ ///
+ /// The right face of the cubemap.
+ /// The left face of the cubemap.
+ /// The top face of the cubemap.
+ /// The bottom face of the cubemap.
+ /// The back face of the cubemap.
+ /// The front face of the cubemap.
+ /// The stream to write the encoded image to.
+ public static void EncodeCubeMapToStream(this BcEncoder encoder, Image right, Image left, Image top, Image down,
+ Image back, Image front, Stream outputStream)
+ {
+ encoder.EncodeCubeMapToStream(
+ ImageToMemory2D(right),
+ ImageToMemory2D(left),
+ ImageToMemory2D(top),
+ ImageToMemory2D(down),
+ ImageToMemory2D(back),
+ ImageToMemory2D(front),
+ outputStream
+ );
+ }
+
+ ///
+ /// Encodes all cubemap faces and mipmap levels into a Ktx file.
+ /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z.
+ ///
+ /// The right face of the cubemap.
+ /// The left face of the cubemap.
+ /// The top face of the cubemap.
+ /// The bottom face of the cubemap.
+ /// The back face of the cubemap.
+ /// The front face of the cubemap.
+ /// The Ktx file containing the encoded image.
+ public static KtxFile EncodeCubeMapToKtx(this BcEncoder encoder, Image right, Image left, Image top, Image down,
+ Image back, Image front)
+ {
+ return encoder.EncodeCubeMapToKtx(
+ ImageToMemory2D(right),
+ ImageToMemory2D(left),
+ ImageToMemory2D(top),
+ ImageToMemory2D(down),
+ ImageToMemory2D(back),
+ ImageToMemory2D(front)
+ );
+ }
+
+ ///
+ /// Encodes all cubemap faces and mipmap levels into a Dds file.
+ /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z.
+ ///
+ /// The right face of the cubemap.
+ /// The left face of the cubemap.
+ /// The top face of the cubemap.
+ /// The bottom face of the cubemap.
+ /// The back face of the cubemap.
+ /// The front face of the cubemap.
+ /// The Dds file containing the encoded image.
+ public static DdsFile EncodeCubeMapToDds(this BcEncoder encoder, Image right, Image left, Image top, Image down,
+ Image back, Image front)
+ {
+ return encoder.EncodeCubeMapToDds(
+ ImageToMemory2D(right),
+ ImageToMemory2D(left),
+ ImageToMemory2D(top),
+ ImageToMemory2D(down),
+ ImageToMemory2D(back),
+ ImageToMemory2D(front)
+ );
+ }
+
+ ///
+ /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously.
+ ///
+ /// The image to encode.
+ /// The stream to write the encoded image to.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ public static Task EncodeToStreamAsync(this BcEncoder encoder, Image inputImage, Stream outputStream, CancellationToken token = default)
+ {
+ return encoder.EncodeToStreamAsync(ImageToMemory2D(inputImage), outputStream, token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Ktx file asynchronously.
+ ///
+ /// The image to encode.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// The Ktx file containing the encoded image.
+ public static Task EncodeToKtxAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default)
+ {
+ return encoder.EncodeToKtxAsync(ImageToMemory2D(inputImage), token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Dds file asynchronously.
+ ///
+ /// The image to encode.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// The Dds file containing the encoded image.
+ public static Task EncodeToDdsAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default)
+ {
+ return encoder.EncodeToDdsAsync(ImageToMemory2D(inputImage), token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded data.
+ ///
+ /// The image to encode.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// A list of raw encoded data.
+ public static Task EncodeToRawBytesAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default)
+ {
+ return encoder.EncodeToRawBytesAsync(ImageToMemory2D(inputImage), token);
+ }
+
+ ///
+ /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded data.
+ ///
+ /// The image to encode.
+ /// The mipmap to encode.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// The raw encoded data.
+ /// To get the width and height of the encoded mipLevel, see .
+ public static Task EncodeToRawBytesAsync(this BcEncoder encoder, Image inputImage, int mipLevel, CancellationToken token = default)
+ {
+ return encoder.EncodeToRawBytesAsync(ImageToMemory2D(inputImage), mipLevel, token);
+ }
+
+ ///
+ /// Encodes all cubemap faces and mipmap levels into either a ktx or a dds file and writes it to the output stream asynchronously.
+ /// Order is +X, -X, +Y, -Y, +Z, -Z
+ ///
+ /// The right face of the cubemap.
+ /// The left face of the cubemap.
+ /// The top face of the cubemap.
+ /// The bottom face of the cubemap.
+ /// The back face of the cubemap.
+ /// The front face of the cubemap.
+ /// The stream to write the encoded image to.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ public static Task EncodeCubeMapToStreamAsync(this BcEncoder encoder, Image right, Image left, Image top, Image down,
+ Image back, Image front, Stream outputStream, CancellationToken token = default)
+ {
+ return encoder.EncodeCubeMapToStreamAsync(
+ ImageToMemory2D(right),
+ ImageToMemory2D(left),
+ ImageToMemory2D(top),
+ ImageToMemory2D(down),
+ ImageToMemory2D(back),
+ ImageToMemory2D(front),
+ outputStream,
+ token
+ );
+ }
+
+ ///
+ /// Encodes all cubemap faces and mipmap levels into a Ktx file asynchronously.
+ /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z.
+ ///
+ /// The right face of the cubemap.
+ /// The left face of the cubemap.
+ /// The top face of the cubemap.
+ /// The bottom face of the cubemap.
+ /// The back face of the cubemap.
+ /// The front face of the cubemap.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// The Ktx file containing the encoded image.
+ public static Task EncodeCubeMapToKtxAsync(this BcEncoder encoder, Image right, Image left, Image top,
+ Image down, Image back, Image front, CancellationToken token = default)
+ {
+ return encoder.EncodeCubeMapToKtxAsync(
+ ImageToMemory2D(right),
+ ImageToMemory2D(left),
+ ImageToMemory2D(top),
+ ImageToMemory2D(down),
+ ImageToMemory2D(back),
+ ImageToMemory2D(front),
+ token
+ );
+ }
+
+ ///
+ /// Encodes all cubemap faces and mipmap levels into a Dds file asynchronously.
+ /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z.
+ ///
+ /// The right face of the cubemap.
+ /// The left face of the cubemap.
+ /// The top face of the cubemap.
+ /// The bottom face of the cubemap.
+ /// The back face of the cubemap.
+ /// The front face of the cubemap.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// The Dds file containing the encoded image.
+ public static Task EncodeCubeMapToDdsAsync(this BcEncoder encoder, Image right, Image left, Image top,
+ Image down, Image back, Image front, CancellationToken token = default)
+ {
+ return encoder.EncodeCubeMapToDdsAsync(
+ ImageToMemory2D(right),
+ ImageToMemory2D(left),
+ ImageToMemory2D(top),
+ ImageToMemory2D(down),
+ ImageToMemory2D(back),
+ ImageToMemory2D(front),
+ token
+ );
+ }
+
+ ///
+ /// Calculates the number of mipmap levels that will be generated for the given input image.
+ ///
+ /// The image to use for the calculation.
+ /// The number of mipmap levels that will be generated for the input image.
+ public static int CalculateNumberOfMipLevels(this BcEncoder encoder, Image inputImage)
+ {
+ return encoder.CalculateNumberOfMipLevels(inputImage.Width, inputImage.Height);
+ }
+
+ ///
+ /// Calculates the size of a given mipmap level.
+ ///
+ /// The image to use for the calculation.
+ /// The mipLevel to calculate (0 is original image)
+ /// The mipmap width calculated
+ /// The mipmap height calculated
+ /*
+ public static void CalculateMipMapSize(this BcEncoder encoder, Image inputImage, int mipLevel, out int mipWidth, out int mipHeight)
+ {
+ BcEncoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, mipLevel, out mipWidth, out mipHeight);
+ }
+ */
+
+ private static Memory2D ImageToMemory2D(Image inputImage)
+ {
+ _ = inputImage.GetPixelMemoryGroup()[0];
+ var colors = new ColorRgba32[inputImage.Width * inputImage.Height];
+ for (var y = 0; y < inputImage.Height; y++)
+ {
+ var yPixels = inputImage.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
+ var yColors = colors.AsSpan(y * inputImage.Width, inputImage.Width);
+
+ MemoryMarshal.Cast(yPixels).CopyTo(yColors);
+ }
+ var memory = colors.AsMemory().AsMemory2D(inputImage.Height, inputImage.Width);
+ return memory;
+ }
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcBlockDecoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcBlockDecoder.cs
new file mode 100644
index 0000000..1c576ac
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcBlockDecoder.cs
@@ -0,0 +1,186 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder
+{
+ internal interface IBcBlockDecoder where T : unmanaged
+ {
+ T[] Decode(ReadOnlyMemory data, OperationContext context);
+ T DecodeBlock(ReadOnlySpan data);
+ }
+
+ internal abstract class BaseBcBlockDecoder : IBcBlockDecoder where T : unmanaged where TBlock : unmanaged
+ {
+ private static readonly object lockObj = new object();
+
+ public TBlock[] Decode(ReadOnlyMemory data, OperationContext context)
+ {
+
+ if (data.Length % Unsafe.SizeOf() != 0)
+ {
+ throw new InvalidDataException("Given data does not align with the block length.");
+ }
+
+ var blockCount = data.Length / Unsafe.SizeOf();
+ var output = new TBlock[blockCount];
+
+ var currentBlocks = 0;
+ if (context.IsParallel)
+ {
+ var options = new ParallelOptions
+ {
+ CancellationToken = context.CancellationToken,
+ MaxDegreeOfParallelism = context.TaskCount
+ };
+ Parallel.For(0, blockCount, options, i =>
+ {
+ var encodedBlocks = MemoryMarshal.Cast(data.Span);
+ output[i] = DecodeBlock(encodedBlocks[i]);
+
+ if (context.Progress != null)
+ {
+ lock (lockObj)
+ {
+ context.Progress.Report(++currentBlocks);
+ }
+ }
+ });
+ }
+ else
+ {
+ var encodedBlocks = MemoryMarshal.Cast(data.Span);
+ for (var i = 0; i < blockCount; i++)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ output[i] = DecodeBlock(encodedBlocks[i]);
+
+ context.Progress?.Report(++currentBlocks);
+ }
+ }
+
+ return output;
+ }
+
+ public TBlock DecodeBlock(ReadOnlySpan data)
+ {
+ var encodedBlock = MemoryMarshal.Cast(data)[0];
+ return DecodeBlock(encodedBlock);
+ }
+
+ protected abstract TBlock DecodeBlock(T block);
+ }
+
+ internal class Bc1NoAlphaDecoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(Bc1Block block)
+ {
+ return block.Decode(false);
+ }
+ }
+
+ internal class Bc1ADecoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(Bc1Block block)
+ {
+ return block.Decode(true);
+ }
+ }
+
+ internal class Bc2Decoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(Bc2Block block)
+ {
+ return block.Decode();
+ }
+ }
+
+ internal class Bc3Decoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(Bc3Block block)
+ {
+ return block.Decode();
+ }
+ }
+
+ internal class Bc4Decoder : BaseBcBlockDecoder
+ {
+ private readonly ColorComponent component;
+
+ public Bc4Decoder(ColorComponent component)
+ {
+ this.component = component;
+ }
+
+ protected override RawBlock4X4Rgba32 DecodeBlock(Bc4Block block)
+ {
+ return block.Decode(component);
+ }
+ }
+
+ internal class Bc5Decoder : BaseBcBlockDecoder
+ {
+ private readonly ColorComponent component1;
+ private readonly ColorComponent component2;
+
+ public Bc5Decoder(ColorComponent component1, ColorComponent component2)
+ {
+ this.component1 = component1;
+ this.component2 = component2;
+ }
+
+ protected override RawBlock4X4Rgba32 DecodeBlock(Bc5Block block)
+ {
+ return block.Decode(component1, component2);
+ }
+ }
+
+ internal class Bc6UDecoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4RgbFloat DecodeBlock(Bc6Block block)
+ {
+ return block.Decode(false);
+ }
+ }
+
+ internal class Bc6SDecoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4RgbFloat DecodeBlock(Bc6Block block)
+ {
+ return block.Decode(true);
+ }
+ }
+
+ internal class Bc7Decoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(Bc7Block block)
+ {
+ return block.Decode();
+ }
+ }
+
+ internal class AtcDecoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(AtcBlock block)
+ {
+ return block.Decode();
+ }
+ }
+
+ internal class AtcExplicitAlphaDecoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(AtcExplicitAlphaBlock block)
+ {
+ return block.Decode();
+ }
+ }
+
+ internal class AtcInterpolatedAlphaDecoder : BaseBcBlockDecoder
+ {
+ protected override RawBlock4X4Rgba32 DecodeBlock(AtcInterpolatedAlphaBlock block)
+ {
+ return block.Decode();
+ }
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcDecoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcDecoder.cs
new file mode 100644
index 0000000..7e4904d
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcDecoder.cs
@@ -0,0 +1,1824 @@
+using CommunityToolkit.HighPerformance;
+using System.Runtime.CompilerServices;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder
+{
+ ///
+ /// Decodes compressed files into Rgba Format.
+ ///
+ public class BcDecoder
+ {
+ ///
+ /// The input options of the decoder.
+ ///
+ public DecoderInputOptions InputOptions { get; } = new DecoderInputOptions();
+
+ ///
+ /// The options for the decoder.
+ ///
+ public DecoderOptions Options { get; } = new DecoderOptions();
+
+ ///
+ /// The output options of the decoder.
+ ///
+ public DecoderOutputOptions OutputOptions { get; } = new DecoderOutputOptions();
+ #region LDR
+ #region Async Api
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ ///
+ /// The stream containing the encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeRawAsync(Stream inputStream, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ return Task.Run(() => DecodeRawInternal(dataArray, pixelWidth, pixelHeight, format, token), token);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ ///
+ /// The containing the encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeRawAsync(ReadOnlyMemory input, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeRawInternal(input, pixelWidth, pixelHeight, format, token), token);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternal(file, false, token)[0], token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeAllMipMapsAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternal(file, true, token), token);
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternal(file, false, token)[0], token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeAllMipMapsAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternal(file, true, token), token);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ ///
+ /// The stream containing the raw encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> DecodeRaw2DAsync(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ return Task.Run(() => DecodeRawInternal(dataArray, pixelWidth, pixelHeight, format, token)
+ .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ ///
+ /// The containing the encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> DecodeRaw2DAsync(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeRawInternal(input, pixelWidth, pixelHeight, format, token)
+ .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token);
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode the main image from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> Decode2DAsync(Stream inputStream, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeFromStreamInternal2D(inputStream, false, token)[0], token);
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task[]> DecodeAllMipMaps2DAsync(Stream inputStream, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeFromStreamInternal2D(inputStream, false, token), token);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> Decode2DAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternal(file, false, token)[0]
+ .AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth), token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task[]> DecodeAllMipMaps2DAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() =>
+ {
+ var decoded = DecodeInternal(file, true, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }, token);
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> Decode2DAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternal(file, false, token)[0]
+ .AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth), token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task[]> DecodeAllMipMaps2DAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() =>
+ {
+ var decoded = DecodeInternal(file, true, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.Faces[0].MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }, token);
+ }
+
+ #endregion
+
+ #region Sync API
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ ///
+ /// The stream containing the raw encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public ColorRgba32[] DecodeRaw(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ return DecodeRaw(dataArray, pixelWidth, pixelHeight, format);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ ///
+ /// The byte array containing the raw encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public ColorRgba32[] DecodeRaw(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ return DecodeRawInternal(input, pixelWidth, pixelHeight, format, default);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The decoded image.
+ public ColorRgba32[] Decode(KtxFile file)
+ {
+ return DecodeInternal(file, false, default)[0];
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// An array of decoded images.
+ public ColorRgba32[][] DecodeAllMipMaps(KtxFile file)
+ {
+ return DecodeInternal(file, true, default);
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The decoded image.
+ public ColorRgba32[] Decode(DdsFile file)
+ {
+ return DecodeInternal(file, false, default)[0];
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// An array of decoded images.
+ public ColorRgba32[][] DecodeAllMipMaps(DdsFile file)
+ {
+ return DecodeInternal(file, true, default);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ ///
+ /// The stream containing the encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public Memory2D DecodeRaw2D(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ var decoded = DecodeRaw(dataArray, pixelWidth, pixelHeight, format);
+ return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ ///
+ /// The byte array containing the raw encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public Memory2D DecodeRaw2D(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ var decoded = DecodeRawInternal(input, pixelWidth, pixelHeight, format, default);
+ return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth);
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode the main image from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// The decoded image.
+ public Memory2D Decode2D(Stream inputStream)
+ {
+ return DecodeFromStreamInternal2D(inputStream, false, default)[0];
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it.
+ /// The type of file will be detected automatically.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// An array of decoded images.
+ public Memory2D[] DecodeAllMipMaps2D(Stream inputStream)
+ {
+ return DecodeFromStreamInternal2D(inputStream, true, default);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// The decoded image.
+ public Memory2D Decode2D(KtxFile file)
+ {
+ return DecodeInternal(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ ///
+ /// The loaded Ktx file.
+ /// An array of decoded images.
+ public Memory2D[] DecodeAllMipMaps2D(KtxFile file)
+ {
+ var decoded = DecodeInternal(file, true, default);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// The decoded image.
+ public Memory2D Decode2D(DdsFile file)
+ {
+ return DecodeInternal(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ ///
+ /// The loaded Dds file.
+ /// An array of decoded images.
+ public Memory2D[] DecodeAllMipMaps2D(DdsFile file)
+ {
+ var decoded = DecodeInternal(file, true, default);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.Faces[0].MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }
+
+ ///
+ /// Decode a single block from raw bytes and return it as a .
+ /// Input Span size needs to equal the block size.
+ /// To get the block size (in bytes) of the compression format used, see .
+ ///
+ /// The encoded block in bytes.
+ /// The compression format used.
+ /// The decoded 4x4 block.
+ public Memory2D DecodeBlock(ReadOnlySpan blockData, CompressionFormat format)
+ {
+ var output = new ColorRgba32[4, 4];
+ DecodeBlockInternal(blockData, format, output);
+ return output;
+ }
+
+ ///
+ /// Decode a single block from raw bytes and write it to the given output span.
+ /// Output span size must be exactly 4x4 and input Span size needs to equal the block size.
+ /// To get the block size (in bytes) of the compression format used, see .
+ ///
+ /// The encoded block in bytes.
+ /// The compression format used.
+ /// The destination span of the decoded data.
+ public void DecodeBlock(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan)
+ {
+ if (outputSpan.Width != 4 || outputSpan.Height != 4)
+ {
+ throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4");
+ }
+ DecodeBlockInternal(blockData, format, outputSpan);
+ }
+
+ ///
+ /// Decode a single block from a stream and write it to the given output span.
+ /// Output span size must be exactly 4x4.
+ ///
+ /// The stream to read encoded blocks from.
+ /// The compression format used.
+ /// The destination span of the decoded data.
+ /// The number of bytes read from the stream. Zero (0) if reached the end of stream.
+ public int DecodeBlock(Stream inputStream, CompressionFormat format, Span2D outputSpan)
+ {
+ if (outputSpan.Width != 4 || outputSpan.Height != 4)
+ {
+ throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4");
+ }
+
+ Span input = stackalloc byte[16];
+ input = input[..GetBlockSize(format)];
+
+ var bytesRead = inputStream.Read(input);
+
+ if (bytesRead == 0)
+ {
+ return 0; //End of stream
+ }
+
+ if (bytesRead != input.Length)
+ {
+ throw new Exception("Input stream does not have enough data available for a full block.");
+ }
+
+ DecodeBlockInternal(input, format, outputSpan);
+ return bytesRead;
+ }
+
+ ///
+ /// Check whether a file is encoded in a supported format.
+ ///
+ /// The loaded ktx file to check
+ /// If the format of the file is one of the supported formats.
+ public static bool IsSupportedFormat(KtxFile file)
+ {
+ return GetCompressionFormat(file.header.GlInternalFormat) != CompressionFormat.Unknown;
+ }
+
+ ///
+ /// Check whether a file is encoded in a supported format.
+ ///
+ /// The loaded dds file to check
+ /// If the format of the file is one of the supported formats.
+ public bool IsSupportedFormat(DdsFile file)
+ {
+ return GetCompressionFormat(file) != CompressionFormat.Unknown;
+ }
+
+ ///
+ /// Gets the format of the file.
+ ///
+ /// The loaded ktx file to check
+ /// The of the file.
+ public static CompressionFormat GetFormat(KtxFile file)
+ {
+ return GetCompressionFormat(file.header.GlInternalFormat);
+ }
+
+ ///
+ /// Gets the format of the file.
+ ///
+ /// The loaded dds file to check
+ /// The of the file.
+ public CompressionFormat GetFormat(DdsFile file)
+ {
+ return GetCompressionFormat(file);
+ }
+
+
+ #endregion
+ #endregion
+
+ #region HDR
+ #region Async Api
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing the encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeRawHdrAsync(Stream inputStream, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ return Task.Run(() => DecodeRawInternalHdr(dataArray, pixelWidth, pixelHeight, format, token), token);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The containing the encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeRawHdrAsync(ReadOnlyMemory input, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, token), token);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeHdrAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternalHdr(file, false, token)[0], token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeAllMipMapsHdrAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternalHdr(file, true, token), token);
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeHdrAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternalHdr(file, false, token)[0], token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task DecodeAllMipMapsHdrAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternalHdr(file, true, token), token);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing the raw encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> DecodeRawHdr2DAsync(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ return Task.Run(() => DecodeRawInternalHdr(dataArray, pixelWidth, pixelHeight, format, token)
+ .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The containing the encoded data.
+ /// The Format the encoded data is in.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> DecodeRawHdr2DAsync(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, token)
+ .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token);
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode the main image from it.
+ /// The type of file will be detected automatically.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> DecodeHdr2DAsync(Stream inputStream, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, false, token)[0], token);
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it.
+ /// The type of file will be detected automatically.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task[]> DecodeAllMipMapsHdr2DAsync(Stream inputStream, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, false, token), token);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> DecodeHdr2DAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternalHdr(file, false, token)[0]
+ .AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth), token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task[]> DecodeAllMipMapsHdr2DAsync(KtxFile file, CancellationToken token = default)
+ {
+ return Task.Run(() =>
+ {
+ var decoded = DecodeInternalHdr(file, true, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }, token);
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task> DecodeHdr2DAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() => DecodeInternalHdr(file, false, token)[0]
+ .AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth), token);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// The cancellation token for this asynchronous operation.
+ /// The awaitable operation to retrieve the decoded image.
+ public Task[]> DecodeAllMipMapsHdr2DAsync(DdsFile file, CancellationToken token = default)
+ {
+ return Task.Run(() =>
+ {
+ var decoded = DecodeInternalHdr(file, true, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.Faces[0].MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }, token);
+ }
+
+ #endregion
+
+ #region Sync API
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing the raw encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public ColorRgbFloat[] DecodeRawHdr(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ return DecodeRawHdr(dataArray, pixelWidth, pixelHeight, format);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The byte array containing the raw encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public ColorRgbFloat[] DecodeRawHdr(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ return DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, default);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// The decoded image.
+ public ColorRgbFloat[] DecodeHdr(KtxFile file)
+ {
+ return DecodeInternalHdr(file, false, default)[0];
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// An array of decoded images.
+ public ColorRgbFloat[][] DecodeAllMipMapsHdr(KtxFile file)
+ {
+ return DecodeInternalHdr(file, true, default);
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// The decoded image.
+ public ColorRgbFloat[] DecodeHdr(DdsFile file)
+ {
+ return DecodeInternalHdr(file, false, default)[0];
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// An array of decoded images.
+ public ColorRgbFloat[][] DecodeAllMipMapsHdr(DdsFile file)
+ {
+ return DecodeInternalHdr(file, true, default);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method will read the expected amount of bytes from the given input stream and decode it.
+ /// Make sure there is no file header information left in the stream before the encoded data.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing the encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public Memory2D DecodeRawHdr2D(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)];
+ inputStream.Read(dataArray, 0, dataArray.Length);
+
+ var decoded = DecodeRawHdr(dataArray, pixelWidth, pixelHeight, format);
+ return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth);
+ }
+
+ ///
+ /// Decode a single encoded image from raw bytes.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The byte array containing the raw encoded data.
+ /// The pixelWidth of the image.
+ /// The pixelHeight of the image.
+ /// The Format the encoded data is in.
+ /// The decoded image.
+ public Memory2D DecodeRawHdr2D(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format)
+ {
+ var decoded = DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, default);
+ return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth);
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode the main image from it.
+ /// The type of file will be detected automatically.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// The decoded image.
+ public Memory2D DecodeHdr2D(Stream inputStream)
+ {
+ return DecodeFromStreamInternalHdr2D(inputStream, false, default)[0];
+ }
+
+ ///
+ /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it.
+ /// The type of file will be detected automatically.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream containing a Ktx or Dds file.
+ /// An array of decoded images.
+ public Memory2D[] DecodeAllMipMapsHdr2D(Stream inputStream)
+ {
+ return DecodeFromStreamInternalHdr2D(inputStream, true, default);
+ }
+
+ ///
+ /// Decode the main image from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// The decoded image.
+ public Memory2D DecodeHdr2D(KtxFile file)
+ {
+ return DecodeInternalHdr(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Ktx file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Ktx file.
+ /// An array of decoded images.
+ public Memory2D[] DecodeAllMipMapsHdr2D(KtxFile file)
+ {
+ var decoded = DecodeInternalHdr(file, true, default);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }
+
+ ///
+ /// Decode the main image from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// The decoded image.
+ public Memory2D DecodeHdr2D(DdsFile file)
+ {
+ return DecodeInternalHdr(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth);
+ }
+
+ ///
+ /// Decode all available mipmaps from a Dds file.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The loaded Dds file.
+ /// An array of decoded images.
+ public Memory2D[] DecodeAllMipMapsHdr2D(DdsFile file)
+ {
+ var decoded = DecodeInternalHdr(file, true, default);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.Faces[0].MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+ return mem2Ds;
+ }
+
+ ///
+ /// Decode a single block from raw bytes and return it as a .
+ /// Input Span size needs to equal the block size.
+ /// To get the block size (in bytes) of the compression format used, see .
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The encoded block in bytes.
+ /// The compression format used.
+ /// The decoded 4x4 block.
+ public static Memory2D DecodeBlockHdr(ReadOnlySpan blockData, CompressionFormat format)
+ {
+ var output = new ColorRgbFloat[4, 4];
+ DecodeBlockInternalHdr(blockData, format, output);
+ return output;
+ }
+
+ ///
+ /// Decode a single block from raw bytes and write it to the given output span.
+ /// Output span size must be exactly 4x4 and input Span size needs to equal the block size.
+ /// To get the block size (in bytes) of the compression format used, see .
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The encoded block in bytes.
+ /// The compression format used.
+ /// The destination span of the decoded data.
+ public static void DecodeBlockHdr(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan)
+ {
+ if (outputSpan.Width != 4 || outputSpan.Height != 4)
+ {
+ throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4");
+ }
+ DecodeBlockInternalHdr(blockData, format, outputSpan);
+ }
+
+ ///
+ /// Decode a single block from a stream and write it to the given output span.
+ /// Output span size must be exactly 4x4.
+ /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats.
+ ///
+ /// The stream to read encoded blocks from.
+ /// The compression format used.
+ /// The destination span of the decoded data.
+ /// The number of bytes read from the stream. Zero (0) if reached the end of stream.
+ public int DecodeBlockHdr(Stream inputStream, CompressionFormat format, Span2D outputSpan)
+ {
+ if (outputSpan.Width != 4 || outputSpan.Height != 4)
+ {
+ throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4");
+ }
+
+ Span input = stackalloc byte[16];
+ input = input[..GetBlockSize(format)];
+
+ var bytesRead = inputStream.Read(input);
+
+ if (bytesRead == 0)
+ {
+ return 0; //End of stream
+ }
+
+ if (bytesRead != input.Length)
+ {
+ throw new Exception("Input stream does not have enough data available for a full block.");
+ }
+
+ DecodeBlockInternalHdr(input, format, outputSpan);
+ return bytesRead;
+ }
+
+ ///
+ /// Check whether a file is encoded in a supported HDR format.
+ ///
+ /// The loaded ktx file to check
+ /// If the format of the file is one of the supported HDR formats.
+ public static bool IsHdrFormat(KtxFile file)
+ {
+ return GetCompressionFormat(file.header.GlInternalFormat).IsHdrFormat();
+ }
+
+ ///
+ /// Check whether a file is encoded in a supported HDR format.
+ ///
+ /// The loaded dds file to check
+ /// If the format of the file is one of the supported HDR formats.
+ public bool IsHdrFormat(DdsFile file)
+ {
+ return GetCompressionFormat(file).IsHdrFormat();
+ }
+
+ #endregion
+ #endregion
+ ///
+ /// Load a stream and extract either the main image or all mip maps.
+ ///
+ /// The stream containing the image file.
+ /// If all mip maps or only the main image should be decoded.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// An array of decoded Rgba32 images.
+ private Memory2D[] DecodeFromStreamInternal2D(Stream stream, bool allMipMaps, CancellationToken token)
+ {
+ var format = ImageFile.DetermineImageFormat(stream);
+
+ switch (format)
+ {
+ case ImageFileFormat.Dds:
+ {
+ var file = DdsFile.Load(stream);
+ var decoded = DecodeInternal(file, allMipMaps, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.Faces[0].MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+
+ return mem2Ds;
+ }
+
+ case ImageFileFormat.Ktx:
+ {
+ var file = KtxFile.Load(stream);
+ var decoded = DecodeInternal(file, allMipMaps, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+
+ return mem2Ds;
+ }
+
+ default:
+ throw new InvalidOperationException("Unknown image format.");
+ }
+ }
+
+ ///
+ /// Load a KTX file and extract either the main image or all mip maps.
+ ///
+ /// The Ktx file to decode.
+ /// If all mip maps or only the main image should be decoded.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// An array of decoded Rgba32 images.
+ private ColorRgba32[][] DecodeInternal(KtxFile file, bool allMipMaps, CancellationToken token)
+ {
+ var mipMaps = allMipMaps ? file.MipMaps.Count : 1;
+ var colors = new ColorRgba32[mipMaps][];
+
+ var context = new OperationContext
+ {
+ CancellationToken = token,
+ IsParallel = Options.IsParallel,
+ TaskCount = Options.TaskCount
+ };
+
+ // Calculate total blocks
+ var blockSize = GetBlockSize(file.header.GlInternalFormat);
+ var totalBlocks = file.MipMaps.Take(mipMaps).Sum(m => m.Faces[0].Data.Length / blockSize);
+
+ context.Progress = new OperationProgress(Options.Progress, totalBlocks);
+
+ if (IsSupportedRawFormat(file.header.GlInternalFormat))
+ {
+ var decoder = GetRawDecoder(file.header.GlInternalFormat);
+
+ for (var mip = 0; mip < mipMaps; mip++)
+ {
+ var data = file.MipMaps[mip].Faces[0].Data;
+
+ colors[mip] = decoder.Decode(data, context);
+
+ context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize));
+ }
+ }
+ else
+ {
+ var decoder = GetRgba32Decoder(file.header.GlInternalFormat);
+ var format = GetCompressionFormat(file.header.GlInternalFormat);
+ if (format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {file.header.GlInternalFormat}");
+ }
+
+ for (var mip = 0; mip < mipMaps; mip++)
+ {
+ var data = file.MipMaps[mip].Faces[0].Data;
+ var pixelWidth = file.MipMaps[mip].Width;
+ var pixelHeight = file.MipMaps[mip].Height;
+
+ var blocks = decoder.Decode(data, context);
+
+ colors[mip] = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight);
+
+ context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize));
+ }
+ }
+
+ return colors;
+ }
+
+ ///
+ /// Load a DDS file and extract either the main image or all mip maps.
+ ///
+ /// The Dds file to decode.
+ /// If all mip maps or only the main image should be decoded.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// An array of decoded Rgba32 images.
+ private ColorRgba32[][] DecodeInternal(DdsFile file, bool allMipMaps, CancellationToken token)
+ {
+ var mipMaps = allMipMaps ? Math.Max(1, file.header.dwMipMapCount) : 1;
+ var colors = new ColorRgba32[mipMaps][];
+
+ var context = new OperationContext
+ {
+ CancellationToken = token,
+ IsParallel = Options.IsParallel,
+ TaskCount = Options.TaskCount
+ };
+
+ // Calculate total blocks
+ var blockSize = GetBlockSize(file);
+ var totalBlocks = file.Faces[0].MipMaps.Take((int)mipMaps).Sum(m => m.Data.Length / blockSize);
+
+ context.Progress = new OperationProgress(Options.Progress, totalBlocks);
+
+ if (IsSupportedRawFormat(file))
+ {
+ var decoder = GetRawDecoder(file);
+
+ for (var mip = 0; mip < mipMaps; mip++)
+ {
+ var data = file.Faces[0].MipMaps[mip].Data;
+
+ colors[mip] = decoder.Decode(data, context);
+
+ context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize));
+ }
+ }
+ else
+ {
+ var dxtFormat = file.header.ddsPixelFormat.IsDxt10Format
+ ? file.dx10Header.dxgiFormat
+ : file.header.ddsPixelFormat.DxgiFormat;
+ var format = GetCompressionFormat(file);
+ var decoder = GetRgba32Decoder(format);
+
+ if (format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {dxtFormat}");
+ }
+
+ for (var mip = 0; mip < mipMaps; mip++)
+ {
+ var data = file.Faces[0].MipMaps[mip].Data;
+ var pixelWidth = file.Faces[0].MipMaps[mip].Width;
+ var pixelHeight = file.Faces[0].MipMaps[mip].Height;
+
+ var blocks = decoder.Decode(data, context);
+
+ var image = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight);
+
+ colors[mip] = image;
+
+ context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize));
+ }
+ }
+
+ return colors;
+ }
+
+ ///
+ /// Decode raw encoded image asynchronously.
+ ///
+ /// The containing the encoded data.
+ /// The width of the image.
+ /// The height of the image.
+ /// The Format the encoded data is in.
+ /// The cancellation token for this operation. May be default, if the operation is not asynchronous.
+ /// The decoded Rgba32 image.
+ private ColorRgba32[] DecodeRawInternal(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token)
+ {
+ if (input.Length % GetBlockSize(format) != 0)
+ {
+ throw new ArgumentException("The size of the input buffer does not align with the compression format.");
+ }
+
+ var context = new OperationContext
+ {
+ CancellationToken = token,
+ IsParallel = Options.IsParallel,
+ TaskCount = Options.TaskCount
+ };
+
+ // Calculate total blocks
+ var blockSize = GetBlockSize(format);
+ var totalBlocks = input.Length / blockSize;
+
+ context.Progress = new OperationProgress(Options.Progress, totalBlocks);
+
+ var isCompressedFormat = format.IsCompressedFormat();
+ if (isCompressedFormat)
+ {
+ // DecodeInternal as compressed data
+ var decoder = GetRgba32Decoder(format);
+
+ if (format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {format}");
+ }
+
+ var blocks = decoder.Decode(input, context);
+
+ return ImageToBlocks.ColorsFromRawBlocks(blocks, pixelWidth, pixelHeight); ;
+ }
+
+ // DecodeInternal as raw data
+ var rawDecoder = GetRawDecoder(format);
+
+ return rawDecoder.Decode(input, context);
+ }
+
+ private void DecodeBlockInternal(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan)
+ {
+ var decoder = GetRgba32Decoder(format);
+ if (format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {format}");
+ }
+ if (blockData.Length != GetBlockSize(format))
+ {
+ throw new ArgumentException("The size of the input buffer does not align with the compression format.");
+ }
+
+ var rawBlock = decoder.DecodeBlock(blockData);
+ var pixels = rawBlock.AsSpan;
+
+ pixels[..4].CopyTo(outputSpan.GetRowSpan(0));
+ pixels.Slice(4, 4).CopyTo(outputSpan.GetRowSpan(1));
+ pixels.Slice(8, 4).CopyTo(outputSpan.GetRowSpan(2));
+ pixels.Slice(12, 4).CopyTo(outputSpan.GetRowSpan(3));
+ }
+
+ #region Hdr internals
+
+ ///
+ /// Load a stream and extract either the main image or all mip maps.
+ ///
+ /// The stream containing the image file.
+ /// If all mip maps or only the main image should be decoded.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// An array of decoded Rgba32 images.
+ private Memory2D[] DecodeFromStreamInternalHdr2D(Stream stream, bool allMipMaps, CancellationToken token)
+ {
+ var format = ImageFile.DetermineImageFormat(stream);
+
+ switch (format)
+ {
+ case ImageFileFormat.Dds:
+ {
+ var file = DdsFile.Load(stream);
+ var decoded = DecodeInternalHdr(file, allMipMaps, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.Faces[0].MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+
+ return mem2Ds;
+ }
+
+ case ImageFileFormat.Ktx:
+ {
+ var file = KtxFile.Load(stream);
+ var decoded = DecodeInternalHdr(file, allMipMaps, token);
+ var mem2Ds = new Memory2D[decoded.Length];
+ for (var i = 0; i < decoded.Length; i++)
+ {
+ var mip = file.MipMaps[i];
+ mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width);
+ }
+
+ return mem2Ds;
+ }
+
+ default:
+ throw new InvalidOperationException("Unknown image format.");
+ }
+ }
+
+ ///
+ /// Load a KTX file and extract either the main image or all mip maps.
+ ///
+ /// The Ktx file to decode.
+ /// If all mip maps or only the main image should be decoded.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// An array of decoded Rgba32 images.
+ private ColorRgbFloat[][] DecodeInternalHdr(KtxFile file, bool allMipMaps, CancellationToken token)
+ {
+ var mipMaps = allMipMaps ? file.MipMaps.Count : 1;
+ var colors = new ColorRgbFloat[mipMaps][];
+
+ var context = new OperationContext
+ {
+ CancellationToken = token,
+ IsParallel = Options.IsParallel,
+ TaskCount = Options.TaskCount
+ };
+
+ // Calculate total blocks
+ var blockSize = GetBlockSize(file.header.GlInternalFormat);
+ var totalBlocks = file.MipMaps.Take(mipMaps).Sum(m => m.Faces[0].Data.Length / blockSize);
+
+ context.Progress = new OperationProgress(Options.Progress, totalBlocks);
+
+ var decoder = GetRgbFloatDecoder(file.header.GlInternalFormat);
+ var format = GetCompressionFormat(file.header.GlInternalFormat);
+ if (!format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {file.header.GlInternalFormat}");
+ }
+
+ for (var mip = 0; mip < mipMaps; mip++)
+ {
+ var data = file.MipMaps[mip].Faces[0].Data;
+ var pixelWidth = file.MipMaps[mip].Width;
+ var pixelHeight = file.MipMaps[mip].Height;
+
+ var blocks = decoder.Decode(data, context);
+
+ colors[mip] = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight);
+
+ context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize));
+ }
+
+ return colors;
+ }
+
+ ///
+ /// Load a DDS file and extract either the main image or all mip maps.
+ ///
+ /// The Dds file to decode.
+ /// If all mip maps or only the main image should be decoded.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// An array of decoded Rgba32 images.
+ private ColorRgbFloat[][] DecodeInternalHdr(DdsFile file, bool allMipMaps, CancellationToken token)
+ {
+ var mipMaps = allMipMaps ? file.header.dwMipMapCount : 1;
+ var colors = new ColorRgbFloat[mipMaps][];
+
+ var context = new OperationContext
+ {
+ CancellationToken = token,
+ IsParallel = Options.IsParallel,
+ TaskCount = Options.TaskCount
+ };
+
+ // Calculate total blocks
+ var blockSize = GetBlockSize(file);
+ var totalBlocks = file.Faces[0].MipMaps.Take((int)mipMaps).Sum(m => m.Data.Length / blockSize);
+
+ context.Progress = new OperationProgress(Options.Progress, totalBlocks);
+
+ var dxtFormat = file.header.ddsPixelFormat.IsDxt10Format
+ ? file.dx10Header.dxgiFormat
+ : file.header.ddsPixelFormat.DxgiFormat;
+ var format = GetCompressionFormat(file);
+ var decoder = GetRgbFloatDecoder(format);
+
+ if (!format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {dxtFormat}");
+ }
+
+ for (var mip = 0; mip < mipMaps; mip++)
+ {
+ var data = file.Faces[0].MipMaps[mip].Data;
+ var pixelWidth = file.Faces[0].MipMaps[mip].Width;
+ var pixelHeight = file.Faces[0].MipMaps[mip].Height;
+
+ var blocks = decoder.Decode(data, context);
+
+ var image = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight);
+
+ colors[mip] = image;
+
+ context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize));
+ }
+
+ return colors;
+ }
+
+ ///
+ /// Decode raw encoded image asynchronously.
+ ///
+ /// The containing the encoded data.
+ /// The width of the image.
+ /// The height of the image.
+ /// The Format the encoded data is in.
+ /// The cancellation token for this operation. May be default, if the operation is not asynchronous.
+ /// The decoded Rgba32 image.
+ private ColorRgbFloat[] DecodeRawInternalHdr(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token)
+ {
+ if (input.Length % GetBlockSize(format) != 0)
+ {
+ throw new ArgumentException("The size of the input buffer does not align with the compression format.");
+ }
+
+ var context = new OperationContext
+ {
+ CancellationToken = token,
+ IsParallel = Options.IsParallel,
+ TaskCount = Options.TaskCount
+ };
+
+ // Calculate total blocks
+ var blockSize = GetBlockSize(format);
+ var totalBlocks = input.Length / blockSize;
+
+ context.Progress = new OperationProgress(Options.Progress, totalBlocks);
+
+ var decoder = GetRgbFloatDecoder(format);
+
+ if (!format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {format}");
+ }
+
+ var blocks = decoder.Decode(input, context);
+
+ return ImageToBlocks.ColorsFromRawBlocks(blocks, pixelWidth, pixelHeight);
+ }
+
+ private static void DecodeBlockInternalHdr(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan)
+ {
+ var decoder = GetRgbFloatDecoder(format);
+ if (!format.IsHdrFormat())
+ {
+ throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods.");
+ }
+ if (decoder == null)
+ {
+ throw new NotSupportedException($"This Format is not supported: {format}");
+ }
+ if (blockData.Length != GetBlockSize(format))
+ {
+ throw new ArgumentException("The size of the input buffer does not align with the compression format.");
+ }
+
+ var rawBlock = decoder.DecodeBlock(blockData);
+ var pixels = rawBlock.AsSpan;
+
+ pixels[..4].CopyTo(outputSpan.GetRowSpan(0));
+ pixels.Slice(4, 4).CopyTo(outputSpan.GetRowSpan(1));
+ pixels.Slice(8, 4).CopyTo(outputSpan.GetRowSpan(2));
+ pixels.Slice(12, 4).CopyTo(outputSpan.GetRowSpan(3));
+ }
+ #endregion
+
+ #region Support
+
+ #region Is supported format
+
+ private static bool IsSupportedRawFormat(GlInternalFormat format)
+ {
+ return IsSupportedRawFormat(GetCompressionFormat(format));
+ }
+
+ private bool IsSupportedRawFormat(DdsFile file)
+ {
+ return IsSupportedRawFormat(GetCompressionFormat(file));
+ }
+
+ private static bool IsSupportedRawFormat(CompressionFormat format)
+ {
+ return format switch
+ {
+ CompressionFormat.R or CompressionFormat.Rg or CompressionFormat.Rgb or CompressionFormat.Rgba or CompressionFormat.Bgra => true,
+ _ => false,
+ };
+ }
+
+ #endregion
+
+ #region Get decoder
+
+ private IBcBlockDecoder GetRgba32Decoder(GlInternalFormat format)
+ {
+ return GetRgba32Decoder(GetCompressionFormat(format));
+ }
+
+ private IBcBlockDecoder GetRgba32Decoder(DdsFile file)
+ {
+ return GetRgba32Decoder(GetCompressionFormat(file));
+ }
+
+ private IBcBlockDecoder GetRgba32Decoder(CompressionFormat format)
+ {
+ return format switch
+ {
+ CompressionFormat.Bc1 => new Bc1NoAlphaDecoder(),
+ CompressionFormat.Bc1WithAlpha => new Bc1ADecoder(),
+ CompressionFormat.Bc2 => new Bc2Decoder(),
+ CompressionFormat.Bc3 => new Bc3Decoder(),
+ CompressionFormat.Bc4 => new Bc4Decoder(OutputOptions.Bc4Component),
+ CompressionFormat.Bc5 => new Bc5Decoder(OutputOptions.Bc5Component1, OutputOptions.Bc5Component2),
+ CompressionFormat.Bc7 => new Bc7Decoder(),
+ CompressionFormat.Atc => new AtcDecoder(),
+ CompressionFormat.AtcExplicitAlpha => new AtcExplicitAlphaDecoder(),
+ CompressionFormat.AtcInterpolatedAlpha => new AtcInterpolatedAlphaDecoder(),
+ _ => null,
+ };
+ }
+
+ private static IBcBlockDecoder GetRgbFloatDecoder(GlInternalFormat format)
+ {
+ return GetRgbFloatDecoder(GetCompressionFormat(format));
+ }
+
+ private IBcBlockDecoder GetRgbFloatDecoder(DdsFile file)
+ {
+ return GetRgbFloatDecoder(GetCompressionFormat(file));
+ }
+
+ private static IBcBlockDecoder GetRgbFloatDecoder(CompressionFormat format)
+ {
+ return format switch
+ {
+ CompressionFormat.Bc6S => new Bc6SDecoder(),
+ CompressionFormat.Bc6U => new Bc6UDecoder(),
+ _ => null,
+ };
+ }
+
+ #endregion
+
+ #region Get raw decoder
+
+ private IRawDecoder GetRawDecoder(GlInternalFormat format)
+ {
+ return GetRawDecoder(GetCompressionFormat(format));
+ }
+
+ private IRawDecoder GetRawDecoder(DdsFile file)
+ {
+ return GetRawDecoder(GetCompressionFormat(file));
+ }
+
+ private IRawDecoder GetRawDecoder(CompressionFormat format)
+ {
+ return format switch
+ {
+ CompressionFormat.R => new RawRDecoder(OutputOptions.RedAsLuminance),
+ CompressionFormat.Rg => new RawRgDecoder(),
+ CompressionFormat.Rgb => new RawRgbDecoder(),
+ CompressionFormat.Rgba => new RawRgbaDecoder(),
+ CompressionFormat.Bgra => new RawBgraDecoder(),
+ _ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
+ };
+ }
+
+ #endregion
+
+ #region Get block size
+
+ ///
+ /// Gets the number of total blocks in an image with the given pixel width and height.
+ ///
+ /// The pixel width of the image
+ /// The pixel height of the image
+ /// The total number of blocks.
+ public static int GetBlockCount(int pixelWidth, int pixelHeight)
+ {
+ return ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight);
+ }
+
+ ///
+ /// Gets the number of blocks in an image with the given pixel width and height.
+ ///
+ /// The pixel width of the image
+ /// The pixel height of the image
+ /// The amount of blocks in the x-axis
+ /// The amount of blocks in the y-axis
+ public static void GetBlockCount(int pixelWidth, int pixelHeight, out int blocksWidth, out int blocksHeight)
+ {
+ ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight, out blocksWidth, out blocksHeight);
+ }
+
+ private static int GetBlockSize(GlInternalFormat format)
+ {
+ return GetBlockSize(GetCompressionFormat(format));
+ }
+
+ private int GetBlockSize(DdsFile file)
+ {
+ return GetBlockSize(GetCompressionFormat(file));
+ }
+
+ ///
+ /// Get the size of blocks for the given compression format in bytes.
+ ///
+ /// The compression format used.
+ /// The size of a single block in bytes.
+ public static int GetBlockSize(CompressionFormat format)
+ {
+ return format switch
+ {
+ CompressionFormat.R => 1,
+ CompressionFormat.Rg => 2,
+ CompressionFormat.Rgb => 3,
+ CompressionFormat.Rgba => 4,
+ CompressionFormat.Bgra => 4,
+ CompressionFormat.Bc1 or CompressionFormat.Bc1WithAlpha => Unsafe.SizeOf(),
+ CompressionFormat.Bc2 => Unsafe.SizeOf(),
+ CompressionFormat.Bc3 => Unsafe.SizeOf(),
+ CompressionFormat.Bc4 => Unsafe.SizeOf(),
+ CompressionFormat.Bc5 => Unsafe.SizeOf(),
+ CompressionFormat.Bc6S or CompressionFormat.Bc6U => Unsafe.SizeOf(),
+ CompressionFormat.Bc7 => Unsafe.SizeOf(),
+ CompressionFormat.Atc => Unsafe.SizeOf(),
+ CompressionFormat.AtcExplicitAlpha => Unsafe.SizeOf(),
+ CompressionFormat.AtcInterpolatedAlpha => Unsafe.SizeOf(),
+ CompressionFormat.Unknown => 0,
+ _ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
+ };
+ }
+
+ #endregion
+
+ private static CompressionFormat GetCompressionFormat(GlInternalFormat format)
+ {
+ return format switch
+ {
+ GlInternalFormat.GlR8 => CompressionFormat.R,
+ GlInternalFormat.GlRg8 => CompressionFormat.Rg,
+ GlInternalFormat.GlRgb8 => CompressionFormat.Rgb,
+ GlInternalFormat.GlRgba8 => CompressionFormat.Rgba,
+ // HINT: Bgra is not supported by default. The format enum is added by an extension by Apple.
+ GlInternalFormat.GlBgra8Extension => CompressionFormat.Bgra,
+ GlInternalFormat.GlCompressedRgbS3TcDxt1Ext => CompressionFormat.Bc1,
+ GlInternalFormat.GlCompressedRgbaS3TcDxt1Ext => CompressionFormat.Bc1WithAlpha,
+ GlInternalFormat.GlCompressedRgbaS3TcDxt3Ext => CompressionFormat.Bc2,
+ GlInternalFormat.GlCompressedRgbaS3TcDxt5Ext => CompressionFormat.Bc3,
+ GlInternalFormat.GlCompressedRedRgtc1Ext => CompressionFormat.Bc4,
+ GlInternalFormat.GlCompressedRedGreenRgtc2Ext => CompressionFormat.Bc5,
+ GlInternalFormat.GlCompressedRgbBptcUnsignedFloatArb => CompressionFormat.Bc6U,
+ GlInternalFormat.GlCompressedRgbBptcSignedFloatArb => CompressionFormat.Bc6S,
+ // TODO: Not sure what to do with SRGB input.
+ GlInternalFormat.GlCompressedRgbaBptcUnormArb or GlInternalFormat.GlCompressedSrgbAlphaBptcUnormArb => CompressionFormat.Bc7,
+ GlInternalFormat.GlCompressedRgbAtc => CompressionFormat.Atc,
+ GlInternalFormat.GlCompressedRgbaAtcExplicitAlpha => CompressionFormat.AtcExplicitAlpha,
+ GlInternalFormat.GlCompressedRgbaAtcInterpolatedAlpha => CompressionFormat.AtcInterpolatedAlpha,
+ _ => CompressionFormat.Unknown,
+ };
+ }
+
+ private CompressionFormat GetCompressionFormat(DdsFile file)
+ {
+ var format = file.header.ddsPixelFormat.IsDxt10Format ?
+ file.dx10Header.dxgiFormat :
+ file.header.ddsPixelFormat.DxgiFormat;
+
+ switch (format)
+ {
+ case DxgiFormat.DxgiFormatR8Unorm:
+ return CompressionFormat.R;
+
+ case DxgiFormat.DxgiFormatR8G8Unorm:
+ return CompressionFormat.Rg;
+
+ // HINT: R8G8B8 has no DxgiFormat to convert from
+
+ case DxgiFormat.DxgiFormatR8G8B8A8Unorm:
+ return CompressionFormat.Rgba;
+
+ case DxgiFormat.DxgiFormatB8G8R8A8Unorm:
+ return CompressionFormat.Bgra;
+
+ case DxgiFormat.DxgiFormatBc1Unorm:
+ case DxgiFormat.DxgiFormatBc1UnormSrgb:
+ case DxgiFormat.DxgiFormatBc1Typeless:
+ if (file.header.ddsPixelFormat.dwFlags.HasFlag(PixelFormatFlags.DdpfAlphaPixels))
+ return CompressionFormat.Bc1WithAlpha;
+
+ if (InputOptions.DdsBc1ExpectAlpha)
+ return CompressionFormat.Bc1WithAlpha;
+
+ return CompressionFormat.Bc1;
+
+ case DxgiFormat.DxgiFormatBc2Unorm:
+ case DxgiFormat.DxgiFormatBc2UnormSrgb:
+ case DxgiFormat.DxgiFormatBc2Typeless:
+ return CompressionFormat.Bc2;
+
+ case DxgiFormat.DxgiFormatBc3Unorm:
+ case DxgiFormat.DxgiFormatBc3UnormSrgb:
+ case DxgiFormat.DxgiFormatBc3Typeless:
+ return CompressionFormat.Bc3;
+
+ case DxgiFormat.DxgiFormatBc4Unorm:
+ case DxgiFormat.DxgiFormatBc4Snorm:
+ case DxgiFormat.DxgiFormatBc4Typeless:
+ return CompressionFormat.Bc4;
+
+ case DxgiFormat.DxgiFormatBc5Unorm:
+ case DxgiFormat.DxgiFormatBc5Snorm:
+ case DxgiFormat.DxgiFormatBc5Typeless:
+ return CompressionFormat.Bc5;
+
+ case DxgiFormat.DxgiFormatBc6HTypeless:
+ case DxgiFormat.DxgiFormatBc6HUf16:
+ return CompressionFormat.Bc6U;
+
+ case DxgiFormat.DxgiFormatBc6HSf16:
+ return CompressionFormat.Bc6S;
+
+ case DxgiFormat.DxgiFormatBc7Unorm:
+ case DxgiFormat.DxgiFormatBc7UnormSrgb:
+ case DxgiFormat.DxgiFormatBc7Typeless:
+ return CompressionFormat.Bc7;
+
+ case DxgiFormat.DxgiFormatAtcExt:
+ return CompressionFormat.Atc;
+
+ case DxgiFormat.DxgiFormatAtcExplicitAlphaExt:
+ return CompressionFormat.AtcExplicitAlpha;
+
+ case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt:
+ return CompressionFormat.AtcInterpolatedAlpha;
+
+ default:
+ return CompressionFormat.Unknown;
+ }
+ }
+
+ private static int GetBufferSize(CompressionFormat format, int pixelWidth, int pixelHeight)
+ {
+ return format switch
+ {
+ CompressionFormat.R => pixelWidth * pixelHeight,
+ CompressionFormat.Rg => 2 * pixelWidth * pixelHeight,
+ CompressionFormat.Rgb => 3 * pixelWidth * pixelHeight,
+ CompressionFormat.Rgba or CompressionFormat.Bgra => 4 * pixelWidth * pixelHeight,
+ CompressionFormat.Bc1 or CompressionFormat.Bc1WithAlpha or CompressionFormat.Bc2 or CompressionFormat.Bc3 or CompressionFormat.Bc4 or CompressionFormat.Bc5 or CompressionFormat.Bc6S or CompressionFormat.Bc6U or CompressionFormat.Bc7 or CompressionFormat.Atc or CompressionFormat.AtcExplicitAlpha or CompressionFormat.AtcInterpolatedAlpha => GetBlockSize(format) * ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight),
+ CompressionFormat.Unknown => 0,
+ _ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
+ };
+ }
+
+ #endregion
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderInputOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderInputOptions.cs
new file mode 100644
index 0000000..ef4b46a
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderInputOptions.cs
@@ -0,0 +1,16 @@
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options
+{
+ ///
+ /// A class for the decoder input options.
+ ///
+ public class DecoderInputOptions
+ {
+ ///
+ /// The DDS file Format doesn't seem to have a standard for indicating whether a BC1 texture
+ /// includes 1bit of alpha. This option will assume that all Bc1 textures contain alpha.
+ /// If this option is false, but the dds header includes a DDPF_ALPHAPIXELS flag, alpha will be included.
+ /// Default is true.
+ ///
+ public bool DdsBc1ExpectAlpha { get; set; } = true;
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOptions.cs
new file mode 100644
index 0000000..bf91cad
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOptions.cs
@@ -0,0 +1,29 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options
+{
+ ///
+ /// General options for the decoder.
+ ///
+ public class DecoderOptions
+ {
+ ///
+ /// Whether the blocks should be decoded in parallel. This can be much faster than single-threaded decoding,
+ /// but is slow if multiple textures are being processed at the same time.
+ /// When a debugger is attached, the decoder defaults to single-threaded operation to ease debugging.
+ /// Default is false.
+ ///
+ /// Parallel execution will be ignored in RawDecoders, due to minimal performance gain.
+ public bool IsParallel { get; set; }
+
+ ///
+ /// Determines how many tasks should be used for parallel processing.
+ ///
+ public int TaskCount { get; set; } = Environment.ProcessorCount;
+
+ ///
+ /// The progress context for the operation.
+ ///
+ public IProgress Progress { get; set; }
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOutputOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOutputOptions.cs
new file mode 100644
index 0000000..1d2f109
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOutputOptions.cs
@@ -0,0 +1,32 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options
+{
+ ///
+ /// A class for the decoder output options.
+ ///
+ public class DecoderOutputOptions
+ {
+ ///
+ /// If true, when decoding from R8 raw format,
+ /// output pixels will have all colors set to the same value (greyscale).
+ /// Default is true. (Does not apply to BC4 format.)
+ ///
+ public bool RedAsLuminance { get; set; } = true;
+
+ ///
+ /// The color channel to populate with the values of a BC4 block.
+ ///
+ public ColorComponent Bc4Component { get; set; } = ColorComponent.R;
+
+ ///
+ /// The color channel to populate with the values of the first BC5 block.
+ ///
+ public ColorComponent Bc5Component1 { get; set; } = ColorComponent.R;
+
+ ///
+ /// The color channel to populate with the values of the second BC5 block.
+ ///
+ public ColorComponent Bc5Component2 { get; set; } = ColorComponent.G;
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/RawDecoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/RawDecoder.cs
new file mode 100644
index 0000000..4d5df8d
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/RawDecoder.cs
@@ -0,0 +1,190 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder
+{
+ internal interface IRawDecoder
+ {
+ ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context);
+ }
+
+ ///
+ /// A class to decode data to R components.
+ ///
+ public class RawRDecoder : IRawDecoder
+ {
+ private readonly bool redAsLuminance;
+
+ ///
+ /// Create a new instance of .
+ ///
+ /// If the decoded component should be used as the red component or luminance.
+ public RawRDecoder(bool redAsLuminance)
+ {
+ this.redAsLuminance = redAsLuminance;
+ }
+
+ ///
+ /// Decode the data to color components.
+ ///
+ /// The data to decode.
+ /// The context of the current operation.
+ /// The decoded color components.
+ public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context)
+ {
+ var output = new ColorRgba32[data.Length];
+
+ // HINT: Ignoring parallel execution since we wouldn't gain performance from it.
+
+ var span = data.Span;
+ for (var i = 0; i < output.Length; i++)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ if (redAsLuminance)
+ {
+ output[i].r = span[i];
+ output[i].g = span[i];
+ output[i].b = span[i];
+ }
+ else
+ {
+ output[i].r = span[i];
+ output[i].g = 0;
+ output[i].b = 0;
+ }
+
+ output[i].a = 255;
+ }
+
+ return output;
+ }
+ }
+
+ ///
+ /// A class to decode data to RG components.
+ ///
+ public class RawRgDecoder : IRawDecoder
+ {
+ ///
+ /// Decode the data to color components.
+ ///
+ /// The data to decode.
+ /// The context of the current operation.
+ /// The decoded color components.
+ public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context)
+ {
+ var output = new ColorRgba32[data.Length / 2];
+
+ // HINT: Ignoring parallel execution since we wouldn't gain performance from it.
+
+ var span = data.Span;
+ for (var i = 0; i < output.Length; i++)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ output[i].r = span[i * 2];
+ output[i].g = span[i * 2 + 1];
+ output[i].b = 0;
+ output[i].a = 255;
+ }
+
+ return output;
+ }
+ }
+
+ ///
+ /// A class to decode data to RGB components.
+ ///
+ public class RawRgbDecoder : IRawDecoder
+ {
+ ///
+ /// Decode the data to color components.
+ ///
+ /// The data to decode.
+ /// The context of the current operation.
+ /// The decoded color components.
+ public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context)
+ {
+ var output = new ColorRgba32[data.Length / 3];
+
+ // HINT: Ignoring parallel execution since we wouldn't gain performance from it.
+
+ var span = data.Span;
+ for (var i = 0; i < output.Length; i++)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ output[i].r = span[i * 3];
+ output[i].g = span[i * 3 + 1];
+ output[i].b = span[i * 3 + 2];
+ output[i].a = 255;
+ }
+
+ return output;
+ }
+ }
+
+ ///
+ /// A class to decode data to RGBA components.
+ ///
+ public class RawRgbaDecoder : IRawDecoder
+ {
+ ///
+ /// Decode the data to color components.
+ ///
+ /// The data to decode.
+ /// The context of the current operation.
+ /// The decoded color components.
+ public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context)
+ {
+ var output = new ColorRgba32[data.Length / 4];
+
+ // HINT: Ignoring parallel execution since we wouldn't gain performance from it.
+
+ var span = data.Span;
+ for (var i = 0; i < output.Length; i++)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ output[i].r = span[i * 4];
+ output[i].g = span[i * 4 + 1];
+ output[i].b = span[i * 4 + 2];
+ output[i].a = span[i * 4 + 3];
+ }
+
+ return output;
+ }
+ }
+
+ ///
+ /// A class to decode data to BGRA components.
+ ///
+ public class RawBgraDecoder : IRawDecoder
+ {
+ ///
+ /// Decode the data to color components.
+ ///
+ /// The data to decode.
+ /// The context of the current operation.
+ /// The decoded color components.
+ public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context)
+ {
+ var output = new ColorRgba32[data.Length / 4];
+
+ // HINT: Ignoring parallel execution since we wouldn't gain performance from it.
+
+ var span = data.Span;
+ for (var i = 0; i < output.Length; i++)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ output[i].b = span[i * 4];
+ output[i].g = span[i * 4 + 1];
+ output[i].r = span[i * 4 + 2];
+ output[i].a = span[i * 4 + 3];
+ }
+
+ return output;
+ }
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/AtcBlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/AtcBlockEncoder.cs
new file mode 100644
index 0000000..9c153a6
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/AtcBlockEncoder.cs
@@ -0,0 +1,136 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ internal unsafe class AtcBlockEncoder : BaseBcBlockEncoder
+ {
+ private readonly Bc1BlockEncoder bc1BlockEncoder;
+
+ public AtcBlockEncoder()
+ {
+ bc1BlockEncoder = new Bc1BlockEncoder();
+ }
+
+ public override AtcBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ var atcBlock = new AtcBlock();
+
+ // EncodeBlock with BC1 first
+ var bc1Block = bc1BlockEncoder.EncodeBlock(block, quality);
+
+ // Atc specific modifications to BC1
+ // According to http://www.guildsoftware.com/papers/2012.Converting.DXTC.to.Atc.pdf
+
+ // Change color0 from rgb565 to rgb555 with method 0
+ atcBlock.color0 = new ColorRgb555(bc1Block.color0.R, bc1Block.color0.G, bc1Block.color0.B);
+ atcBlock.color1 = bc1Block.color1;
+
+ // Remap color indices from BC1 to ATC
+ var remap = stackalloc byte[] { 0, 3, 1, 2 };
+ for (var i = 0; i < 16; i++)
+ {
+ atcBlock[i] = remap[bc1Block[i]];
+ }
+
+ return atcBlock;
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRgbAtc;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRgb;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatAtcExt;
+ }
+ }
+
+ internal class AtcExplicitAlphaBlockEncoder : BaseBcBlockEncoder
+ {
+ private readonly AtcBlockEncoder atcBlockEncoder;
+
+ public AtcExplicitAlphaBlockEncoder()
+ {
+ atcBlockEncoder = new AtcBlockEncoder();
+ }
+
+ public override AtcExplicitAlphaBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ var atcBlock = atcBlockEncoder.EncodeBlock(block, quality);
+
+ // EncodeBlock alpha
+ var bc2AlphaBlock = new Bc2AlphaBlock();
+ for (var i = 0; i < 16; i++)
+ {
+ bc2AlphaBlock.SetAlpha(i, block[i].a);
+ }
+
+ return new AtcExplicitAlphaBlock
+ {
+ alphas = bc2AlphaBlock,
+ colors = atcBlock
+ };
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRgbaAtcExplicitAlpha;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRgba;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatAtcExplicitAlphaExt;
+ }
+ }
+
+ internal class AtcInterpolatedAlphaBlockEncoder : BaseBcBlockEncoder
+ {
+ private readonly Bc4ComponentBlockEncoder bc4BlockEncoder;
+ private readonly AtcBlockEncoder atcBlockEncoder;
+
+ public AtcInterpolatedAlphaBlockEncoder()
+ {
+ bc4BlockEncoder = new Bc4ComponentBlockEncoder(ColorComponent.A);
+ atcBlockEncoder = new AtcBlockEncoder();
+ }
+
+ public override AtcInterpolatedAlphaBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ var bc4Block = bc4BlockEncoder.EncodeBlock(block, quality);
+ var atcBlock = atcBlockEncoder.EncodeBlock(block, quality);
+
+ return new AtcInterpolatedAlphaBlock
+ {
+ alphas = bc4Block,
+ colors = atcBlock
+ };
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRgbaAtcInterpolatedAlpha;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRgba;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt;
+ }
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BaseBcBlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BaseBcBlockEncoder.cs
new file mode 100644
index 0000000..4ad540d
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BaseBcBlockEncoder.cs
@@ -0,0 +1,74 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ internal abstract class BaseBcBlockEncoder : IBcBlockEncoder where T : unmanaged where TBlock : unmanaged
+ {
+ private static readonly object lockObj = new object();
+
+ public byte[] Encode(TBlock[] blocks, int blockWidth, int blockHeight, CompressionQuality quality, OperationContext context)
+ {
+ var outputData = new byte[blockWidth * blockHeight * Unsafe.SizeOf()];
+
+ var currentBlocks = 0;
+ if (context.IsParallel)
+ {
+ var options = new ParallelOptions
+ {
+ CancellationToken = context.CancellationToken,
+ MaxDegreeOfParallelism = context.TaskCount
+ };
+ Parallel.For(0, blocks.Length, options, i =>
+ {
+ var outputBlocks = MemoryMarshal.Cast(outputData);
+ outputBlocks[i] = EncodeBlock(blocks[i], quality);
+
+ if (context.Progress != null)
+ {
+ lock (lockObj)
+ {
+ context.Progress.Report(++currentBlocks);
+ }
+ }
+ });
+ }
+ else
+ {
+ var outputBlocks = MemoryMarshal.Cast(outputData);
+ for (var i = 0; i < blocks.Length; i++)
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+
+ outputBlocks[i] = EncodeBlock(blocks[i], quality);
+
+ context.Progress?.Report(++currentBlocks);
+ }
+ }
+
+ return outputData;
+ }
+
+ public void EncodeBlock(TBlock block, CompressionQuality quality, Span output)
+ {
+ if (output.Length != Unsafe.SizeOf())
+ {
+ throw new Exception("Cannot encode block! Output buffer is not the correct size.");
+ }
+ var encoded = EncodeBlock(block, quality);
+ MemoryMarshal.Cast(output)[0] = encoded;
+ }
+
+ public abstract GlInternalFormat GetInternalFormat();
+ public abstract GlFormat GetBaseInternalFormat();
+ public abstract DxgiFormat GetDxgiFormat();
+ public int GetBlockSize()
+ {
+ return Unsafe.SizeOf();
+ }
+
+ public abstract T EncodeBlock(TBlock block, CompressionQuality quality);
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc1BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc1BlockEncoder.cs
new file mode 100644
index 0000000..29a2efe
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc1BlockEncoder.cs
@@ -0,0 +1,473 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ internal class Bc1BlockEncoder : BaseBcBlockEncoder
+ {
+ public override Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ switch (quality)
+ {
+ case CompressionQuality.Fast:
+ return Bc1BlockEncoderFast.EncodeBlock(block);
+ case CompressionQuality.Balanced:
+ return Bc1BlockEncoderBalanced.EncodeBlock(block);
+ case CompressionQuality.BestQuality:
+ return Bc1BlockEncoderSlow.EncodeBlock(block);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(quality), quality, null);
+ }
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRgbS3TcDxt1Ext;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRgb;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatBc1Unorm;
+ }
+
+ #region Encoding private stuff
+
+ private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f)
+ {
+ var output = new Bc1Block();
+
+ var pixels = rawBlock.AsSpan;
+
+ output.color0 = color0;
+ output.color1 = color1;
+
+ var c0 = color0.ToColorRgb24();
+ var c1 = color1.ToColorRgb24();
+
+ ReadOnlySpan colors = output.HasAlphaOrBlack ?
+ stackalloc ColorRgb24[] {
+ c0,
+ c1,
+ c0.InterpolateHalf(c1),
+ new ColorRgb24(0, 0, 0)
+ } : stackalloc ColorRgb24[] {
+ c0,
+ c1,
+ c0.InterpolateThird(c1, 1),
+ c0.InterpolateThird(c1, 2)
+ };
+
+ error = 0;
+ for (var i = 0; i < 16; i++)
+ {
+ var color = pixels[i];
+ output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e);
+ error += e;
+ }
+
+ return output;
+ }
+
+
+ #endregion
+
+ #region Encoders
+
+ private static class Bc1BlockEncoderFast
+ {
+
+ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var output = new Bc1Block();
+
+ var pixels = rawBlock.AsSpan;
+
+ RgbBoundingBox.Create565(pixels, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ output = TryColors(rawBlock, c0, c1, out var error);
+
+ return output;
+ }
+ }
+
+ private static class Bc1BlockEncoderBalanced
+ {
+ private const int MaxTries = 24 * 2;
+ private const float ErrorThreshold = 0.05f;
+
+ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (c0.data < c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ if (newC0.data < newC1.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ }
+
+ if (bestError < ErrorThreshold)
+ {
+ break;
+ }
+ }
+
+ return best;
+ }
+ }
+
+ private static class Bc1BlockEncoderSlow
+ {
+ private const int MaxTries = 9999;
+ private const float ErrorThreshold = 0.01f;
+
+ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (c0.data < c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ var lastChanged = 0;
+
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ if (newC0.data < newC1.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ lastChanged++;
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ lastChanged = 0;
+ }
+
+ if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount)
+ {
+ break;
+ }
+ }
+
+ return best;
+ }
+ }
+
+ #endregion
+ }
+
+ internal class Bc1AlphaBlockEncoder : BaseBcBlockEncoder
+ {
+ public override Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ switch (quality)
+ {
+ case CompressionQuality.Fast:
+ return Bc1AlphaBlockEncoderFast.EncodeBlock(block);
+ case CompressionQuality.Balanced:
+ return Bc1AlphaBlockEncoderBalanced.EncodeBlock(block);
+ case CompressionQuality.BestQuality:
+ return Bc1AlphaBlockEncoderSlow.EncodeBlock(block);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(quality), quality, null);
+ }
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRgbaS3TcDxt1Ext;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRgba;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatBc1Unorm;
+ }
+
+ #region Encoding private stuff
+
+ private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f)
+ {
+ var output = new Bc1Block();
+
+ var pixels = rawBlock.AsSpan;
+
+ output.color0 = color0;
+ output.color1 = color1;
+
+ var c0 = color0.ToColorRgb24();
+ var c1 = color1.ToColorRgb24();
+
+ var hasAlpha = output.HasAlphaOrBlack;
+
+ ReadOnlySpan colors = hasAlpha ?
+ stackalloc ColorRgb24[] {
+ c0,
+ c1,
+ c0.InterpolateHalf(c1),
+ new ColorRgb24(0, 0, 0)
+ } : stackalloc ColorRgb24[] {
+ c0,
+ c1,
+ c0.InterpolateThird(c1, 1),
+ c0.InterpolateThird(c1, 2)
+ };
+
+ error = 0;
+ for (var i = 0; i < 16; i++)
+ {
+ var color = pixels[i];
+ output[i] = ColorChooser.ChooseClosestColor4AlphaCutoff(colors, color, rWeight, gWeight, bWeight,
+ 128, hasAlpha, out var e);
+ error += e;
+ }
+
+ return output;
+ }
+
+ #endregion
+
+ #region Encoders
+
+ private static class Bc1AlphaBlockEncoderFast
+ {
+
+ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var output = new Bc1Block();
+
+ var pixels = rawBlock.AsSpan;
+
+ var hasAlpha = rawBlock.HasTransparentPixels();
+
+ RgbBoundingBox.Create565AlphaCutoff(pixels, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (hasAlpha && c0.data > c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ output = TryColors(rawBlock, c0, c1, out var error);
+
+ return output;
+ }
+ }
+
+ private static class Bc1AlphaBlockEncoderBalanced
+ {
+ private const int MaxTries = 24 * 2;
+ private const float ErrorThreshold = 0.05f;
+
+
+ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ var hasAlpha = rawBlock.HasTransparentPixels();
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (!hasAlpha && c0.data < c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+ else if (hasAlpha && c1.data < c0.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ if (!hasAlpha && newC0.data < newC1.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+ else if (hasAlpha && newC1.data < newC0.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ }
+
+ if (bestError < ErrorThreshold)
+ {
+ break;
+ }
+ }
+
+ return best;
+ }
+ }
+
+ private static class Bc1AlphaBlockEncoderSlow
+ {
+ private const int MaxTries = 9999;
+ private const float ErrorThreshold = 0.05f;
+
+ internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ var hasAlpha = rawBlock.HasTransparentPixels();
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (!hasAlpha && c0.data < c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+ else if (hasAlpha && c1.data < c0.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ var lastChanged = 0;
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ if (!hasAlpha && newC0.data < newC1.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+ else if (hasAlpha && newC1.data < newC0.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ lastChanged++;
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ lastChanged = 0;
+ }
+
+ if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount)
+ {
+ break;
+ }
+ }
+
+ return best;
+ }
+ }
+
+ #endregion
+
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc2BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc2BlockEncoder.cs
new file mode 100644
index 0000000..a38e750
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc2BlockEncoder.cs
@@ -0,0 +1,200 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ internal class Bc2BlockEncoder : BaseBcBlockEncoder
+ {
+ public override Bc2Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ switch (quality)
+ {
+ case CompressionQuality.Fast:
+ return Bc2BlockEncoderFast.EncodeBlock(block);
+ case CompressionQuality.Balanced:
+ return Bc2BlockEncoderBalanced.EncodeBlock(block);
+ case CompressionQuality.BestQuality:
+ return Bc2BlockEncoderSlow.EncodeBlock(block);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(quality), quality, null);
+ }
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRgbaS3TcDxt3Ext;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRgba;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatBc2Unorm;
+ }
+
+ #region Encoding private stuff
+
+ private static Bc2Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f)
+ {
+ var output = new Bc2Block();
+
+ var pixels = rawBlock.AsSpan;
+
+ output.color0 = color0;
+ output.color1 = color1;
+
+ var c0 = color0.ToColorRgb24();
+ var c1 = color1.ToColorRgb24();
+
+ ReadOnlySpan colors = stackalloc ColorRgb24[] {
+ c0,
+ c1,
+ c0.InterpolateThird(c1, 1),
+ c0.InterpolateThird(c1, 2)
+ };
+
+ error = 0;
+ for (var i = 0; i < 16; i++)
+ {
+ var color = pixels[i];
+ output.SetAlpha(i, color.a);
+ output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e);
+ error += e;
+ }
+
+ return output;
+ }
+
+
+ #endregion
+
+ #region Encoders
+
+ private static class Bc2BlockEncoderFast
+ {
+
+ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var principalAxis);
+ PcaVectors.GetMinMaxColor565(pixels, mean, principalAxis, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ var output = TryColors(rawBlock, c0, c1, out var _);
+
+ return output;
+ }
+ }
+
+ private static class Bc2BlockEncoderBalanced
+ {
+ private const int MaxTries = 24 * 2;
+ private const float ErrorThreshold = 0.05f;
+
+ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ }
+
+ if (bestError < ErrorThreshold)
+ {
+ break;
+ }
+ }
+
+ return best;
+ }
+ }
+
+ private static class Bc2BlockEncoderSlow
+ {
+ private const int MaxTries = 9999;
+ private const float ErrorThreshold = 0.01f;
+
+
+ internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (c0.data < c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ var lastChanged = 0;
+
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ if (newC0.data < newC1.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ lastChanged++;
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ lastChanged = 0;
+ }
+
+ if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount)
+ {
+ break;
+ }
+ }
+
+ return best;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc3BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc3BlockEncoder.cs
new file mode 100644
index 0000000..8f056b4
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc3BlockEncoder.cs
@@ -0,0 +1,207 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ internal class Bc3BlockEncoder : BaseBcBlockEncoder
+ {
+ private static readonly Bc4ComponentBlockEncoder bc4BlockEncoder = new Bc4ComponentBlockEncoder(ColorComponent.A);
+
+ public override Bc3Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ switch (quality)
+ {
+ case CompressionQuality.Fast:
+ return Bc3BlockEncoderFast.EncodeBlock(block);
+ case CompressionQuality.Balanced:
+ return Bc3BlockEncoderBalanced.EncodeBlock(block);
+ case CompressionQuality.BestQuality:
+ return Bc3BlockEncoderSlow.EncodeBlock(block);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(quality), quality, null);
+ }
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRgbaS3TcDxt5Ext;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRgba;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatBc3Unorm;
+ }
+
+ #region Encoding private stuff
+
+ private static Bc3Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f)
+ {
+ var output = new Bc3Block();
+
+ var pixels = rawBlock.AsSpan;
+
+ output.color0 = color0;
+ output.color1 = color1;
+
+ var c0 = color0.ToColorRgb24();
+ var c1 = color1.ToColorRgb24();
+
+ ReadOnlySpan colors = stackalloc ColorRgb24[] {
+ c0,
+ c1,
+ c0.InterpolateThird(c1, 1),
+ c0.InterpolateThird(c1, 2)
+ };
+
+ error = 0;
+ for (var i = 0; i < 16; i++)
+ {
+ var color = pixels[i];
+ output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e);
+ error += e;
+ }
+
+ return output;
+ }
+
+ #endregion
+
+ #region Encoders
+
+ private static class Bc3BlockEncoderFast
+ {
+ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var principalAxis);
+ PcaVectors.GetMinMaxColor565(pixels, mean, principalAxis, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (c0.data <= c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ var output = TryColors(rawBlock, c0, c1, out _);
+ output.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.Fast);
+
+ return output;
+ }
+ }
+
+ private static class Bc3BlockEncoderBalanced
+ {
+ private const int MaxTries = 24 * 2;
+ private const float ErrorThreshold = 0.05f;
+
+ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ }
+
+ if (bestError < ErrorThreshold)
+ {
+ break;
+ }
+ }
+ best.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.Balanced);
+ return best;
+ }
+ }
+
+ private static class Bc3BlockEncoderSlow
+ {
+ private const int MaxTries = 9999;
+ private const float ErrorThreshold = 0.01f;
+
+
+ internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock)
+ {
+ var pixels = rawBlock.AsSpan;
+
+ PcaVectors.Create(pixels, out var mean, out var pa);
+ PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max);
+
+ var c0 = max;
+ var c1 = min;
+
+ if (c0.data < c1.data)
+ {
+ var c = c0;
+ c0 = c1;
+ c1 = c;
+ }
+
+ var best = TryColors(rawBlock, c0, c1, out var bestError);
+
+ var lastChanged = 0;
+
+ for (var i = 0; i < MaxTries; i++)
+ {
+ var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i);
+
+ if (newC0.data < newC1.data)
+ {
+ var c = newC0;
+ newC0 = newC1;
+ newC1 = c;
+ }
+
+ var block = TryColors(rawBlock, newC0, newC1, out var error);
+
+ lastChanged++;
+
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ c0 = newC0;
+ c1 = newC1;
+ lastChanged = 0;
+ }
+
+ if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount)
+ {
+ break;
+ }
+ }
+
+ best.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.BestQuality);
+ return best;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc4BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc4BlockEncoder.cs
new file mode 100644
index 0000000..4bf766b
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc4BlockEncoder.cs
@@ -0,0 +1,268 @@
+using System.Diagnostics;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ internal class Bc4BlockEncoder : BaseBcBlockEncoder
+ {
+ private readonly Bc4ComponentBlockEncoder bc4Encoder;
+
+ public Bc4BlockEncoder(ColorComponent component)
+ {
+ bc4Encoder = new Bc4ComponentBlockEncoder(component);
+ }
+
+ public override Bc4Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ var output = new Bc4Block
+ {
+ componentBlock = bc4Encoder.EncodeBlock(block, quality)
+ };
+
+ return output;
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRedRgtc1Ext;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRed;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatBc4Unorm;
+ }
+ }
+
+ internal class Bc4ComponentBlockEncoder
+ {
+ private readonly ColorComponent component;
+
+ public Bc4ComponentBlockEncoder(ColorComponent component)
+ {
+ this.component = component;
+ }
+
+ public Bc4ComponentBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ var output = new Bc4ComponentBlock();
+
+ var pixels = block.AsSpan;
+ var colors = new byte[pixels.Length];
+
+ for (var i = 0; i < pixels.Length; i++)
+ colors[i] = ComponentHelper.ColorToComponent(pixels[i], component);
+
+ switch (quality)
+ {
+ case CompressionQuality.Fast:
+ return FindComponentValues(output, colors, 3);
+ case CompressionQuality.Balanced:
+ return FindComponentValues(output, colors, 4);
+ case CompressionQuality.BestQuality:
+ return FindComponentValues(output, colors, 8);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(quality), quality, null);
+ }
+ }
+
+ #region Encoding private stuff
+
+ private static Bc4ComponentBlock FindComponentValues(Bc4ComponentBlock colorBlock, byte[] pixels, int variations)
+ {
+
+ //Find min and max alpha
+ byte min = 255;
+ byte max = 0;
+ var hasExtremeValues = false;
+ for (var i = 0; i < pixels.Length; i++)
+ {
+ if (pixels[i] < 255 && pixels[i] > 0)
+ {
+ if (pixels[i] < min) min = pixels[i];
+ if (pixels[i] > max) max = pixels[i];
+ }
+ else
+ {
+ hasExtremeValues = true;
+ }
+ }
+
+
+ int SelectIndices(ref Bc4ComponentBlock block)
+ {
+ var cumulativeError = 0;
+ var c0 = block.Endpoint0;
+ var c1 = block.Endpoint1;
+ var colors = c0 > c1 ? stackalloc byte[] {
+ c0,
+ c1,
+ c0.InterpolateSeventh(c1, 1),
+ c0.InterpolateSeventh(c1, 2),
+ c0.InterpolateSeventh(c1, 3),
+ c0.InterpolateSeventh(c1, 4),
+ c0.InterpolateSeventh(c1, 5),
+ c0.InterpolateSeventh(c1, 6)
+ } : stackalloc byte[] {
+ c0,
+ c1,
+ c0.InterpolateFifth(c1, 1),
+ c0.InterpolateFifth(c1, 2),
+ c0.InterpolateFifth(c1, 3),
+ c0.InterpolateFifth(c1, 4),
+ 0,
+ 255
+ };
+ for (var i = 0; i < pixels.Length; i++)
+ {
+ byte bestIndex = 0;
+ var bestError = Math.Abs(pixels[i] - colors[0]);
+ for (byte j = 1; j < colors.Length; j++)
+ {
+ var error = Math.Abs(pixels[i] - colors[j]);
+ if (error < bestError)
+ {
+ bestIndex = j;
+ bestError = error;
+ }
+
+ if (bestError == 0) break;
+ }
+
+ block.SetComponentIndex(i, bestIndex);
+ cumulativeError += bestError * bestError;
+ }
+
+ return cumulativeError;
+ }
+
+ //everything is either fully black or fully red
+ if (hasExtremeValues && min == 255 && max == 0)
+ {
+ colorBlock.Endpoint0 = 0;
+ colorBlock.Endpoint1 = 255;
+ var error = SelectIndices(ref colorBlock);
+ Debug.Assert(0 == error);
+ return colorBlock;
+ }
+
+ var best = colorBlock;
+ best.Endpoint0 = max;
+ best.Endpoint1 = min;
+ var bestError = SelectIndices(ref best);
+ if (bestError == 0)
+ {
+ return best;
+ }
+
+ for (var i = (byte)variations; i > 0; i--)
+ {
+ {
+ var c0 = ByteHelper.ClampToByte(max - i);
+ var c1 = ByteHelper.ClampToByte(min + i);
+ var block = colorBlock;
+ block.Endpoint0 = hasExtremeValues ? c1 : c0;
+ block.Endpoint1 = hasExtremeValues ? c0 : c1;
+ var error = SelectIndices(ref block);
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ max = c0;
+ min = c1;
+ }
+ }
+ {
+ var c0 = ByteHelper.ClampToByte(max + i);
+ var c1 = ByteHelper.ClampToByte(min - i);
+ var block = colorBlock;
+ block.Endpoint0 = hasExtremeValues ? c1 : c0;
+ block.Endpoint1 = hasExtremeValues ? c0 : c1;
+ var error = SelectIndices(ref block);
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ max = c0;
+ min = c1;
+ }
+ }
+ {
+ var c0 = ByteHelper.ClampToByte(max);
+ var c1 = ByteHelper.ClampToByte(min - i);
+ var block = colorBlock;
+ block.Endpoint0 = hasExtremeValues ? c1 : c0;
+ block.Endpoint1 = hasExtremeValues ? c0 : c1;
+ var error = SelectIndices(ref block);
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ max = c0;
+ min = c1;
+ }
+ }
+ {
+ var c0 = ByteHelper.ClampToByte(max + i);
+ var c1 = ByteHelper.ClampToByte(min);
+ var block = colorBlock;
+ block.Endpoint0 = hasExtremeValues ? c1 : c0;
+ block.Endpoint1 = hasExtremeValues ? c0 : c1;
+ var error = SelectIndices(ref block);
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ max = c0;
+ min = c1;
+ }
+ }
+ {
+ var c0 = ByteHelper.ClampToByte(max);
+ var c1 = ByteHelper.ClampToByte(min + i);
+ var block = colorBlock;
+ block.Endpoint0 = hasExtremeValues ? c1 : c0;
+ block.Endpoint1 = hasExtremeValues ? c0 : c1;
+ var error = SelectIndices(ref block);
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ max = c0;
+ min = c1;
+ }
+ }
+ {
+ var c0 = ByteHelper.ClampToByte(max - i);
+ var c1 = ByteHelper.ClampToByte(min);
+ var block = colorBlock;
+ block.Endpoint0 = hasExtremeValues ? c1 : c0;
+ block.Endpoint1 = hasExtremeValues ? c0 : c1;
+ var error = SelectIndices(ref block);
+ if (error < bestError)
+ {
+ best = block;
+ bestError = error;
+ max = c0;
+ min = c1;
+ }
+ }
+
+ if (bestError < 5)
+ {
+ break;
+ }
+ }
+
+ return best;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc5BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc5BlockEncoder.cs
new file mode 100644
index 0000000..5db2aed
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc5BlockEncoder.cs
@@ -0,0 +1,41 @@
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ internal class Bc5BlockEncoder : BaseBcBlockEncoder
+ {
+ private readonly Bc4ComponentBlockEncoder redBlockEncoder;
+ private readonly Bc4ComponentBlockEncoder greenBlockEncoder;
+
+ public Bc5BlockEncoder(ColorComponent component1, ColorComponent component2)
+ {
+ redBlockEncoder = new Bc4ComponentBlockEncoder(component1);
+ greenBlockEncoder = new Bc4ComponentBlockEncoder(component2);
+ }
+
+ public override Bc5Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality)
+ {
+ return new Bc5Block
+ {
+ redBlock = redBlockEncoder.EncodeBlock(block, quality),
+ greenBlock = greenBlockEncoder.EncodeBlock(block, quality)
+ };
+ }
+
+ public override GlInternalFormat GetInternalFormat()
+ {
+ return GlInternalFormat.GlCompressedRedGreenRgtc2Ext;
+ }
+
+ public override GlFormat GetBaseInternalFormat()
+ {
+ return GlFormat.GlRg;
+ }
+
+ public override DxgiFormat GetDxgiFormat()
+ {
+ return DxgiFormat.DxgiFormatBc5Unorm;
+ }
+ }
+}
diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BcEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BcEncoder.cs
new file mode 100644
index 0000000..85a33b8
--- /dev/null
+++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BcEncoder.cs
@@ -0,0 +1,2185 @@
+using CommunityToolkit.HighPerformance;
+using System.Diagnostics;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Options;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared;
+using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles;
+
+namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder
+{
+ ///
+ /// The pixel format determining the rgba layout of input data in .
+ ///
+ public enum PixelFormat
+ {
+ ///
+ /// 8 bits per channel RGBA.
+ ///
+ Rgba32,
+
+ ///
+ /// 8 bits per channel BGRA.
+ ///
+ Bgra32,
+
+ ///
+ /// 8 bits per channel ARGB.
+ ///
+ Argb32,
+
+ ///
+ /// 8 bits per channel RGB.
+ ///
+ Rgb24,
+
+ ///
+ /// 8 bits per channel BGR.
+ ///
+ Bgr24
+ }
+
+ ///
+ /// Handles all encoding of images into compressed or uncompressed formats. For decoding,
+ ///
+ public class BcEncoder
+ {
+ ///
+ /// The input options of the encoder.
+ ///
+ public EncoderInputOptions InputOptions { get; } = new EncoderInputOptions();
+
+ ///
+ /// The output options of the encoder.
+ ///
+ public EncoderOutputOptions OutputOptions { get; } = new EncoderOutputOptions();
+
+ ///
+ /// The encoder options.
+ ///
+ public EncoderOptions Options { get; } = new EncoderOptions();
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The block compression Format to encode an image with.
+ public BcEncoder(CompressionFormat format = CompressionFormat.Bc1)
+ {
+ OutputOptions.Format = format;
+ }
+
+ #region LDR
+ #region Async Api
+
+ ///
+ /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously.
+ ///
+ /// The input to encode represented by a .
+ /// The width of the image.
+ /// The height of the image.
+ /// The pixel format the given data is in.
+ /// The stream to write the encoded image to.
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ public Task EncodeToStreamAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, Stream outputStream, CancellationToken token = default)
+ {
+ return Task.Run(() =>
+ {
+ EncodeToStreamInternal(ByteToColorMemory(input.Span, width, height, format), outputStream, token);
+ }, token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously.
+ ///
+ /// The input to encode represented by a .
+ /// The stream to write the encoded image to.
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ public Task EncodeToStreamAsync(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token = default)
+ {
+ return Task.Run(() =>
+ {
+ EncodeToStreamInternal(input, outputStream, default);
+ }, token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Ktx file asynchronously.
+ ///
+ /// The input to encode represented by a .
+ /// The width of the image.
+ /// The height of the image.
+ /// The pixel format the given data is in.
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// The Ktx file containing the encoded image.
+ public Task EncodeToKtxAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToKtxInternal(ByteToColorMemory(input.Span, width, height, format), token), token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Ktx file asynchronously.
+ ///
+ /// The input to encode represented by a .
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// The Ktx file containing the encoded image.
+ public Task EncodeToKtxAsync(ReadOnlyMemory2D input, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToKtxInternal(input, token), token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Dds file asynchronously.
+ ///
+ /// The input to encode represented by a .
+ /// The width of the image.
+ /// The height of the image.
+ /// The pixel format the given data is in.
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// The Dds file containing the encoded image.
+ public Task EncodeToDdsAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToDdsInternal(ByteToColorMemory(input.Span, width, height, format), token), token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a Dds file asynchronously.
+ ///
+ /// The input to encode represented by a .
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// The Dds file containing the encoded image.
+ public Task EncodeToDdsAsync(ReadOnlyMemory2D input, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToDdsInternal(input, token), token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into a list of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data.
+ ///
+ /// The input to encode represented by a .
+ /// The width of the image.
+ /// The height of the image.
+ /// The pixel format the given data is in.
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// A list of raw encoded mipmap input.
+ public Task EncodeToRawBytesAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToRawInternal(ByteToColorMemory(input.Span, width, height, format), token), token);
+ }
+
+ ///
+ /// Encodes all mipmap levels into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data.
+ ///
+ /// The input to encode represented by a .
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// A list of raw encoded mipmap input.
+ /// To get the width and height of the encoded mip levels, see .
+ public Task EncodeToRawBytesAsync(ReadOnlyMemory2D input, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToRawInternal(input, token), token);
+ }
+
+ ///
+ /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data.
+ ///
+ /// The input to encode represented by a .
+ /// The width of the image.
+ /// The height of the image.
+ /// The pixel format the given data is in.
+ /// The mipmap to encode.
+ /// The cancellation token for this operation. Can be default, if the operation is not asynchronous.
+ /// The raw encoded input.
+ /// To get the width and height of the encoded mip level, see .
+ public Task EncodeToRawBytesAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, int mipLevel, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToRawInternal(ByteToColorMemory(input.Span, width, height, format), mipLevel, out _, out _, token), token);
+ }
+
+ ///
+ /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data.
+ ///
+ /// The input to encode represented by a .
+ /// The mipmap to encode.
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// The raw encoded input.
+ /// To get the width and height of the encoded mip level, see .
+ public Task EncodeToRawBytesAsync(ReadOnlyMemory2D input, int mipLevel, CancellationToken token = default)
+ {
+ return Task.Run(() => EncodeToRawInternal(input, mipLevel, out _, out _, token), token);
+ }
+
+ ///
+ /// Encodes all mipMaps of a cubeMap image to a stream asynchronously either in ktx or dds format.
+ /// The format can be set in .
+ /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z.
+ ///
+ /// The positive X-axis face of the cubeMap
+ /// The negative X-axis face of the cubeMap
+ /// The positive Y-axis face of the cubeMap
+ /// The negative Y-axis face of the cubeMap
+ /// The positive Z-axis face of the cubeMap
+ /// The negative Z-axis face of the cubeMap
+ /// The stream to write the encoded image to.
+ /// The cancellation token for this operation. Can be default if cancellation is not needed.
+ /// A .
+ public Task EncodeCubeMapToStreamAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left,
+ ReadOnlyMemory2D