Changes

Jump to navigation Jump to search

FF7/LGP format

4,466 bytes added, 02:29, 14 December 2025
m
Introduction
=== LGP Archive format for PC by [[User:Ficedula|Ficedula]] =Introduction ==
This section explains how LGP is an archive format used by Final Fantasy VII to store game assets. The format is used in the LGP archives from FF7 PC are constructed. If you're looking for a tool that already manages LGP archivesversion of the game to package various game resources including textures, models, scripts, try [[User:Ficedula|Ficedula]]'s [http://sylphds.net/f2k3/index.html LGP Editor]and other data files.
Essentially the An LGP file is split up into four archive consists of a header, table of contents (maybe lessTOC), hash lookup table, optional path table for directories, depending on how you count it) sectionsand the actual file data. All multi-byte integers in the format are stored in '''little-endian''' format.
# File header/Table The archive terminates with the magic string "FINAL FANTASY7" to mark the end of contents# CRC code# Actual data# File terminatorthe file.
==== Section 1: File Header ==Structure Overview ==
This contains two partsAn LGP archive is divided into six sections in the following order: A header of fixed size, then the table of contents.
The first item is 12 {| class="wikitable"! Section !! Size !! Description|-| Header || 16 bytes || Archive metadata and file count|-| Table of Contents || 27 bytes × file count || File entries with names and offsets|-| Hash Table || 3600 bytes containing the (fixed) || Lookup table for fast file creator. This is a standard string, except it is "rightaligned". In other words the blank space comes before the actual text, not after. In FF7 it's always "SQUARESOFT" preceded by two nulls to make it 12 access|-| Path Table || Variable || Optional directory paths for files|-| File Data || Variable || Actual file contents with individual headers|-| Footer || 14 bytes. The only other thing you might see is the header || "FICEDULA-LGPFINAL FANTASY7", which I use to indicate a file is an LGP *patch* one of my programs has constructed, not a complete archive.terminator string|}
Next is a four-byte integer saying how many files the archive contains.== Header ==
Following this The header is always 16 bytes and contains the table of contents (TOC): One entry per archive's magic identifier and filecount.
Each entry {| class="wikitable"! Offset !! Size !! Type !! Description|-| 0x00 || 2 bytes || uint16 || Reserved (always 0)|-| 0x02 || 10 bytes || char[10] || Magic string: "SQUARESOFT" (ASCII, no null terminator)|-| 0x0C || 2 bytes || uint16 || Number of files in archive|-| 0x0E || 2 bytes || uint16 || Reserved (always 0)|} '''Validation:''' The magic string at offset 0x02 must exactly match "SQUARESOFT" (ASCII, no null terminator within the 10 bytes). == Table of Contents (TOC has ) == Immediately follows the following structure:header and contains one 27-byte entry for each file in the archive.
{| class="wikitable"
! Offset! Length! Size !! Type !! Description
|-
| 0x00 || 20 bytes| Null terminated string| char[20] || Filename (null-padded, giving filenameno path)
|-
| 0x14 || 4 bytes || uint32 || Absolute file offset in archive|-| 0x18 || 1 byte integer|| uint8 || File type (always 0x0E / 14)|-| Position in 0x19 || 2 bytes || uint16 || Path index (0 = no path, 1+ = path table index)|} '''Notes:'''* Filenames are stored without directory paths* Filenames are null-terminated within the 20-byte field* The offset field points to the start of the File Header for this file where data starts for * The path field references the Path Table (1-indexed); 0 means the fileis in the root directory* Maximum filename length is 19 characters plus null terminator == Hash Table == The hash table immediately follows the TOC and is always exactly 3600 bytes (900 entries × 4 bytes). It provides fast O(1) lookup of files by filename. === Hash Table Entry === Each entry is 4 bytes: {| class="wikitable"! Offset !! Size !! Type !! Description
|-
| 1 byte0x00 || 2 bytes || uint16 || style="background: rgbIndex into TOC (2551-indexed,255,2040 = empty bucket)" | Some sort of check code. File attributes? Normally seems to be<br />14 but it does vary.
|-
| 0x02 || 2 byte shortbytes || uint16 | style="background: rgb(255,255,204)" | Something to do with duplicate file names. If a name is unique it is 0, otherwise it is assigned a value based on existing duplicates. (Hard to explain)Count of consecutive entries in this bucket
|}
==== Section 2: CRC Code Hash Function === The hash is computed from the filename (without path or extension) using only the first two characters of the file stem: <pre>hash_value =hash(first_char) × 30 + hash(second_char) + 1</pre>
This code is used to validate the LGP archive. The bad news is I have no idea how to make it (I've figured out how to decode it, ie. find out whether the archive is valid, but I can't create my own). The good news is you don't need to! The only thing this CRC is based on is the number of files in the archive (maybe the For filenames too, haven't checked that). Anyway, the TOC is the with only thing this check relates to. So if you're replicating an archive from FF7 for use one character in the game with the same number of files and filenames you can just copy the CRC section from an existing file.stem:
Normally it's 3602 bytes long <pre>hash_value = hash(one archive may be different, possibly MAGIC.LGPfirst_char). Anyway, one normally-safe way of calculating the CRC size is to find the end of the TOC and the beginning of the first file. Anything in between is probably CRC code (this is not guaranteed to work. It works with "official" archives but editors - such as [http://www.ficedula.com× 30</ LGP Editor] - can alter the TOC to achieve extra things).pre>
==== Section 3: Actual Data =Character Hash Values ===
The data from the files. However it's not that simple: the TOC doesn't list how long each file is (somewhat useful). It's done here. The offset in the TOC is actually the position of yet another file header. Format ishash function maps characters to numeric values as follows:
{| class="wikitable"
! style="background: rgb(204,204,204); width: 80px" align="center" Character !! Hash Value| Size-! style="background: rgb| a-z (204,204,204case insensitive); width: 200px" | Description| 0-25
|-
| 20 bytes0-9 | Null terminated string, giving filename| 0-9
|-
| 4 bytes_ (underscore) | File length| 10 (same as 'k')
|-
| Varies- (hyphen) | The file data itself| 11 (same as 'l')
|}
'''Note:''' The hash function is case-insensitive, treating 'A' and 'a' identically. ===Lookup Algorithm === Section 4 # Compute hash from the filename (first two characters of stem)# Read HashTable[hash]# If index is 0, file not found (empty bucket)# Search TOC entries starting at index-1 (converting to 0-indexed) for count entries# Match by exact filename comparison '''Example:''' To find "test.dat":# Compute hash: Terminator hash('t') × 30 + hash('e') + 1 =19 × 30 + 4 + 1 =575# Read HashTable[575]: {index: 42, count: 3}# Search TOC entries 41-43 (0-indexed: index-1)# Find entry where name =="test.dat"
After == Path Table == The path table immediately follows the last piece hash table and stores directory paths for files. It uses a variable-length structure organized into "path groups" for files that share the same filename but exist in different directories. === Path Table Header === {| class="wikitable"! Offset !! Size !! Type !! Description|-| 0x00 || 2 bytes || uint16 || Number of data comes path groups|} === Path Group === Each path group contains all directory paths for files sharing the file descriptor. This same filename: {| class="wikitable"! Offset !! Size !! Type !! Description|-| 0x00 || 2 bytes || uint16 || Number of paths in this group|-| 0x02 || Variable || Path[] || Array of Path entries|} === Path Entry === Each path entry is 130 bytes: {| class="wikitable"! Offset !! Size !! Type !! Description|-| 0x00 || 128 bytes || char[128] || Directory path (null-padded, no trailing slash)|-| 0x80 || 2 bytes || uint16 || TOC index this path belongs to|} '''Notes:'''* Path index in TOC entries is 1-indexed into path groups* Multiple files with the same name but different paths share a simple path group* Empty path string, except instead means root directory* Maximum path length is 127 characters plus null terminator == File Data == File data blocks follow the path table. Each file consists of being nulla 24-terminated it's terminated byte header followed by the end of raw file content. === File Header === {| class="wikitable"! Offset !! Size !! Type !! Description|-| 0x00 || 20 bytes || char[20] || Filename (same as in TOC)|-| 0x14 || 4 bytes || uint32 || File size in bytes|} === File Content === Immediately follows the fileheader. ItThe size is specified in the header's file size field. {| class="wikitable"! Offset !! Size !! Type !! Description|-| 0x00 || file_size || byte[] || Raw file data|} == Footer == The archive ends with a 14-byte terminator string: {| class="wikitable"! Offset !! Size !! Type !! Description|-| 0x00 || 14 bytes || char[14] || "FINAL FANTASY 7FANTASY7" (no null terminator)|} == Reading Algorithm == To read an LGP archive: # '''Read Header''' - Validate magic string "SQUARESOFT" and extract file count# '''Read TOC''' - Read file_count entries of 27 bytes each, storing filename, offset, type, and path index# '''Skip Hash Table''' - Skip 3600 bytes (or read for all archivesvalidation/fast lookup)# '''Read Path Table''' - Read path group count, then for each group read path count and path entries# '''Resolve Full Paths''' - For each TOC entry with path index > 0, look up path group at path_index-1, find path entry matching TOC index, and prepend path to filename# '''Read File Data''' (on demand) - Seek to TOC entry's offset, read 24-byte file header, except read file_size bytes of content == Writing Algorithm == To create an LGP patchesarchive: # '''Build Metadata''' - Compute hash for each file, group files by hash bucket, identify files needing path entries# '''Write Header''' - Write magic string, file count, where itand reserved fields# '''s Write TOC''' - Write entries with placeholder offsets (will be updated later)# '''Write Hash Table''' - Populate 900 entries based on file hash groupings# '''Write Path Table''' - Group paths by filename collisions and write path groups# '''Write File Data''' - For each file write header and content, recording actual offset# '''Update TOC''' - Seek back to TOC section and update offsets with actual values# '''Write Footer''' - Append "FINAL FANTASY7"terminator == Size Limits == The LGP PATCH FILEformat has the following technical limitations: {| class="wikitable".! Limit !! Maximum Value !! Reason|-| Files per archive || 65,535 || uint16 in header|-| File size || 4 GB || uint32 in file header|-| Archive size || 4 GB || uint32 offsets in TOC|-| Filename length || 19 characters || 20 bytes with null terminator|-| Path length || 127 characters || 128 bytes with null terminator|-| Hash buckets || 900 || Fixed 30 × 30 grid (letters × letters)|}
==== Notes ====
The game is remarkably flexible about LGP archives. So long as the TOC and the CRC data is intact it'll accept just about anything.
109
edits

Navigation menu