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

Fairyland Online (Lager Network Technologies) - .adf resource format

Featured Replies

  • Localization

Hello everyone,

I’m looking for some help with deciphering/extracting the ADF files from the game Fairyland Online. I’ve attached some files for anyone who would like to investigate them, as well as the game executable in case it is needed for analysis.

The specific file I’m interested in is mouse.adf, as I believe it may contain the game’s mouse cursors.

Any help in figuring out the structure of these files or extracting their contents would be greatly appreciated.

Thank you very much in advance to everyone who takes the time to help!

----

Extract the mouse cursor sprites from the client of "Fairyland Online" (aka

King of Kings 3 engine, developer: Lager Network Technologies). The cursors

live in a loose file: data\mouse.adf

I've fully reverse engineered the sibling container format (.lpq, the big

multi-entry archives) by disassembling Fairyland.exe with Ghidra, but the

single-resource .adf format is resisting the last step (LZO decompression of

the pixel payload). Posting my findings in case someone here has already

documented this engine, or can spot what I'm missing.

Tools used: Ghidra 12.1.3 (headless, scripted in Java), objdump, pefile,

capstone, liblzo2 via Python ctypes, python-lzo.

PART 1 - .lpq FORMAT (SOLVED, for reference / context)

--------------------------------------------------------

Magic: "LPQ\x1a" (dword 0x1A51504C) + dword 0x20 (constant)

Header struct at file offset 0x08, 40 (0x28) bytes:

struct+0x00 (file 0x08): total file size

struct+0x04 (file 0x0c): constant 0x1000000

struct+0x08 (file 0x10): approx. total decompressed size

struct+0x0c (file 0x14): flags (bit 0x02 = directory is XOR-"encrypted")

struct+0x10 (file 0x18): seek_pos -> file offset where the directory table

(ring buffer) starts

struct+0x14 (file 0x1c): number of entries to read in this pass

struct+0x18 (file 0x20): read position within the ring buffer

struct+0x24 (file 0x2c): ring buffer capacity (total slots)

Directory table: `capacity` slots of 28 (0x1c) bytes each, located at

`seek_pos`. If the "encrypted" flag is set, the whole block that gets read

(count*28 bytes) is passed through this XOR-chain cipher (found by

decompiling FUN_0059dcc0 in Fairyland.exe):

prev = 0x55555555 # seed / IV

for each little-endian dword D in the block, in order:

original = D # the still-"encrypted" value

D = original XOR prev

prev = original # chains on the ENCRYPTED value,

# not the decrypted one

Each decrypted directory entry (7 dwords / 28 bytes):

[0] absolute file offset of the compressed resource

[1] compressed size

[2] decompressed size

[3] flag (=1 for valid entries)

[4] small value, purpose unknown (not needed for extraction)

[5],[6] constants copied from the header, ignore

Payload at [0..1]: standard **LZO1X** stream (lzo1x_decompress_safe from

liblzo2 works out of the box, no extra transform needed). Decompresses

directly to a completely standard Windows BMP (BITMAPINFOHEADER etc, nothing

proprietary in the pixel data itself).

There's usually a special entry called "(listfile)" (real filenames,

newline-separated) whose compressed data sits immediately BEFORE seek_pos.

Verified against real files: 207/207 entries extracted correctly from a UI

archive, and 10991/10992 from a much bigger model archive (the 1 failure was

the expected "(patchlist)" metadata entry, not a real asset).

PART 2 - .adf FORMAT (WHERE I'M STUCK)

----------------------------------------

These are small, standalone, single-or-few-resource files (as opposed to the

big multi-thousand-entry .lpq archives). Confirmed samples analyzed:

nui_char_05.adf, mouse.adf, nfui001_00.adf, nsys004.adf, nsys014.adf,

nsys032.adf.

Magic: "FGF300" (6 bytes), then 2 more bytes.

Right after the magic, there's a run of bytes that LOOKS like padding

(0x55 55 55 55 ...) up to file offset 0x46. This is NOT raw padding - I

confirmed it decrypts cleanly to all-zero bytes using the exact same

XOR-chain cipher as the .lpq directory (seed 0x55555555, dword-aligned,

chained on the previous *encrypted* dword, running continuously from file

offset 0). This is a strong indicator the whole file uses the same cipher

primitive as .lpq.

At file offset 0x46 (uint16): a record count (1 for single-image files, 3

for nui_char_05.adf which has 3 sub-images, etc).

Following that: one variable-length record per image. In every sample, the

record contains (readable in RAW/plaintext form, i.e. NOT XOR-encrypted -

decrypting this region actually destroys it):

- a constant 4-byte marker: 4c 0f 1d 70

- a "dimension-ish" field (varies)

- alpha/flag dword: ff 00 00 00

- COMPRESSED SIZE (dword)

- ASCII marker "LrtJ" (4c 72 74 4a)

- DECOMPRESSED SIZE (dword)

- flag dword: 01 00 00 00

Example values (comp_size / decomp_size, both plausible and self-consistent

with file size):

nfui001_00.adf: 3315 / 3084 (single 2407-byte file)

nsys014.adf: 3825 / 3598

nsys032.adf: 7140 / 6939

nsys004.adf: 15045 / 14906 (first of 3 records)

Right after the record, the payload begins with one byte followed

immediately by the ASCII bytes "BM" (Windows bitmap signature) - e.g.

`f9 42 4d ...`. This strongly suggests the leading byte is meant to be

interpreted as the LZO1X literal-run opcode (0xf9 = 249; per LZO1X's

first-command rule, byte-17=232, i.e. "copy the next 232 bytes literally") -

and indeed, "BM" plus a nice chunk of following bytes decode perfectly clean

this way.

THE PROBLEM:

Feeding this straight into lzo1x_decompress_safe (liblzo2), with or without

re-applying the XOR-chain to the payload itself, always fails with

LZO_E_LOOKBEHIND_OVERRUN (-6) shortly after that initial literal run - in

two different files it fails at EXACTLY the same output length (235 bytes),

which smells like the 232-byte literal-run "success" is a coincidence

(copying raw bytes verbatim doesn't validate anything) rather than proof the

stream is genuinely stock LZO1X from that point on.

Byte right after the literal run in nsys014.adf is 0x73, which under

standard LZO1X opcode rules (0x40-0x7F range) should decode as a perfectly

valid short match (length 5, small distance) - yet the safe decompressor

rejects it as looking behind the start of the output buffer. I don't know if

that's because:

a) it's genuinely not LZO1X past the literal run (different variant, or

scrambled/substituted somehow), or

b) there's some other per-chunk transform I'm missing, or

c) my offset/alignment for where the "true" stream starts is still off by

a few bytes despite the clean "BM" landmark.

Static tracing dead end: I found the file-open helper used by the .lpq

loader (a wrapper resembling CreateFileA, at 0x005953f0 in this exe build)

and enumerated ALL its callers - there are exactly two: the confirmed .lpq

loader (FUN_0059e1d0), and one more (FUN_0059de50) which turned out to be a

*generic* container loader that hard-checks for magic "LPQ\x1a" - so it's

not what parses "FGF300" files despite superficially similar structure.

Tracing outward from the "data\\mouse.adf" string literal itself leads into

a huge, generic startup/init function (FUN_004024b0) that fans out into

hundreds of subsystems, impractical to fully trace via static xref-chasing

alone without an interactive decompiler session.

WHAT WOULD HELP

----------------

- Anyone who has already documented Lager Network Technologies' .adf format

(King of Kings 3 uses the same engine/format family, so KoK3 tooling /

threads would likely apply directly).

- A sanity check on whether the payload after the small record header really

is LZO1X, or something else (custom LZ variant / substitution box applied

before or after LZO / etc).

- Anyone comfortable doing live/dynamic analysis (x64dbg or similar) who

could set a breakpoint on the file read for data\\mouse.adf and step

through to the real decompression call - that would settle this in

minutes versus my current blind static guessing.

Happy to share the exe, sample .adf files, my Ghidra scripts, or the Python

XOR-chain implementation on request. Thanks in advance for any pointers.

APPENDIX - XOR-chain reference implementation (Python)

---------------------------------------------------------

def dexor_chain(data: bytes, seed: int = 0x55555555) -> bytes:

import struct

n = len(data) // 4

out = bytearray(data[:n * 4])

prev = seed

for i in range(n):

cur = struct.unpack_from("<I", out, i * 4)[0]

dec = cur ^ prev

struct.pack_into("<I", out, i * 4, dec)

prev = cur # chains on the RAW/encrypted dword

return bytes(out)

(Confirmed correct against the .lpq directory table and against the .adf

zero-padding region; it's the .adf payload's exact transform - if any -

that's still unresolved.)

Fairyland adf files.zip

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.