LGP Format - niemasd/PyFF7 GitHub Wiki
LGP is an archive file format and is used in many file packages in Final Fantasy VII. Files are not compressed when they are packed into an LGP archive.
An LGP archive's File Header gives general information about the LGP archive.
- This is a "right-aligned" string: strings shorter than 12 characters are left-padded with
<NULL>
(i.e.,0x0
) - In FF7, it's always
<NULL><NULL>SQUARESOFT
- In some mods, it's
FICEDULA-LGP
to indicate that the file is an "patch" (i.e., not a complete archive)
- This is a 4-byte integer denoting the number of files in the archive
The Table of Contents stores information about the files stored in the LGP archive. There is one entry for each file in the archive (see Number of Files). Each entry is 27 bytes and contains the following sections:
- This is a "left-aligned" string: strings shorter than 20 characters are right-padded with
<NULL>
(i.e.,0x0
)
- This is a 4-byte integer denoting the position in this LGP archive where the data for this file starts
- It's unclear what this is, but it seems to be some form of a "check code" (e.g. file attribute?)
- It normally seems to be 14, but it varies
- This is a 2-byte short indicating the (1-based) index in the Conflict Table that corresponds to this filename
- If this filename is unique, the Conflict Table Index is 0, and it doesn't show up in the Conflict Table
The Lookup Table is always 3,600 bytes. It is a hash table with 900 entries, where each entry is composed of two 2-byte integers (ToC index followed by file count). Every filename can be hashed to a Lookup Table index using just its first two characters. For the i-th slot of the Lookup Table, the first 2-byte integer denotes the earliest ToC index of all files hashing to slot i, and the second 2-byte integer denotes the total number of files hashing to slot i.
Given a filename name
, the corresponding (0-based) Lookup Table index is computed as follows:
MAX_LOOKUP_HASH = 30
def hash(c):
if c == '.':
return -1
elif c == '-':
return 10 # 'k' - 'a'
elif c == '_':
return 11 # 'l' - 'a'
elif str.isdigit(c): # '0' <= c <= '9'
return ord(c) - ord('0')
elif str.isalpha(c): # 'a' <= c <= 'z' or 'A' <= c <= 'Z'
return ord(c.lower()) - ord('a')
else: # invalid character
raise ValueError
def index(name): # filename does not include folders: just the actual file
hash(name[0]) * MAX_LOOKUP_HASH + hash(filename[1]) + 1
The Conflict Table stores directory information for files that have conflicting filenames but are in different directories. The first 2 bytes are an integer designating the number of conflicts (i.e., the number of entries in the Conflict Table).
Each entry starts with a 2 byte integer designating the number locations this entry corresponds to (i.e., how many files have this exact filename, but in different folders). For each of these locations, we have 128 bytes designating the full path to the corresponding folder as a "left-aligned" string (right-padded with <NULL>
, i.e., 0x0
), followed by a 2 byte integer designating the corresponding 0-based index in the Table of Contents.
There is one entry for each file in the archive (see Number of Files). Note that there can be dummy space between the entries, so it is safer to go directly off the positions listed in the Table of Contents instead of iteratively going byte-by-byte. Each entry is 24+n bytes and contains the following sections:
- This is a "left-aligned" string: strings shorter than 20 characters are right-padded with
<NULL>
(i.e.,0x0
)
- This is a 4-byte integer denoting the length of this file
- This is the actual data of this file, and its length is specified in the previous section of the entry (File Size)
-
NOTE: In some LGP archives, you can have filename conflicts on a case-insensitive filesystem (e.g.
smab
andSMAB
inbattle.lgp
)- In this case, my LGP Info script will work fine regardless, but my LGP Unpacker may not work on your machine
-
NOTE: In some LGP archives, there are extra files that do not appear in the Table of Contents (e.g.
battle.lgp
)- My LGP Unpacker now handles this properly, assuming you don't run into the case-sensitivity issue described above
- However, it seems like this only happens with the case-insensitive filename conflicts (e.g.
smab
andSMAB
)
-
NOTE: The above two issues (case-insensitive filename conflicts and not appearing in the Table of Contents) seem to appear much more frequently than originally thought (definitely not just in
battle.lgp
)
The terminator is the remaining content following the end of the last data entry. It's a string that is terminated by EOF
(as opposed to NULL
). For all official LGP archives, it's FINAL FANTASY7
.
-
File Header
- 12 bytes (File Creator)
- Default:
<NULL><NULL>SQUARESOFT
- Default:
- 4 bytes (Number of Files, nF)
- 12 bytes (File Creator)
-
Table of Contents (nF entries)
-
File 1
- 20 bytes (Filename)
- 4 bytes (Data Start Position, 0-based)
- 1 byte (Check Code?)
- 2 bytes (Conflict Table Index)
- ...
-
File nF
- ...
-
File 1
-
Lookup Table (900 entries)
-
Entry 1
- 2 bytes (Table of Contents Index)
- 2 bytes (File Count)
- ...
-
Entry 900
- ...
-
Entry 1
-
Conflict Table (nC entries)
- 2 bytes (Number of Conflicts, nC)
- Nothing else in Conflict Table if nC = 0
-
Conflict 1
- 2 bytes (Number of Locations, nL)
-
Location 1
- 128 bytes (Full Path to Folder)
- 2 bytes (Table of Contents Index)
- ...
-
Location nL
- ...
- ...
-
Conflict nC
- ...
- 2 bytes (Number of Conflicts, nC)
-
File Data (nF entries)
-
File 1
- 20 bytes (Filename)
- 4 bytes (File Size, s)
- s bytes (File Data)
- ...
-
File nF
- ...
- Potentially extra files not in the Table of Contents
-
File 1
-
File Terminator
- Remaining bytes until
EOF
- Default:
FINAL FANTASY7
- Remaining bytes until