Skip to content
View in the app

A better way to browse. Learn more.

ResHax

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
Help us keep the site running.
Zero Tolerance for Disrespect

Heroes of Malgrimia .tex unpack

Featured Replies

  • Localization

Hello everyone,

I’m looking for some help extracting the textures from this .tex file from the game mentioned in the title. I’ve tried several different methods, but unfortunately I haven’t been able to figure out the file format or successfully unpack it.

I’ve attached the .tex file in question, along with an .md file containing an analysis of the file structure, in case anyone would like to take a look and see where the problem might be preventing the file from being extracted/unpacked.

Any help or suggestions would be greatly appreciated. Thank you very much in advance for taking the time to look into this!

https://www.mediafire.com/file/u82e38919q2kmzd/tex.rar/file

heroes_of_malgrimia_research.doc

Solved by Hazza12555

  • Localization
  • Solution

All integers are little-endian.

1. File header

| offset | type | value |

| 0x00 | char[8] | "TEXTURES" |

| 0x08 | u32 | version — 0x00001200 |

| 0x0C | u32 | texture count — 26805 |

| 0x10 | u32 | 0 |

| 0x14 | u32 | 0 |

| 0x18 | u32 | name-table size in bytes — 546198 |

2. Name table (at 0x1C)

count consecutive Pascal-style strings:

u8  len          ; includes the trailing NUL
u8  bytes[len]   ; CP1251 text, last byte is 0x00

Names are Russian (CP1251), e.g. Городишко_Мерия_1_0. The table ends exactly at 0x855B2, where the record table begins.

3. Record table (0x855B2, count × 136 bytes)

A record is a u32 storage type followed by a verbatim MicrosoftDDSURFACEDESC2 / DDS_HEADER (dwSize = 124), then the block location.

Because the DDS header starts at +0x04, the usual DDS offsets are shifted by four:

| offset | field |

| 0x00 | u32 type0 = raw, 2 = compressed |

| 0x04 | dwSize = 124 |

| 0x08 | dwFlags = 0 |

| 0x0C | dwHeight |

| 0x10 | dwWidth |

| 0x14 | dwPitchOrLinearSizeunreliable, ignore |

| 0x18 | dwDepth = 0 |

| 0x1C | dwMipMapCount = 0 |

| 0x20 | dwReserved1[11] — all zero |

| 0x4C | DDPIXELFORMAT: dwSize = 32, dwFlags = 0x41 (32bpp) or 0x40 (24bpp), dwFourCC = 0, dwRGBBitCount = 32 or 24, masks R=0x00FF0000 G=0x0000FF00 B=0x000000FF A=0xFF000000 |

| 0x6C | dwCapsCaps2Caps3Caps4dwReserved2 — all zero |

| 0x80 | u32 dataSize |

| 0x84 | u32 dataOffset — absolute file offset |

The channel masks describe a 32-bit ARGB DWORD, i.e. B, G, R, A byte order in memory.
592 records store pitch = width × 3 even though the pixel format is 32bpp - a stale field left over from a 24-bit build.
Derive the stride from width instead. The 12 genuinely 24bpp records are all type 0 and are tiny UI fills.
Data blocks are perfectly contiguous; the last one ends exactly at EOF.

4. Payload — type 0 (18 records, raw)

width × height × bpp bytes, row-major, no padding: BGRA for 32bpp, BGRfor 24bpp. No colour transform.

5. Payload — type 2 (26,787 records, compressed)

u16 width
u16 height
u8  bytesPerPixel        ; always 4
4 ×
{
    u24 compressedSize   ; little-endian
    u8  compressedData[compressedSize]
}

Each of the four chunks is one independently compressed 8-bit plane that decompresses to exactly width × height bytes.

5.1 Plane codec — NRV2B (UCL)

The planes use NRV2B, the LZ77 variant from Markus Oberhumer's [UCL] (https://www.oberhumer.com/opensource/ucl/) library (the algorithm UPX uses), in its 32-bit-bit-buffer form:

  • a 32-bit bit buffer is refilled with a little-endian u32 taken from the stream whenever it runs dry, and its bits are consumed LSB-first;

  • literal bytes and match-offset bytes are read as raw bytes from the same stream, interleaved with those refills - so the byte layout of a plane is

word · literals · word · … · 0xFF;

  • M2_MAX_OFFSET is 0xd00 (3328): a match longer than that gets one extra byte;

  • the stream ends with the standard 0xFFFFFFFF offset marker, which is why every plane's final byte is 0xFF.

last = 1;
for (;;) {
    while (getbit()) emit(nextbyte());          /* literal run */
    m_off = 1;
    do { m_off = m_off*2 + getbit(); } while (!getbit());
    if (m_off == 2) {                            /* reuse previous offset */
        m_off = last;
        m_len = getbit();
    } else {
        v = (m_off - 3)*256 + nextbyte();
        if (v == 0xffffffff) break;              /* end of stream */
        m_len = getbit();
        m_off = v + 1;
        last  = m_off;
    }
    if      (m_len)    m_len = 3 + getbit();     /* 3..4 */
    else if (getbit()) m_len = 2;                /* 2    */
    else {                                       /* >=5  */
        m_len = 1;
        do { m_len = m_len*2 + getbit(); } while (!getbit());
        m_len += 3;
    }
    if (m_off > 0xd00) m_len++;
    copy m_len bytes from distance m_off;
}

5.2 Colour transform

The four planes are not B, G, R, A directly.
Green is hoisted to plane 0 and the other two colour channels are stored as differences against it, biased
by 128 - a reversible decorrelation that makes the planes far more compressible (planes 1 and 2 of a typical texture sit tightly around 128):

G = p0
B = p0 + p1 - 128
R = p0 + p2 - 128
A = p3

6. Tools in this zip.

| file | what it is |

| extract_tex.py | full extractor → PNG (uses nrv2b.dll, falls back to a pure-python decoder) |

| nrv2b.c | the NRV2B decompressor in C, plus a helper that decodes all four planes of a block |

| nrv2b.dll | prebuilt x64 build of the above (MSVC) |

malgrimia-tex-tools.zip

  • Author
  • Localization

@Hazza12555 man, Hi again! It worked perfectly! I’m truly grateful that you took the time to help me. Thank you so much for your patience, your time, and your expertise! You’re amazing bro!!! 🙌😊

Create an account or sign in to comment

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.