├── .gitattributes ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── .gitmodules ├── WorldMapCompiler.sln └── WorldMapCompiler ├── BLP ├── BlpFile.cs └── DXTDecompression.cs ├── CASCDBCProvider.cs ├── Program.cs ├── WorldMapCompiler.csproj └── settings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | create_release: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Create Release 14 | id: create_release 15 | uses: softprops/action-gh-release@v0.1.13 16 | with: 17 | name: ${{ github.ref_name }} 18 | draft: false 19 | prerelease: false 20 | generate_release_notes: false 21 | build: 22 | name: Build Release 23 | needs: create_release 24 | strategy: 25 | matrix: 26 | kind: ['linux', 'windows', 'macOS'] 27 | include: 28 | - kind: linux 29 | os: ubuntu-latest 30 | target: linux-x64 31 | - kind: windows 32 | os: windows-latest 33 | target: win-x64 34 | - kind: macOS 35 | os: macos-latest 36 | target: osx-x64 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - uses: actions/checkout@v3 40 | with: 41 | submodules: recursive 42 | - name: Setup .NET 43 | uses: actions/setup-dotnet@v3 44 | with: 45 | dotnet-version: 8.0.x 46 | - name: Restore dependencies 47 | run: dotnet restore 48 | - name: Build 49 | run: dotnet build --configuration Release --no-restore 50 | - name: Test 51 | run: dotnet test --no-build --verbosity normal 52 | - name: Archive Release 53 | uses: thedoctor0/zip-release@main 54 | with: 55 | directory: ${{ github.workspace }}/WorldMapCompiler/bin/Release/net8.0 56 | type: 'zip' 57 | filename: ${{ github.workspace }}/Release-${{ matrix.target }}.zip 58 | - name: Release 59 | uses: softprops/action-gh-release@v0.1.13 60 | with: 61 | files: ${{ github.workspace }}/Release-${{ matrix.target }}.zip 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio stuff 2 | *.[Cc]ache 3 | *.bak 4 | *.baml 5 | *.dll 6 | *.DS_Store 7 | *.exe 8 | *.g.cs 9 | *.g.i.cs 10 | *.log 11 | *.lref 12 | *.pdb 13 | *.resharper 14 | *.resources 15 | *.suo 16 | *.user 17 | *.dtbcache 18 | .vs/* 19 | *FileListAbsolute* 20 | *TemporaryGenerated* 21 | [Bb]in/ 22 | [Dd]ebug/ 23 | [Oo]bj/ 24 | [Rr]elease/ 25 | Properties/ 26 | [Tt]humbs.db 27 | _ReSharper.* 28 | Ankh.NoLoad 29 | build.force 30 | build/ 31 | x64/ 32 | 33 | # Libraries 34 | CSDBCReader.dll 35 | SereniaBLPLib.dll 36 | SharpDX 37 | 38 | # NuGet 39 | Packages/* 40 | 41 | # Don't include cached data files 42 | *.db2 43 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "CASCLib"] 2 | path = CASCLib 3 | url = https://github.com/Marlamin/CASCLib 4 | [submodule "DBCD"] 5 | path = DBCD 6 | url = https://github.com/wowdev/DBCD 7 | -------------------------------------------------------------------------------- /WorldMapCompiler.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33205.214 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldMapCompiler", "WorldMapCompiler\WorldMapCompiler.csproj", "{48D49A12-6167-4C48-9699-83222DF2BFC8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DBCD", "DBCD\DBCD\DBCD.csproj", "{91D89A12-EB0E-4FDF-9D78-F1247AFD52EB}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DBFileReaderLib", "DBCD\DBFileReaderLib\DBFileReaderLib.csproj", "{AE3E048C-E8D5-43CB-BEBA-8BDC2FEFD069}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CascLib", "CASCLib\CascLib\CascLib.csproj", "{53A50EEA-7FA4-4313-A9C8-D7C124CA0B41}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {48D49A12-6167-4C48-9699-83222DF2BFC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {48D49A12-6167-4C48-9699-83222DF2BFC8}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {48D49A12-6167-4C48-9699-83222DF2BFC8}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {48D49A12-6167-4C48-9699-83222DF2BFC8}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {91D89A12-EB0E-4FDF-9D78-F1247AFD52EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {91D89A12-EB0E-4FDF-9D78-F1247AFD52EB}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {91D89A12-EB0E-4FDF-9D78-F1247AFD52EB}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {91D89A12-EB0E-4FDF-9D78-F1247AFD52EB}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {AE3E048C-E8D5-43CB-BEBA-8BDC2FEFD069}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {AE3E048C-E8D5-43CB-BEBA-8BDC2FEFD069}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {AE3E048C-E8D5-43CB-BEBA-8BDC2FEFD069}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {AE3E048C-E8D5-43CB-BEBA-8BDC2FEFD069}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {53A50EEA-7FA4-4313-A9C8-D7C124CA0B41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {53A50EEA-7FA4-4313-A9C8-D7C124CA0B41}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {53A50EEA-7FA4-4313-A9C8-D7C124CA0B41}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {53A50EEA-7FA4-4313-A9C8-D7C124CA0B41}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {625A2E4A-74E7-4752-B032-40820809D4E7} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /WorldMapCompiler/BLP/BlpFile.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) <2011> 3 | * Permission is hereby granted, free of charge, to any person obtaining 4 | * a copy of this software and associated documentation files (the "Software"), 5 | * to deal in the Software without restriction, including without limitation 6 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | * and/or sell copies of the Software, and to permit persons to whom the 8 | * Software is furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included 11 | * in all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 17 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | */ 20 | 21 | using System; 22 | using System.Drawing; 23 | using System.Drawing.Imaging; 24 | using System.IO; 25 | using System.Runtime.InteropServices; 26 | 27 | namespace SereniaBLPLib 28 | { 29 | // Some Helper Struct to store Color-Data 30 | public struct ARGBColor8 31 | { 32 | public byte red; 33 | public byte green; 34 | public byte blue; 35 | public byte alpha; 36 | 37 | /// 38 | /// Converts the given Pixel-Array into the BGRA-Format 39 | /// This will also work vice versa 40 | /// 41 | /// 42 | public static void convertToBGRA(byte[] pixel) 43 | { 44 | byte tmp; 45 | for (int i = 0; i < pixel.Length; i += 4) 46 | { 47 | tmp = pixel[i]; // store red 48 | pixel[i] = pixel[i + 2]; // Write blue into red 49 | pixel[i + 2] = tmp; // write stored red into blue 50 | } 51 | } 52 | } 53 | 54 | public sealed class BlpFile : IDisposable 55 | { 56 | //uint type; // compression: 0 = JPEG Compression, 1 = Uncompressed or DirectX Compression 57 | public byte encoding; // 1 = Uncompressed, 2 = DirectX Compressed 58 | public byte alphaDepth; // 0 = no alpha, 1 = 1 Bit, 4 = Bit (only DXT3), 8 = 8 Bit Alpha 59 | public byte alphaEncoding; // 0: DXT1 alpha (0 or 1 Bit alpha), 1 = DXT2/3 alpha (4 Bit), 7: DXT4/5 (interpolated alpha) 60 | public byte hasMipmaps; // If true (1), then there are Mipmaps 61 | public int width; // X Resolution of the biggest Mipmap 62 | public int height; // Y Resolution of the biggest Mipmap 63 | 64 | public uint[] mipmapOffsets = new uint[16]; // Offset for every Mipmap level. If 0 = no more mitmap level 65 | public uint[] mippmapSize = new uint[16]; // Size for every level 66 | ARGBColor8[] paletteBGRA = new ARGBColor8[256]; // The color-palette for non-compressed pictures 67 | 68 | Stream str; // Reference of the stream 69 | 70 | /// 71 | /// Extracts the palettized Image-Data from the given Mipmap and returns a byte-Array in the 32Bit RGBA-Format 72 | /// 73 | /// The desired Mipmap-Level. If the given level is invalid, the smallest available level is choosen 74 | /// 75 | /// 76 | /// 77 | /// Pixel-data 78 | private byte[] GetPictureUncompressedByteArray(int w, int h, byte[] data) 79 | { 80 | int length = w * h; 81 | byte[] pic = new byte[length * 4]; 82 | for (int i = 0; i < length; i++) 83 | { 84 | pic[i * 4] = paletteBGRA[data[i]].red; 85 | pic[i * 4 + 1] = paletteBGRA[data[i]].green; 86 | pic[i * 4 + 2] = paletteBGRA[data[i]].blue; 87 | pic[i * 4 + 3] = GetAlpha(data, i, length); 88 | } 89 | return pic; 90 | } 91 | 92 | private byte GetAlpha(byte[] data, int index, int alphaStart) 93 | { 94 | switch (alphaDepth) 95 | { 96 | default: 97 | return 0xFF; 98 | case 1: 99 | { 100 | byte b = data[alphaStart + (index / 8)]; 101 | return (byte)((b & (0x01 << (index % 8))) == 0 ? 0x00 : 0xff); 102 | } 103 | case 4: 104 | { 105 | byte b = data[alphaStart + (index / 2)]; 106 | return (byte)(index % 2 == 0 ? (b & 0x0F) << 4 : b & 0xF0); 107 | } 108 | case 8: 109 | return data[alphaStart + index]; 110 | } 111 | } 112 | 113 | /// 114 | /// Returns the raw Mipmap-Image Data. This data can either be compressed or uncompressed, depending on the Header-Data 115 | /// 116 | /// 117 | /// 118 | public byte[] GetPictureData(int mipmapLevel) 119 | { 120 | if (str != null) 121 | { 122 | byte[] data = new byte[mippmapSize[mipmapLevel]]; 123 | str.Position = mipmapOffsets[mipmapLevel]; 124 | str.Read(data, 0, data.Length); 125 | return data; 126 | } 127 | return null; 128 | } 129 | 130 | /// 131 | /// Returns the amount of Mipmaps in this BLP-File 132 | /// 133 | public int MipMapCount 134 | { 135 | get 136 | { 137 | int i = 0; 138 | while (mipmapOffsets[i] != 0) i++; 139 | return i; 140 | } 141 | } 142 | 143 | public BlpFile(Stream stream) 144 | { 145 | str = stream; 146 | byte[] buffer = new byte[4]; 147 | // Well, have to fix this... looks weird o.O 148 | str.Read(buffer, 0, 4); 149 | 150 | // Checking for correct Magic-Code 151 | if (BitConverter.ToUInt32(buffer, 0) != 0x32504c42) 152 | throw new Exception("Invalid BLP Format"); 153 | 154 | // Reading type 155 | str.Read(buffer, 0, 4); 156 | uint type = BitConverter.ToUInt32(buffer, 0); 157 | if (type != 1) 158 | throw new Exception("Invalid BLP-Type! Should be 1 but " + type + " was found"); 159 | 160 | // Reading encoding, alphaBitDepth, alphaEncoding and hasMipmaps 161 | str.Read(buffer, 0, 4); 162 | encoding = buffer[0]; 163 | alphaDepth = buffer[1]; 164 | alphaEncoding = buffer[2]; 165 | hasMipmaps = buffer[3]; 166 | 167 | // Reading width 168 | str.Read(buffer, 0, 4); 169 | width = BitConverter.ToInt32(buffer, 0); 170 | 171 | // Reading height 172 | str.Read(buffer, 0, 4); 173 | height = BitConverter.ToInt32(buffer, 0); 174 | 175 | // Reading MipmapOffset Array 176 | for (int i = 0; i < 16; i++) 177 | { 178 | stream.Read(buffer, 0, 4); 179 | mipmapOffsets[i] = BitConverter.ToUInt32(buffer, 0); 180 | } 181 | 182 | // Reading MipmapSize Array 183 | for (int i = 0; i < 16; i++) 184 | { 185 | str.Read(buffer, 0, 4); 186 | mippmapSize[i] = BitConverter.ToUInt32(buffer, 0); 187 | } 188 | 189 | // When encoding is 1, there is no image compression and we have to read a color palette 190 | if (encoding == 1) 191 | { 192 | // Reading palette 193 | for (int i = 0; i < 256; i++) 194 | { 195 | byte[] color = new byte[4]; 196 | str.Read(color, 0, 4); 197 | paletteBGRA[i].blue = color[0]; 198 | paletteBGRA[i].green = color[1]; 199 | paletteBGRA[i].red = color[2]; 200 | paletteBGRA[i].alpha = color[3]; 201 | } 202 | } 203 | } 204 | 205 | /// 206 | /// Returns the uncompressed image as a bytarray in the 32pppRGBA-Format 207 | /// 208 | private byte[] GetImageBytes(int w, int h, byte[] data) 209 | { 210 | switch (encoding) 211 | { 212 | case 1: 213 | return GetPictureUncompressedByteArray(w, h, data); 214 | case 2: 215 | DXTDecompression.DXTFlags flag = (alphaDepth > 1) ? ((alphaEncoding == 7) ? DXTDecompression.DXTFlags.DXT5 : DXTDecompression.DXTFlags.DXT3) : DXTDecompression.DXTFlags.DXT1; 216 | return DXTDecompression.DecompressImage(w, h, data, flag); 217 | case 3: 218 | return data; 219 | default: 220 | return new byte[0]; 221 | } 222 | } 223 | 224 | /// 225 | /// Converts the BLP to a System.Drawing.Bitmap 226 | /// 227 | /// The desired Mipmap-Level. If the given level is invalid, the smallest available level is choosen 228 | /// The Bitmap 229 | public Bitmap GetBitmap(int mipmapLevel) 230 | { 231 | if (mipmapLevel >= MipMapCount) mipmapLevel = MipMapCount - 1; 232 | if (mipmapLevel < 0) mipmapLevel = 0; 233 | 234 | int scale = (int)Math.Pow(2, mipmapLevel); 235 | int w = width / scale; 236 | int h = height / scale; 237 | Bitmap bmp = new Bitmap(w, h); 238 | 239 | byte[] data = GetPictureData(mipmapLevel); 240 | byte[] pic = GetImageBytes(w, h, data); // This bytearray stores the Pixel-Data 241 | 242 | // Faster bitmap Data copy 243 | BitmapData bmpdata = bmp.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); 244 | // when we want to copy the pixeldata directly into the bitmap, we have to convert them into BGRA befor doing so 245 | ARGBColor8.convertToBGRA(pic); 246 | Marshal.Copy(pic, 0, bmpdata.Scan0, pic.Length); // copy! :D 247 | bmp.UnlockBits(bmpdata); 248 | 249 | return bmp; 250 | } 251 | 252 | /// 253 | /// Runs close() 254 | /// 255 | public void Dispose() 256 | { 257 | Close(); 258 | } 259 | 260 | /// 261 | /// Closes the Memorystream 262 | /// 263 | public void Close() 264 | { 265 | str?.Close(); 266 | str = null; 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /WorldMapCompiler/BLP/DXTDecompression.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) <2011> 3 | * Permission is hereby granted, free of charge, to any person obtaining 4 | * a copy of this software and associated documentation files (the "Software"), 5 | * to deal in the Software without restriction, including without limitation 6 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | * and/or sell copies of the Software, and to permit persons to whom the 8 | * Software is furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included 11 | * in all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 17 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | */ 20 | 21 | // most of the algorithms and data used in this Class-file has been ported from LibSquish! 22 | // http://code.google.com/p/libsquish/ 23 | 24 | 25 | namespace SereniaBLPLib 26 | { 27 | public static class DXTDecompression 28 | { 29 | public enum DXTFlags 30 | { 31 | DXT1 = 1 << 0, 32 | DXT3 = 1 << 1, 33 | DXT5 = 1 << 2, 34 | // Additional Enums not implemented :o 35 | } 36 | 37 | private static void Decompress(byte[] rgba, byte[] block, int blockIndex, DXTFlags flags) 38 | { 39 | // get the block locations 40 | int colorBlockIndex = blockIndex; 41 | 42 | if ((flags & (DXTFlags.DXT3 | DXTFlags.DXT5)) != 0) 43 | colorBlockIndex += 8; 44 | 45 | // decompress color 46 | DecompressColor(rgba, block, colorBlockIndex, (flags & DXTFlags.DXT1) != 0); 47 | 48 | // decompress alpha separately if necessary 49 | if ((flags & DXTFlags.DXT3) != 0) 50 | DecompressAlphaDxt3(rgba, block, blockIndex); 51 | else if ((flags & DXTFlags.DXT5) != 0) 52 | DecompressAlphaDxt5(rgba, block, blockIndex); 53 | } 54 | 55 | private static void DecompressAlphaDxt3(byte[] rgba, byte[] block, int blockIndex) 56 | { 57 | // Unpack the alpha values pairwise 58 | for (int i = 0; i < 8; i++) 59 | { 60 | // Quantise down to 4 bits 61 | byte quant = block[blockIndex + i]; 62 | 63 | byte lo = (byte)(quant & 0x0F); 64 | byte hi = (byte)(quant & 0xF0); 65 | 66 | // Convert back up to bytes 67 | rgba[8 * i + 3] = (byte)(lo | (lo << 4)); 68 | rgba[8 * i + 7] = (byte)(hi | (hi >> 4)); 69 | } 70 | } 71 | 72 | private static void DecompressAlphaDxt5(byte[] rgba, byte[] block, int blockIndex) 73 | { 74 | // Get the two alpha values 75 | byte alpha0 = block[blockIndex + 0]; 76 | byte alpha1 = block[blockIndex + 1]; 77 | 78 | // compare the values to build the codebook 79 | byte[] codes = new byte[8]; 80 | codes[0] = alpha0; 81 | codes[1] = alpha1; 82 | if (alpha0 <= alpha1) 83 | { 84 | // Use 5-Alpha Codebook 85 | for (int i = 1; i < 5; i++) 86 | codes[1 + i] = (byte)(((5 - i) * alpha0 + i * alpha1) / 5); 87 | codes[6] = 0; 88 | codes[7] = 255; 89 | } 90 | else 91 | { 92 | // Use 7-Alpha Codebook 93 | for (int i = 1; i < 7; i++) 94 | { 95 | codes[i + 1] = (byte)(((7 - i) * alpha0 + i * alpha1) / 7); 96 | } 97 | } 98 | 99 | // decode indices 100 | byte[] indices = new byte[16]; 101 | int blockSrc_pos = 2; 102 | int indices_pos = 0; 103 | for (int i = 0; i < 2; i++) 104 | { 105 | // grab 3 bytes 106 | int value = 0; 107 | for (int j = 0; j < 3; j++) 108 | { 109 | int _byte = block[blockIndex + blockSrc_pos++]; 110 | value |= (_byte << 8 * j); 111 | } 112 | 113 | // unpack 8 3-bit values from it 114 | for (int j = 0; j < 8; j++) 115 | { 116 | int index = (value >> 3 * j) & 0x07; 117 | indices[indices_pos++] = (byte)index; 118 | } 119 | } 120 | 121 | // write out the indexed codebook values 122 | for (int i = 0; i < 16; i++) 123 | { 124 | rgba[4 * i + 3] = codes[indices[i]]; 125 | } 126 | } 127 | 128 | private static void DecompressColor(byte[] rgba, byte[] block, int blockIndex, bool isDxt1) 129 | { 130 | // Unpack Endpoints 131 | byte[] codes = new byte[16]; 132 | int a = Unpack565(block, blockIndex, 0, codes, 0); 133 | int b = Unpack565(block, blockIndex, 2, codes, 4); 134 | 135 | // generate Midpoints 136 | for (int i = 0; i < 3; i++) 137 | { 138 | int c = codes[i]; 139 | int d = codes[4 + i]; 140 | 141 | if (isDxt1 && a <= b) 142 | { 143 | codes[8 + i] = (byte)((c + d) / 2); 144 | codes[12 + i] = 0; 145 | } 146 | else 147 | { 148 | codes[8 + i] = (byte)((2 * c + d) / 3); 149 | codes[12 + i] = (byte)((c + 2 * d) / 3); 150 | } 151 | } 152 | 153 | // Fill in alpha for intermediate values 154 | codes[8 + 3] = 255; 155 | codes[12 + 3] = (isDxt1 && a <= b) ? (byte)0 : (byte)255; 156 | 157 | // unpack the indices 158 | byte[] indices = new byte[16]; 159 | for (int i = 0; i < 4; i++) 160 | { 161 | byte packed = block[blockIndex + 4 + i]; 162 | 163 | indices[0 + i * 4] = (byte)(packed & 0x3); 164 | indices[1 + i * 4] = (byte)((packed >> 2) & 0x3); 165 | indices[2 + i * 4] = (byte)((packed >> 4) & 0x3); 166 | indices[3 + i * 4] = (byte)((packed >> 6) & 0x3); 167 | } 168 | 169 | // store out the colours 170 | for (int i = 0; i < 16; i++) 171 | { 172 | int offset = 4 * indices[i]; 173 | 174 | rgba[4 * i + 0] = codes[offset + 0]; 175 | rgba[4 * i + 1] = codes[offset + 1]; 176 | rgba[4 * i + 2] = codes[offset + 2]; 177 | rgba[4 * i + 3] = codes[offset + 3]; 178 | } 179 | } 180 | 181 | private static int Unpack565(byte[] block, int blockIndex, int packed_offset, byte[] colour, int colour_offset) 182 | { 183 | // Build packed value 184 | int value = block[blockIndex + packed_offset] | (block[blockIndex + 1 + packed_offset] << 8); 185 | 186 | // get components in the stored range 187 | byte red = (byte)((value >> 11) & 0x1F); 188 | byte green = (byte)((value >> 5) & 0x3F); 189 | byte blue = (byte)(value & 0x1F); 190 | 191 | // Scale up to 8 Bit 192 | colour[0 + colour_offset] = (byte)((red << 3) | (red >> 2)); 193 | colour[1 + colour_offset] = (byte)((green << 2) | (green >> 4)); 194 | colour[2 + colour_offset] = (byte)((blue << 3) | (blue >> 2)); 195 | colour[3 + colour_offset] = 255; 196 | 197 | return value; 198 | } 199 | 200 | public static byte[] DecompressImage(int width, int height, byte[] data, DXTFlags flags) 201 | { 202 | byte[] rgba = new byte[width * height * 4]; 203 | 204 | // initialise the block input 205 | int sourceBlock_pos = 0; 206 | int bytesPerBlock = (flags & DXTFlags.DXT1) != 0 ? 8 : 16; 207 | byte[] targetRGBA = new byte[4 * 16]; 208 | 209 | // loop over blocks 210 | for (int y = 0; y < height; y += 4) 211 | { 212 | for (int x = 0; x < width; x += 4) 213 | { 214 | // decompress the block 215 | int targetRGBA_pos = 0; 216 | if (data.Length == sourceBlock_pos) continue; 217 | Decompress(targetRGBA, data, sourceBlock_pos, flags); 218 | 219 | // Write the decompressed pixels to the correct image locations 220 | for (int py = 0; py < 4; py++) 221 | { 222 | for (int px = 0; px < 4; px++) 223 | { 224 | int sx = x + px; 225 | int sy = y + py; 226 | if (sx < width && sy < height) 227 | { 228 | int targetPixel = 4 * (width * sy + sx); 229 | 230 | rgba[targetPixel + 0] = targetRGBA[targetRGBA_pos + 0]; 231 | rgba[targetPixel + 1] = targetRGBA[targetRGBA_pos + 1]; 232 | rgba[targetPixel + 2] = targetRGBA[targetRGBA_pos + 2]; 233 | rgba[targetPixel + 3] = targetRGBA[targetRGBA_pos + 3]; 234 | 235 | targetRGBA_pos += 4; 236 | } 237 | else 238 | { 239 | // Ignore that pixel 240 | targetRGBA_pos += 4; 241 | } 242 | } 243 | } 244 | sourceBlock_pos += bytesPerBlock; 245 | } 246 | } 247 | return rgba; 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /WorldMapCompiler/CASCDBCProvider.cs: -------------------------------------------------------------------------------- 1 | using CASCLib; 2 | using DBCD.Providers; 3 | using System; 4 | using System.IO; 5 | 6 | namespace WorldMapCompiler 7 | { 8 | class CASCDBCProvider : IDBCProvider 9 | { 10 | private static CASCHandler CASC; 11 | 12 | public CASCDBCProvider(CASCHandler casc) 13 | { 14 | CASC = casc; 15 | } 16 | 17 | public Stream StreamForTableName(string tableName, string build) 18 | { 19 | uint fileDataID = 0; 20 | 21 | switch (tableName) 22 | { 23 | case "WorldMapOverlay": 24 | fileDataID = 1134579; 25 | break; 26 | case "WorldMapOverlayTile": 27 | fileDataID = 1957212; 28 | break; 29 | case "UiMapArt": 30 | fileDataID = 1957202; 31 | break; 32 | case "UiMap": 33 | fileDataID = 1957206; 34 | break; 35 | case "UiMapArtStyleLayer": 36 | fileDataID = 1957208; 37 | break; 38 | case "UiMapArtTile": 39 | fileDataID = 1957210; 40 | break; 41 | case "UiMapXMapArt": 42 | fileDataID = 1957217; 43 | break; 44 | default: 45 | throw new Exception("Unable to find FileDataID for DBC " + tableName); 46 | } 47 | 48 | if (CASC.FileExists((int)fileDataID)) 49 | { 50 | return CASC.OpenFile((int)fileDataID); 51 | } 52 | else 53 | { 54 | throw new FileNotFoundException("Could not find " + fileDataID); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /WorldMapCompiler/Program.cs: -------------------------------------------------------------------------------- 1 | using CASCLib; 2 | using Microsoft.Extensions.Configuration; 3 | using SereniaBLPLib; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Drawing; 7 | using System.IO; 8 | using System.Linq; 9 | 10 | namespace WorldMapCompiler 11 | { 12 | internal class Program 13 | { 14 | private static void Main(string[] args) 15 | { 16 | var config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("settings.json", true, true).Build(); 17 | 18 | var saveExplored = bool.Parse(config["saveExploredMaps"]); 19 | var saveUnexplored = bool.Parse(config["saveUnexploredMaps"]); 20 | var saveLayers = bool.Parse(config["saveMapLayers"]); 21 | var saveExploredMapsWithoutUnexplored = bool.Parse(config["saveExploredMapsWithoutUnexplored"]); 22 | 23 | if (saveExplored && !Directory.Exists("explored")) 24 | { 25 | Directory.CreateDirectory("explored"); 26 | } 27 | 28 | if (saveUnexplored && !Directory.Exists("unexplored")) 29 | { 30 | Directory.CreateDirectory("unexplored"); 31 | } 32 | 33 | if (saveLayers && !Directory.Exists("layers")) 34 | { 35 | Directory.CreateDirectory("layers"); 36 | } 37 | 38 | if (saveExploredMapsWithoutUnexplored && !Directory.Exists("exploredNoUnexplored")) 39 | { 40 | Directory.CreateDirectory("exploredNoUnexplored"); 41 | } 42 | 43 | var locale = LocaleFlags.enUS; 44 | 45 | if (config["locale"] != string.Empty) 46 | { 47 | switch (config["locale"]) 48 | { 49 | case "deDE": 50 | locale = LocaleFlags.deDE; 51 | break; 52 | case "enUS": 53 | locale = LocaleFlags.enUS; 54 | break; 55 | case "ruRU": 56 | locale = LocaleFlags.ruRU; 57 | break; 58 | case "zhCN": 59 | locale = LocaleFlags.zhCN; 60 | break; 61 | case "zhTW": 62 | locale = LocaleFlags.zhTW; 63 | break; 64 | case "frFR": 65 | locale = LocaleFlags.frFR; 66 | break; 67 | } 68 | } 69 | 70 | CASCHandler cascHandler; 71 | if (config["installDir"] != string.Empty && Directory.Exists(config["installDir"])) 72 | { 73 | cascHandler = CASCHandler.OpenLocalStorage(config["installDir"], config["program"]); 74 | } 75 | else 76 | { 77 | cascHandler = CASCHandler.OpenOnlineStorage(config["program"]); 78 | } 79 | 80 | cascHandler.Root.SetFlags(locale); 81 | 82 | var dbcd = new DBCD.DBCD(new CASCDBCProvider(cascHandler), new DBCD.Providers.GithubDBDProvider()); 83 | 84 | string build; 85 | 86 | if (cascHandler.Config.BuildName.StartsWith("WOW-")) 87 | { 88 | var buildNumber = cascHandler.Config.BuildName.Split("patch")[0].Replace("WOW-", ""); 89 | var buildName = cascHandler.Config.BuildName.Split("patch")[1].Split('_')[0]; 90 | build = buildName + "." + buildNumber; 91 | } 92 | else 93 | { 94 | build = cascHandler.Config.BuildName; 95 | } 96 | 97 | var UIMap = dbcd.Load("UiMap", build); 98 | var UIMapXArt = dbcd.Load("UiMapXMapArt", build); 99 | var UIMapArtTile = dbcd.Load("UiMapArtTile", build); 100 | var UIMapArt = dbcd.Load("UiMapArt", build); 101 | var UIMapArtStyleLayer = dbcd.Load("UiMapArtStyleLayer", build); 102 | var WorldMapOverlay = dbcd.Load("WorldMapOverlay", build); 103 | var WorldMapOverlayTile = dbcd.Load("WorldMapOverlayTile", build); 104 | 105 | Console.WriteLine(); // new line after wdc2 debug output 106 | 107 | foreach (dynamic mapRow in UIMap.Values.Reverse()) 108 | { 109 | var mapName = mapRow.Name_lang; 110 | 111 | Console.WriteLine(mapRow.ID + " = " + mapName); 112 | 113 | foreach (dynamic mxaRow in UIMapXArt.Values) 114 | { 115 | var uiMapArtID = mxaRow.UiMapArtID; 116 | var uiMapID = mxaRow.UiMapID; 117 | 118 | if (mxaRow.PhaseID != 0) 119 | continue; // Skip phase stuff for now 120 | 121 | if (uiMapID == mapRow.ID) 122 | { 123 | var maxRows = uint.MinValue; 124 | var maxCols = uint.MinValue; 125 | var tileDict = new Dictionary(); 126 | 127 | foreach (dynamic matRow in UIMapArtTile.Values) 128 | { 129 | var matUiMapArtID = matRow.UiMapArtID; 130 | if (matUiMapArtID == uiMapArtID) 131 | { 132 | var fdid = matRow.FileDataID; 133 | var rowIndex = matRow.RowIndex; 134 | var colIndex = matRow.ColIndex; 135 | var layerIndex = matRow.LayerIndex; 136 | 137 | // Skip other layers for now 138 | if (layerIndex != 0) 139 | { 140 | continue; 141 | } 142 | 143 | if (rowIndex > maxRows) 144 | { 145 | maxRows = rowIndex; 146 | } 147 | 148 | if (colIndex > maxCols) 149 | { 150 | maxCols = colIndex; 151 | } 152 | 153 | tileDict.Add(rowIndex + "," + colIndex, fdid); 154 | } 155 | } 156 | 157 | uint res_x = 0; 158 | uint res_y = 0; 159 | 160 | foreach (dynamic maRow in UIMapArt.Values) 161 | { 162 | if (maRow.ID == uiMapArtID) 163 | { 164 | foreach (dynamic mastRow in UIMapArtStyleLayer.Values) 165 | { 166 | if (mastRow.ID == maRow.UiMapArtStyleID) 167 | { 168 | res_x = mastRow.LayerHeight; 169 | res_y = mastRow.LayerWidth; 170 | continue; 171 | } 172 | } 173 | continue; 174 | } 175 | } 176 | 177 | if (res_x == 0) 178 | { 179 | res_x = (maxRows + 1) * 256; 180 | } 181 | 182 | if (res_y == 0) 183 | { 184 | res_y = (maxCols + 1) * 256; 185 | } 186 | 187 | var bmp = new Bitmap((int)res_y, (int)res_x); 188 | var g = Graphics.FromImage(bmp); 189 | 190 | var bmp2 = new Bitmap((int)res_y, (int)res_x); 191 | var g2 = Graphics.FromImage(bmp2); 192 | 193 | for (var cur_x = 0; cur_x < maxRows + 1; cur_x++) 194 | { 195 | for (var cur_y = 0; cur_y < maxCols + 1; cur_y++) 196 | { 197 | var fdid = tileDict[cur_x + "," + cur_y]; 198 | 199 | if (cascHandler.FileExists(fdid)) 200 | { 201 | try 202 | { 203 | using (var stream = cascHandler.OpenFile(fdid)) 204 | { 205 | 206 | var blp = new BlpFile(stream); 207 | g.DrawImage(blp.GetBitmap(0), cur_y * 256, cur_x * 256, new Rectangle(0, 0, 256, 256), GraphicsUnit.Pixel); 208 | 209 | } 210 | } 211 | catch (Exception e) 212 | { 213 | Console.WriteLine("An error occured opening BLP with filedataid " + fdid + ": " + e.Message); 214 | } 215 | } 216 | } 217 | } 218 | 219 | if (saveUnexplored) 220 | { 221 | bmp.Save("unexplored/ " + CleanFileName(mapRow.ID + " - " + mapName + ".png")); 222 | } 223 | 224 | if (!saveLayers && !saveExplored) 225 | { 226 | continue; 227 | } 228 | 229 | foreach (dynamic wmorow in WorldMapOverlay.Values) 230 | { 231 | var WMOUIMapArtID = wmorow.UiMapArtID; 232 | var offsetX = wmorow.OffsetX; 233 | var offsetY = wmorow.OffsetY; 234 | 235 | uint maxWMORows = 0; 236 | uint maxWMOCols = 0; 237 | var wmoTileDict = new Dictionary(); 238 | 239 | if (WMOUIMapArtID == uiMapArtID) 240 | { 241 | foreach (dynamic wmotrow in WorldMapOverlayTile.Values) 242 | { 243 | var worldMapOverlayID = wmotrow.WorldMapOverlayID; 244 | 245 | // something wrong in/around this check 246 | if (worldMapOverlayID == wmorow.ID) 247 | { 248 | var fdid = wmotrow.FileDataID; 249 | var rowIndex = wmotrow.RowIndex; 250 | var colIndex = wmotrow.ColIndex; 251 | var layerIndex = wmotrow.LayerIndex; 252 | 253 | // Skip other layers for now 254 | if (layerIndex != 0) 255 | { 256 | continue; 257 | } 258 | 259 | if (rowIndex > maxWMORows) 260 | { 261 | maxWMORows = rowIndex; 262 | } 263 | 264 | if (colIndex > maxWMOCols) 265 | { 266 | maxWMOCols = colIndex; 267 | } 268 | 269 | wmoTileDict.Add(rowIndex + "," + colIndex, fdid); 270 | } 271 | } 272 | } 273 | 274 | if (wmoTileDict.Count == 0) 275 | { 276 | continue; 277 | } 278 | 279 | var layerResX = (maxWMORows + 1) * 256; 280 | var layerResY = (maxWMOCols + 1) * 256; 281 | 282 | var layerBitmap = new Bitmap((int)layerResY, (int)layerResX); 283 | var layerGraphics = Graphics.FromImage(layerBitmap); 284 | 285 | for (var cur_x = 0; cur_x < maxWMORows + 1; cur_x++) 286 | { 287 | for (var cur_y = 0; cur_y < maxWMOCols + 1; cur_y++) 288 | { 289 | var fdid = wmoTileDict[cur_x + "," + cur_y]; 290 | 291 | if (cascHandler.FileExists(fdid)) 292 | { 293 | try 294 | { 295 | using (var stream = cascHandler.OpenFile(fdid)) 296 | { 297 | var blp = new BlpFile(stream); 298 | var posY = cur_y * 256 + offsetX; 299 | var posX = cur_x * 256 + offsetY; 300 | 301 | if (saveLayers) 302 | { 303 | layerGraphics.DrawImage(blp.GetBitmap(0), cur_y * 256, cur_x * 256, new Rectangle(0, 0, 256, 256), GraphicsUnit.Pixel); 304 | } 305 | 306 | var blpBMP = blp.GetBitmap(0); 307 | g.DrawImage(blpBMP, posY, posX, new Rectangle(0, 0, 256, 256), GraphicsUnit.Pixel); 308 | 309 | if (saveExploredMapsWithoutUnexplored) 310 | { 311 | g2.DrawImage(blpBMP, posY, posX, new Rectangle(0, 0, 256, 256), GraphicsUnit.Pixel); 312 | } 313 | } 314 | 315 | } 316 | catch (Exception e) 317 | { 318 | Console.WriteLine("An error occured opening BLP with filedataid " + fdid); 319 | } 320 | } 321 | } 322 | } 323 | 324 | if (saveLayers) 325 | { 326 | if (!Directory.Exists("layers/" + CleanFileName(mapRow 327 | + " - " + mapName) + "/")) 328 | { 329 | Directory.CreateDirectory("layers/" + CleanFileName(mapRow.ID + " - " + mapName) + "/"); 330 | } 331 | layerBitmap.Save("layers/" + CleanFileName(mapRow.ID + " - " + mapName) + "/" + wmorow.ID + ".png"); 332 | } 333 | } 334 | 335 | if (saveExplored) 336 | { 337 | bmp.Save("explored/" + CleanFileName(mapRow.ID + ".png")); 338 | } 339 | 340 | if (saveExploredMapsWithoutUnexplored) 341 | { 342 | bmp2.Save("exploredNoUnexplored/ " + CleanFileName(mapRow.ID + " - " + mapName + ".png")); 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | private static string CleanFileName(string fileName) 350 | { 351 | return Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /WorldMapCompiler/WorldMapCompiler.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | 0.5.1 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Always 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /WorldMapCompiler/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "installDir": "C:/World of Warcraft/", // Set this to your WoW installation directory, leave empty if you just want to use web 3 | "program": "wow_beta", // Set this to wow for Retail, wowt for PTR or wow_beta for Alpha/Beta 4 | "locale": "enUS", // Set this to deDE, enUS, ruRU, zhCN or zhTW for what language names/map images should be 5 | "saveExploredMaps": true, // Whether or not to save explored versions of maps 6 | "saveExploredMapsWithoutUnexplored": false, // Whether or not to save explored maps without unexplored background 7 | "saveUnexploredMaps": false, // Whether or not to save un-explored versions of maps 8 | "saveMapLayers": false // Whether or not to save the individual exploration layers separately 9 | } 10 | --------------------------------------------------------------------------------