Game: Monster Hunter Freedom Unite (MHP2G) with FUComplete mod
Platform: PPSSPP (PSP emulator)
Game ID: ULJM05500
Cheat file: PPSSPP/PSP/Cheats/ULJM05500.ini
Save states are .ppst files located in PPSSPP/PSP/PPSSPP_STATE/.
Filename pattern: ULJM05500_1.01_{slot}.ppst
| Offset | Size | Content |
|---|---|---|
| 0x00 | 0xB0 | PPSSPP header (skip) |
| 0xB0 | ... | Zstandard-compressed PSP memory snapshot |
import zstandard
raw = open(state_path, 'rb').read()
compressed = raw[0xB0:]
data = zstandard.ZstdDecompressor().decompress(compressed, max_output_size=128*1024*1024)Decompressed size: ~38.7 MB.
- RAM data starts at offset 0x48 in the decompressed data
- PSP RAM starts at address 0x08000000
state_offset = (psp_address - 0x08000000) + 0x48
Or, if you strip the 0x48 header first:
ram_offset = psp_address - 0x08000000
CWCheat codes are written in the cheat .ini file.
_S ULJM-05500
_G Monster Hunter Freedom Unite
_C0 Cheat Name Here # _C0 = disabled, _C1 = enabled
_L 0xTAAAAAAA 0xVVVVVVVV # T = type, A = address offset, V = value
CWCheat addresses use an offset from base 0x08800000:
cwcheat_offset = psp_address - 0x08800000
| Type | Code prefix | Size | Value format |
|---|---|---|---|
| 0 | _L 0x0 |
8-bit (1 byte) | 0x000000VV |
| 1 | _L 0x1 |
16-bit (2 bytes) | 0x0000VVVV |
| 2 | _L 0x2 |
32-bit (4 bytes) | 0xVVVVVVVV |
To write 32-bit value 0x002F002F at PSP address 0x08965AE8:
offset = 0x08965AE8 - 0x08800000 = 0x00165AE8
_L 0x20165AE8 0x002F002F
^ ^
type=2 value
- CWCheat type 5 (copy bytes) does NOT work on PPSSPP
- CWCheat type 8 (multi-write) does NOT work on PPSSPP
- Only types 0, 1, 2 (byte/halfword/word writes) are reliable
Armor data is stored in 5 static tables in PSP memory, one per equipment slot. Each table is an array of fixed-size entries indexed by equip_id (equipment ID).
A pointer table at 0x08975970 stores the base address of each armor table, indexed by type2:
| type2 | Slot | Table base address |
|---|---|---|
| 0 | LEGS | 0x08970D30 |
| 1 | HEAD | 0x08960750 |
| 2 | CHEST | 0x08964B70 |
| 3 | ARMS | 0x08968D10 |
| 4 | WAIST | 0x0896CD48 |
| 5 | (flags, not a pointer) | 0x02030007 |
| 6 | (flags, not a pointer) | 0x04030002 |
Important: LEGS is at index 0 (not 5). Indices 5 and 6 contain flag values, not valid pointers.
| Offset | Type | Field | Description |
|---|---|---|---|
| +0 | s16 | modelIdMale | Male character 3D model ID |
| +2 | s16 | modelIdFemale | Female character 3D model ID |
| +4 | u8 | flag | Variant flag (see below) |
| +5 | u8 | rarity | Item rarity (0-9) |
| +6 | ... | (padding/unknown) | |
| +8 | u32 | sellPrice | Sell price in zenny |
| +12 | u8 | defense | Base defense |
| +13 | u8 | fireRes | Fire resistance |
| +14 | u8 | waterRes | Water resistance |
| +15 | u8 | thunderRes | Thunder resistance |
| +16 | u8 | dragonRes | Dragon resistance |
| +17 | u8 | iceRes | Ice resistance |
| +18 | u8 | slots | Decoration slots (0-3) |
| +19 | u8 | pigmentFlag | Pigment support: 0=none, 1/2/3=enabled (tier) |
| +20-29 | ... | (unknown) | |
| +30 | u8 | skill1Id | First skill ID |
| +31 | u8 | skill1Pts | First skill points |
| +32-39 | ... | (more skills) |
| Flag | Meaning |
|---|---|
| 0x07 | Blademaster (both male and female models present) |
| 0x0B | Gunner (both male and female models present) |
| 0x0F | Gender-neutral / universal (used for HEAD pieces like Chakra) |
entry_address = table_base + equip_id * 40
Armor models are stored as .pac files with slot-specific prefixes:
| Slot | Male prefix | Female prefix | Example (model 96) |
|---|---|---|---|
| HEAD | m_hair | f_hair | m_hair096.pac, f_hair096.pac |
| CHEST | m_body | f_body | m_body092.pac, f_body092.pac |
| ARMS | m_arm | f_arm | m_arm089.pac, f_arm089.pac |
| WAIST | m_wst | f_wst | m_wst076.pac, f_wst076.pac |
| LEGS | m_reg | f_reg | m_reg067.pac, f_reg067.pac |
Special values:
- Model
(0, 0)= "Nothing Equipped" → loadsm_hair000.pac/f_hair000.pac→ shows invisible armor with hair visible (useful for HEAD slot) - Model
(-1, -1)= hides everything including hair (not recommended for HEAD)
Armor entries come in consecutive pairs:
- Even equip_id (or first in pair) = Blademaster variant
- Odd equip_id (or second in pair) = Gunner variant
Both must be patched to cover all equipment variants.
The armor data tables are identical across different save states. They are loaded from the game's ROM/EBOOT data and never modified at runtime. This means CWCheat constant-write codes work reliably.
All weapons (across all weapon types) share a single data table in PSP memory.
| Property | Value |
|---|---|
| Base address | 0x089574E8 |
| Entry size | 24 bytes |
| Total entries | ~1,149 |
| Total size | ~27,576 bytes (0x6BB8) |
| End address | ~0x0895E0A0 |
| Offset | Type | Field | Description |
|---|---|---|---|
| +0 | u8 | byte0 | Unknown (possibly weapon category/flags, values 0-7) |
| +1 | u8 | byte1 | Unknown (possibly upgrade tier, values 0-5) |
| +2 | u16 | attack | Base attack value (true raw before multiplier) |
| +4 | u16 | field4 | Unknown (possibly affinity or element modifier) |
| +6 | u16 | field6 | Unknown (possibly element type + value) |
| +8 | u16 | field8 | Unknown |
| +10 | u16 | field10 | Unknown (possibly decoration slots) |
| +12 | u16 | field12 | Unknown (possibly sharpness index) |
| +14 | u16 | field14 | Unknown (usually 0) |
| +16 | u16 | modelId | Weapon model ID (weXXX.pac) |
| +18 | u16 | rarity | Item rarity (0-9) |
| +20 | u32 | price | Buy price in zenny |
entry_address = 0x089574E8 + entry_index * 24
model_id_address = entry_address + 16
The same weapon model ID appears in multiple entries representing different upgrade tiers of the same weapon. All entries sharing a model ID use the same 3D model visually.
Example — Sieglinde (model 21):
| Entry index | Address | Attack | Rarity | Price |
|---|---|---|---|---|
| 33 | 0x08957800 | 110 | 2 | 6,200z |
| 34 | 0x08957818 | 130 | 4 | 16,000z |
| 701 | 0x0895B6A0 | 270 | 9 | 200,000z |
Example — Black Fatalis Blade (model 242):
| Entry index | Address | Attack | Rarity | Price |
|---|---|---|---|---|
| 376 | 0x08959828 | 130 | 4 | 30,000z |
| 419 | 0x08959C30 | 170 | 7 | 200,000z |
| 651 | 0x0895B1F0 | 210 | 9 | 300,000z |
Weapon models are stored as weXXX.pac files:
we000.pac, we001.pac, ..., we021.pac (Sieglinde), ..., we242.pac (BFB), ...
File ID formula (for reference): file_id = 3388 + model_number
- Sieglinde: 3388 + 21 = 3409
- BFB: 3388 + 242 = 3630
Like armor tables, the weapon data table is static across save states.
Change the visual appearance of armor piece A to look like armor piece B, without affecting stats.
Overwrite the first 4 bytes (modelIdMale + modelIdFemale) of the source armor's entry with the target armor's model IDs.
- Identify the source armor's equip_id and slot (e.g., Rath Soul Helm S = HEAD eid 101)
- Identify the target armor's model IDs (e.g., Black Head model_m=50, model_f=50)
- Calculate the source entry's PSP address:
entry_addr = table_base[slot] + equip_id * 40 - Pack the target model IDs into a 32-bit value:
value = (model_f & 0xFFFF) << 16 | (model_m & 0xFFFF) - Calculate the CWCheat offset:
offset = entry_addr - 0x08800000 - Write the CWCheat line:
_L 0x2{offset:07X} 0x{value:08X}
Each armor set has Blademaster and Gunner variants at consecutive equip_ids. You need to patch both to ensure the transmog works regardless of which variant is equipped.
entry_addr = 0x08960750 + eid * 40
value = 0x00000000 # model (0,0) = invisible with hair
offset = entry_addr - 0x08800000
_L 0x2{offset:07X} 0x00000000
Change the visual appearance of weapon A to look like weapon B, without affecting stats.
Overwrite the model ID field (u16 at offset +16) of ALL entries that share the source weapon's model ID.
- Identify the source weapon's model ID (e.g., Sieglinde = model 21)
- Identify the target weapon's model ID (e.g., BFB = model 242 = 0x00F2)
- Find all entries in the weapon table with the source model ID:
for i in range(1149): entry_addr = 0x089574E8 + i * 24 if read_u16(data, entry_addr + 16) == source_model: # This entry needs patching
- For each matching entry, calculate the CWCheat line:
model_addr = entry_addr + 16 offset = model_addr - 0x08800000 _L 0x1{offset:07X} 0x0000{target_model:04X}
A weapon model appears in multiple table entries (different upgrade tiers). You must patch all of them because you might have any upgrade tier equipped.
Entry 33: model at 0x08957810 → _L 0x10157810 0x000000F2
Entry 34: model at 0x08957828 → _L 0x10157828 0x000000F2
Entry 701: model at 0x0895B6B0 → _L 0x1015B6B0 0x000000F2
The FUComplete mod documentation lists all equipment with their model file names. The model number is extracted from the filename:
m_hair096.pac→ HEAD model 96we021.pac→ weapon model 21
To find all equip_ids that use a specific model, scan the relevant table:
# For armor (e.g., HEAD):
table_base = 0x08960750
for eid in range(400):
addr = table_base + eid * 40
model_m = read_s16(data, addr)
model_f = read_s16(data, addr + 2)
if model_m == target_model:
print(f"Found at eid={eid}")
# For weapons:
for i in range(1149):
addr = 0x089574E8 + i * 24
model = read_u16(data, addr + 16)
if model == target_model:
print(f"Found at index={i}")_C0 Armor Transmog: Rath Soul -> Black (invisible head)
_L 0x20161718 0x00000000 ; HEAD eid 101 (BM) -> invisible
_L 0x20161740 0x00000000 ; HEAD eid 102 (GN) -> invisible
_L 0x20165AE8 0x002F002F ; CHEST eid 99 (BM) -> Black model 47/47
_L 0x20165B10 0x00300030 ; CHEST eid 100 (GN) -> Black model 48/48
_L 0x20169C38 0x002E002E ; ARMS eid 97 (BM) -> Black model 46/46
_L 0x20169C60 0x002F002F ; ARMS eid 98 (GN) -> Black model 47/47
_L 0x2016DC70 0x00240023 ; WAIST eid 97 (BM) -> Black model 35/36
_L 0x2016DC98 0x00220024 ; WAIST eid 98 (GN) -> Black model 34/36
_L 0x20171BE0 0x00220022 ; LEGS eid 94 (BM) -> Black model 34/34
_L 0x20171C08 0x00230023 ; LEGS eid 95 (GN) -> Black model 35/35_C0 Weapon Transmog: Sieglinde -> Black Fatalis Blade
_L 0x10157810 0x000000F2 ; Entry 33 model 21 -> 242
_L 0x10157828 0x000000F2 ; Entry 34 model 21 -> 242
_L 0x1015B6B0 0x000000F2 ; Entry 701 model 21 -> 242- Address: 0x0893E7F0
- Entry size: 8 bytes, indexed by model number
- This maps model IDs to file loading parameters
- WARNING: Modifying this table affects ALL equipment using that model number (e.g., changing model 21 here broke head armor)
| Table | Address | Entry size | Notes |
|---|---|---|---|
| TABLE_A | 0x089972AC | u16 array | Not used at model-load time |
| TABLE_B | 0x08997BA8 | u16 array | Matches modelIdMale but not used for rendering |
| TABLE_E | 0x0899851C | u16 array | Not used at model-load time |
- Address: 0x08938D1A
- Entry size: 26 bytes
- Field +18 = weapon ID, links to materials
- Address: 0x089A1878
- Entry size: 10 bytes
- 87 entries with sequential IDs (0-86)
MHFU allows players to change the pigment (color) of their armor. The pigment is stored as a color index (0–10) per armor slot in the player's character data.
| Index | Color |
|---|---|
| 0 | White |
| 1 | Black |
| 2 | Red |
| 3 | Pink |
| 4 | Orange |
| 5 | Yellow |
| 6 | Green |
| 7 | Jade Green |
| 8 | Deep Blue |
| 9 | Blue |
| 10 | Purple |
Five consecutive 4-byte slots store the pigment index for each armor slot:
| Address | Slot |
|---|---|
| 0x090B0250 | Slot 1 |
| 0x090B0254 | Slot 2 |
| 0x090B0258 | Slot 3 |
| 0x090B025C | Slot 4 |
| 0x090B0260 | Slot 5 |
Each slot stores the color index as a single byte at offset +0, with bytes +1 through +3 set to 0xFF.
| Address | Content |
|---|---|
| 0x090B02D0 | FF FF FF FF 7F 7F 7F 00 — base white/gray reference |
| 0x090B02E0 | RGB bytes of the active pigment color (derived from index) |
| 0x090B02E4 | Duplicate of 0x090B02E0 |
The game checks byte +19 of the equipped armor's entry to determine if pigment is supported:
- 0 = pigment not supported (low-rank / base armor)
- 1 = pigment supported (special elder dragon S armor)
- 2 = pigment supported (high-rank S armor)
- 3 = pigment supported (G-rank X/Z armor)
To enable pigment on armor that doesn't normally support it (e.g., when transmogging to a pigment-supporting target), write 0x02 to byte +19 of the source armor's table entry:
pigment_flag_addr = entry_addr + 19
offset = pigment_flag_addr - 0x08800000
_L 0x0{offset:07X} 0x00000002
Use type 0 (8-bit write) to set the pigment color:
offset = pigment_address - 0x08800000
_L 0x0{offset:07X} 0x000000{color_index:02X}
_C1 Pigment Color: Black
_L 0x008B0250 0x00000001
_L 0x008B0254 0x00000001
_L 0x008B0258 0x00000001
_L 0x008B025C 0x00000001
_L 0x008B0260 0x00000001-
LEGS table is at pointer index 0, not index 5. Index 5 contains
0x02030007(a flag value, not a pointer). -
Do NOT modify the model file lookup table at
0x0893E7F0. It is shared across equipment types and changing an entry there affects ALL equipment using that model number. -
Runtime addresses are unreliable. Addresses like
0x08A35890(current head model file_id) or0x0912F54Cchange depending on which equipment is loaded. They cannot be used for static CWCheat codes. -
Entity model data is overwritten each frame. Patching the entity structure directly (e.g., at
0x099959A0 + offset) does not persist. -
CWCheat type 5 and type 8 don't work on PPSSPP. Only byte (type 0), halfword (type 1), and word (type 2) constant writes are reliable.
-
Armor transmog requires re-equipping. After enabling the cheat, unequip and re-equip the armor piece to trigger a model reload.
-
Multiple weapon entries share the same model. When creating weapon transmog, you must patch ALL entries with the source model ID, not just one.
All scripts are in /Users/Exceen/Downloads/mhfu_transmog/:
| Script | Purpose |
|---|---|
find_weapon_data.py |
Discovered the weapon data table structure at 0x089574E8 |
precise_lookup.py |
Reads specific armor entries and computes CWCheat lines |
gen_full_transmog.py |
Generates full armor transmog codes (searches by model ID) |
find_all_transmog.py |
Comprehensive armor table search |
find_black_armor.py |
Finds Black armor entries across all tables |
read_head_table.py |
Reads HEAD armor table, dumps entry structure |
read_jump_table.py |
Reads the equipment type dispatch jump table |
verify_weapon_table2.py |
Verified the (wrong) model file table at 0x0893E7F0 |
disasm_equip_code.py |
MIPS disassembler helper for analyzing game code |