Skip to content

EverydaySimpleDev/Final-Fantasy-Crystal-Chronicles-Randomizer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FFCC Chest Randomizer

Tools for viewing, randomizing, and hand-editing the treasure-chest contents of Final Fantasy Crystal Chronicles (GameCube), straight from an .iso.

Easiest: the all-in-one GUI

py ffcc_gui.py

One window with tabs for everything:

  • Randomizer — pick a Source ISO (never modified) and an Output ISO (auto-suggested as <source> - randomized.iso, or choose your own), set options, then Preview / Randomize!, and write a spoiler or export/patch JSON. Writes only ever go to the output ISO.
  • Chest Editor — the per-chest / per-cycle editor (below).
  • File Tools — extract / inject / list files in the ISO, and edit item stats in param.cfd.
  • Help — a quick in-app guide.

The command-line tools below do the same things if you prefer a terminal.

Requirements & ground rules

  • Python 3 (invoked as py on Windows, or python3).
  • Always work on a copy of your ISO. run and patch modify the ISO in place. Keep your clean original as a backup (and as a --ref for chest numbering).
  • Quote any path that contains spaces.

What can go in a chest

Chests can hold any droppable item: Artifacts, Magicite (real stones), Phoenix Down, Materials, Food, and Recipes/Scrolls. Craftable equipment (weapons, armor, shields, gauntlets, helmets, belts, accessories) is not grantable from a chest — the chest opens but gives nothing — so the tools never place it. Item names/IDs come from ffcc_items.py.

How chests, cycles, and slots work

Each chest is a set of slots, and the game rolls among the slots that belong to the current cycle (year). For a 7-slot chest: cycle 1 = slots 1-4, cycle 2 = slots 5-6, cycle 3 = slot 7. So a chest can give different items in different cycles, and (depending on --rolls) several possible items within one cycle.


randomizer.py — main tool

py randomizer.py <command> "<iso>" [json] [options]
Command What it does
list Show the dungeons in the ISO and how many chest sets each has
preview Dry run — print what would change, write nothing
run Randomize the ISO in place (auto-writes a spoiler log: chosen options, every chest, and shop stock)
spoiler Write a spoiler log of the ISO's current contents
export Dump every chest to an editable JSON
patch Apply a chest JSON to the ISO
list-shops List the shops in the ISO (Tipa, Alfitaria, …)
preview-shops Dry run — show how many shop slots would change
shops Randomize shop inventories in place (all, or --shop ones; add --prices to shuffle prices too)
prices Shuffle item prices only (the gil/price tags shops use)

Options

Option Values Meaning
--seed N any integer Reproducible result (same seed → same layout). Omit for random
--mode cross (default) / category cross = any droppable item anywhere; category = keep each chest's original category
--rolls cycle (default) / slot / chest cycle = one item per chest per cycle; slot = every slot independent (most variety, multiple items per cycle); chest = one item for the whole chest
--pool all (default) / artifact / magicite / consumable / recipe Restrict which items can appear
--max-artifacts N default 4 Cap artifacts per dungeon per cycle (the player's carry limit); excess chests get a non-artifact item
--dungeon NAME e.g. river, gob Only this dungeon (repeatable). Names come from list
--fill-empty flag Also fill placeholder/empty slots
--chests-only flag Randomize only chests, not enemy drops (they share the loot pool). Isolates Game8-identified chests; needs --ref; undocumented dungeons skipped
--ref "<vanilla.iso>" path Label spoiler/export chests by their Game8 chest number

Examples

# See what's in the ISO
py randomizer.py list "Hacked Rom.iso"

# Preview a full randomization (writes nothing)
py randomizer.py preview "Hacked Rom.iso" --seed 42

# Randomize a COPY (also writes "<iso> - spoiler.txt" next to it)
py randomizer.py run "Hacked Rom - randomized.iso" --seed 42 --ref "Hacked Rom.iso"

# Variations
py randomizer.py run "copy.iso" --mode category               # stay in original category
py randomizer.py run "copy.iso" --rolls slot                  # multiple items per cycle
py randomizer.py run "copy.iso" --pool recipe --dungeon river # only recipes, only River

# Spoiler for an already-made ISO (chest numbers need --ref)
py randomizer.py spoiler "Hacked Rom - randomized.iso" --ref "Hacked Rom.iso"

Shop randomization

Shops keep their inventory in the town scripts; each slot's price is derived from the item, so randomizing a shop just swaps the item (the price follows). Stock is drawn only from items shops already sell (materials, recipes, food, magicite, Phoenix Down), so every price stays valid. Equipment/artifacts are never added.

py randomizer.py list-shops     "copy.iso"                 # Tipa, Alfitaria, ...
py randomizer.py preview-shops  "copy.iso" --seed 42       # dry run, writes nothing
py randomizer.py shops          "copy.iso" --seed 42       # randomize ALL shops
py randomizer.py shops          "copy.iso" --shop village_0 --shop weapon_0  # just these
py randomizer.py shops          "copy.iso" --seed 42 --prices   # items AND prices
py randomizer.py prices         "copy.iso" --seed 42       # shuffle prices only

Prices live per-item in param.cfd (the engine derives each shop price from the item). Price randomization shuffles the real prices among the items shops sell, so every price stays plausible and valid — Bronze might cost what Mythril did, etc. It works with or without item randomization. (The 0xFFFF "not for sale" items are left alone.)

In the GUI, on the Randomizer tab tick "Randomize shop inventories" (and pick all shops or specific ones) and/or "Randomize shop prices". (Per-class restriction for items is planned for the future.)

JSON workflow (precise hand-editing)

py randomizer.py export "Hacked Rom.iso" --ref "Hacked Rom.iso"   # -> "Hacked Rom - chests.json"
# ...edit the JSON...
py randomizer.py patch  "Hacked Rom - patched.iso" "Hacked Rom - chests.json"

The JSON maps dungeon script → area → set index → cycle → item (each dungeon has one or more area files _0, _1, …):

{
  "river": {
    "_name": "River Belle Path",
    "0": {
      "13": {
        "_label": "Chest 1",
        "_chest": 1,
        "1": "Phoenix Down",
        "2": "0x0107",
        "3": "Stone of Cure"
      },
      "19": {
        "_label": "Chest 2",
        "_chest": 2,
        "1": ["Gold", "Silver"],
        "2": "Diamond Ore",
        "3": "Ultimite"
      },
      "6": {
        "_label": "Monster 1",
        "_chest": null,
        "1": "Shuriken",
        "2": "Ice Brand"
      },
      "10": { "_label": "Magicite 1", "_chest": null, "1": "Stone of Fire" },
      "0": { "_label": "Gathering 1", "_chest": null, "1": "Vegetable Seed" }
    },
    "1": {
      "5": {
        "_label": "Chest 7",
        "_chest": 7,
        "1": "Bronze",
        "2": "Iron",
        "3": "Mythril"
      }
    }
  }
}
  • Area index ("0", "1", …) selects the dungeon's area file; set index is the stable key within an area. _name, _label, _chest, and any _-prefixed key are human-readable metadata, ignored on import.
  • _label mirrors the chest editor: each Game8 chest is numbered once per dungeon ("Chest N"); every other set is classified (validated against the GameCube set-list sheet) as "Monster N" (enemy drop), "Magicite N" (stone/element spawn), or "Gathering N" (food/seeds), numbered across areas. _chest is the numeric chest id, or null otherwise. (Labels need --ref; without it every set is just "Set N".)
  • Cycle is "1", "2", or "3"; the value sets every real-item slot in that cycle.
  • Item can be a name ("Phoenix Down"), a hex id ("0x0107"), or the export's "Name [0xID]" label (the hex wins). A list is spread across the cycle's slots (e.g. ["Gold","Silver"] → Gold/Silver/Gold/Silver).
  • Non-droppable or unresolved items are warned about and skipped — no silent failures. Ambiguous names resolve to the droppable item (e.g. "Iron Shield" → the recipe, not the equipment).
  • After patching, if any cycle ends up with more than 4 artifacts (the player's carry limit) you get a warning per dungeon/cycle. The patch still applies — you're in control — but it flags the over-limit cycle.

chesteditor.py — GUI editor

py chesteditor.py
  1. Open ISO (back it up first).
  2. Pick a dungeon, click Load.
  3. The table shows one row per chest; the three columns are what that chest gives in Cycle 1 / 2 / 3. Green "Chest N" rows are matched to Game8's chest numbers (each chest numbered once per dungeon, across all its area files). Every other set is classified (validated against the GameCube set-list sheet) and colour-coded: Monster N (red, enemy drop), Magicite N (purple, stone/element spawn), Gathering N (tan, food/seeds).
  4. Double-click a cycle cell to change what that chest gives that cycle. The picker has a category filter and a search box and lists every droppable item.
  5. Game8 Reference shows the canonical per-chest contents.
  6. Save to ISO writes the edits in place.

Works for all dungeons (signature-based detection).


Adding a custom item

You can repurpose one of the game's unused "Extra N" item slots into a custom item (no table resize needed). customitem.py does the whole thing — copies a donor item's definition (so the new item reuses its 3D model + menu icon + behaviour), sets the price, and writes the name into the in-game name table.

py customitem.py list-free "copy.iso"                          # show repurposable slots
py customitem.py add "copy.iso" 0x162 "AP Item" --like Gold --gil 10
py customitem.py add "copy.iso" 0x162 "AP Item" --like Gold --gil 10 \
                    --shop village_0 --chest river             # also place it for testing
py customitem.py show "copy.iso" 0x162
  • --like is the donor (name or id) — its model/icon/type are copied. --gil sets the price. --shop/--chest optionally place it for testing.
  • Name length is capped by the slot (names are edited in place to keep the file size fixed) — the "Extra N" slots fit roughly an 8-character name. Use a short name or a slot with a longer placeholder.
  • To have the randomizer/editor carry the new item, add NAMES[<id>] to ffcc_items.py and remove the id from randomizer.EXCLUDE (the tool prints the exact lines). Limits: reusing an existing model/icon is easy; a brand-new model or a new behaviour type needs graphics/executable work.

Editing models & textures (Blender round-trip)

Item/character models and textures are GameCube IFF-tag containers (.chm/.cha/.chd/.tex). You can edit them and put them back:

# textures (needs Pillow):  export -> edit the PNG -> re-import (same size) -> inject
py tex.py export "w001_root.tex" out_png/
py tex.py import "w001_root.tex" out_png/ "w001_root_new.tex"
py gciso.py inject "copy.iso" dvd/char/wep/w001/w001_root.tex "w001_root_new.tex"

# 3D models (Blender round-trip, topology-preserving):
py objimport.py export    "w001_root.chm" w001_obj/     # one OBJ per mesh
#   ...open the OBJ in Blender, MOVE/SCALE/MORPH vertices (Keep Vertex Order,
#   axis Forward Z / Up Y), export the OBJ back...
py objimport.py import    "w001_root.chm" w001_obj/ "w001_root_new.chm"
py objimport.py roundtrip "w001_root.chm"               # self-test (byte-identical)
py gciso.py inject "copy.iso" dvd/char/wep/w001/w001_root.chm "w001_root_new.chm"
  • chmio.py is the byte-exact container reader/writer underneath (verified byte-identical on all 3,864 model/texture files). import is topology-preserving (move/scale/morph existing vertices only). To put in a completely new mesh (different vertex/face count), use newmesh:
py objimport.py export  "w001_root.chm" w001_obj/    # OBJ now includes faces (f p/t/n)
#   ...replace/edit the mesh in Blender however you like, export OBJ (with normals+UVs)...
py objimport.py newmesh "w001_root.chm" w001_obj/w001_root.obj "w001_new.chm"
py gciso.py rebuild "copy.iso" dvd/char/wep/w001/w001_root.chm "w001_new.chm" "out.iso"

newmesh rebuilds the mesh's vertices, UVs, normals and faces (writing a fresh GameCube display list via dlst.py), keeping the original material/texture. The model can be any size — gciso rebuild handles it. Works for static models (items, weapons, props); rigged/animated characters also store per-vertex bone weights (SKIN) that newmesh doesn't rewrite yet, so don't change their topology.

Replacing a file at a different size (ISO rebuild)

gciso inject is same-size-only. To put back a file that grew or shrank, use rebuild — it appends the new file at the end of the image and repoints its FST entry, leaving every other file byte-identical:

py gciso.py rebuild "copy.iso" dvd/char/wep/w001/w001_root.chm bigger.chm "out.iso"

The output image can exceed the 1.46 GB disc size, so it's for emulators (Dolphin), not burning. (Repeated rebuilds leave dead space; rebuild from a clean ISO to stay lean.)

Supporting / low-level tools

Usually not needed directly, but available:

py gciso.py list|extract|inject "<iso>" <disc_path> [file]   # raw file in/out of the ISO
py items.py show|list|set <param.cfd> <id> ...               # edit item stats/definitions
py customitem.py list-free|show|add "<iso>" ...              # add a custom item (above)
py tex.py export|import <file.tex> ...                       # edit textures (PNG round-trip)
py objimport.py export|import|newmesh|roundtrip <model.chm>  # edit/replace models (Blender)
py chmio.py <file>          # byte-exact model-container round-trip check
py dlst.py  <model.chm>     # inspect/verify display lists (faces)
py chest.py table|find|setslot|set <file.cft> ...            # CLI single-file chest edits
py cft.py tree|blocks|find|strings|block|calls <file.cft>    # inspect compiled script files

Typical end-to-end flow

  1. Copy your ISO: cp "Hacked Rom.iso" "Hacked Rom - randomized.iso"
  2. Randomize: py randomizer.py run "Hacked Rom - randomized.iso" --seed 42 --ref "Hacked Rom.iso"
  3. Read Hacked Rom - randomized - spoiler.txt, then play that ISO.

Notes & known scope

  • Chests and enemy drops share the same get_treasure item pool, so by default randomizing changes both. Use --chests-only (or untick "Randomize enemy drops too" in the GUI) to limit changes to the Game8-identified chests.
  • The end-of-dungeon boss/bonus reward is a separate subsystem (8 artifact sets gated by cycle and bonus points) and is not touched by these tools — chest randomization is independent of it. (See Boss Reward Sets (GameCube guide).md for a transcription.)
  • All 14 dungeons are covered, including Tida, Moschet Manor, Daemon's Court, and Rebena Te Ra. Dungeons span multiple area files (_0, _1, …) and the randomizer processes every area, with the 4-artifacts-per-cycle cap applied across the whole dungeon (all its areas combined).

About

My own Final Fantasy Crystal Chronicles Randomizer!

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages