impress/docs/customization-architecture.md
Emi Matchu d72d358135 Add high-level documentation
I'm starting to learn how AI agent stuff works, and a lot of what I'm finding is that rushing them into feature development sets you up for disaster, but that having strong collaboration conversations with helpful context works wonders.

So, I'm starting by creating that context: I had a little "here's the codebase" walkthrough conversation with Claude Code, and it generated these docs as output—which came out solid from the jump, with a few tweaks from me for improved nuance.

My hope is that this can serve both as an improved starting point for human collaborators _and_ if I let future Claude instances play around in here. That's a big theme of what I've found with AI tools so far: don't try to get clever, don't expect the world, just give them the same support you'd give people—and then everybody wins 🤞
2025-10-30 07:31:36 +11:00

15 KiB

Dress to Impress: Customization System Architecture

Dress to Impress (DTI) models Neopets's pet customization system: layered 2D images of pets wearing clothing items. This guide explains how DTI's data models represent the customization system and how they combine to render outfit images.

Core Models

The customization system is built on these key models:

Species and Color

  • Species: The type of Neopet (Acara, Zafara, etc.)
  • Color: The paint color applied to a pet (Blue, Maraquan, Halloween, etc.)
  • Colors have flags: basic (starter colors like Blue/Red), standard (follows typical body shape), nonstandard (unusual shapes)

PetType

PetType = Species + Color

  • Represents the combination of a species and color (e.g., "Blue Acara", "Maraquan Acara")
  • Contains the critical body_id field: the physical shape/compatibility ID
  • Body ID determines what clothing items are compatible with this pet
  • Example: "Blue Acara" and "Red Acara" share the same body_id (they have the same shape), but "Maraquan Acara" has a different body_id (different shape)

PetState

PetState = A visual variant of a PetType

  • Represents different presentations: gender (feminine/masculine) and mood (happy/sad/sick)
  • Standard poses: HAPPY_FEM, HAPPY_MASC, SAD_FEM, SAD_MASC, SICK_FEM, SICK_MASC
  • Special case: UNCONVERTED - legacy pets from before the customization system (now mostly replaced by Alt Styles)
  • Has many swf_assets (the actual visual layers for the pet's appearance)

Item

  • Represents a wearable clothing item
  • Can have different appearances for different body IDs
  • Tracks cached_compatible_body_ids: which bodies this item has been seen on
  • Tracks cached_occupied_zone_ids: which zones this item's layers occupy

Item.Appearance

Not a database model - a Struct that represents "this item on this body"

  • Created on-the-fly when rendering
  • Contains: the item, the body (id + species), and the relevant swf_assets
  • Why it exists: items can look completely different on different bodies

SwfAsset

The actual visual layer - a single image in the final composite

  • Two types (via type field):
    • 'biology': Pet appearance layers (tied to PetStates)
    • 'object': Item appearance layers (tied to Items)
  • Has a body_id: either a specific body, or 0 meaning "fits all bodies"
  • Belongs to a Zone (which determines rendering depth/order)
  • Contains URLs for the image assets (Flash SWF legacy, HTML5 canvas/SVG modern)
  • Has zones_restrict: a bitfield indicating which zones this asset restricts

Zone

  • Defines a layer position in the rendering stack
  • Has a depth: lower depths render behind, higher depths render in front
  • Has a label: human-readable name (e.g., "Hat", "Background")
  • Multiple zones can share the same label but have different depths (for items that "wrap around" pets)

AltStyle

Alternative pet appearance - a newer system for non-standard pet looks

  • Used for "Nostalgic" (pre-customization) appearances and other special styles
  • Replaces the pet's normal layers entirely (not additive)
  • Has its own body_id (distinct from regular pet body IDs)
  • Most items are incompatible - only body_id=0 items work with alt styles
  • Example: "Nostalgic Grey Zafara" is an AltStyle, not a PetState

The Rendering Pipeline

This is the core of DTI's customization system: how we turn database records into layered outfit images.

Overview

Input: An Outfit (a pet appearance + a list of worn items) Output: A sorted list of SwfAsset layers to render, bottom-to-top

Step 1: Choose the Biology Layers

# If alt_style is present, use its layers; otherwise use pet_state's layers
biology_layers = outfit.alt_style ? outfit.alt_style.swf_assets : outfit.pet_state.swf_assets
  • Alt styles completely replace pet layers (they don't layer on top)
  • Regular pets use their pet_state's layers
  • All biology layers have type = 'biology'

Step 2: Load Item Appearances

# Get how each worn item looks on this body
body_id = outfit.alt_style ? outfit.alt_style.body_id : outfit.pet_type.body_id
item_appearances = Item.appearances_for(outfit.worn_items, body_id)
item_layers = item_appearances.flat_map(&:swf_assets)
  • For each worn item, find the swf_assets that match this body
  • Matching logic: asset's body_id must equal the pet's body_id, OR asset's body_id = 0 (fits all)
  • For alt styles: only body_id=0 items will match (body-specific items are incompatible)
  • All item layers have type = 'object'

Step 3: Apply Restriction Rules

This is where it gets complex. We need to hide certain layers based on zone restrictions.

Rule 3a: Items Restrict Pet Layers

# Collect all zones that items restrict
item_restricted_zone_ids = item_appearances.flat_map(&:restricted_zone_ids)

# Hide pet layers in those zones
biology_layers.reject! { |layer| item_restricted_zone_ids.include?(layer.zone_id) }

Example: The "Zafara Agent Hood" restricts the "Hair Front" and "Head Transient Biology" zones.

How it works:

  • Items have a zones_restrict bitfield indicating which zones they restrict
  • When an item restricts zone 5, all pet layers in zone 5 are hidden
  • This allows items to "replace" parts of the pet's appearance

Rule 3b: Pets Restrict Body-Specific Item Layers

This rule is asymmetric and more complex!

Note: This is a legacy rule, originally built for Unconverted pets. Now, Unconverted pets don't exist… but pet states do still technically support zone restrictions. We should examine whether any existing pet states still use this feature, and consider simplifying it out of the system if not.

# Collect all zones that the pet restricts
pet_restricted_zone_ids = biology_layers.flat_map(&:restricted_zone_ids)

# Special case: Unconverted pets can't wear ANY body-specific items
if pet_state.pose == "UNCONVERTED"
  item_layers.reject! { |layer| layer.body_specific? }
else
  # Other pets: hide body-specific items only in restricted zones
  item_layers.reject! do |layer|
    layer.body_specific? && pet_restricted_zone_ids.include?(layer.zone_id)
  end
end

Example: Unconverted pets can wear backgrounds (body_id=0) but not species-specific clothing.

Key distinction:

  • Items restricting zones → hide pet layers
  • Pets restricting zones → hide body-specific item layers
  • Unconverted pets are special: they reject ALL body-specific items, regardless of zone

Why body_specific? matters:

  • Items with body_id=0 fit everyone and are never hidden by pet restrictions
  • Items with specific body IDs are "body-specific" and can be hidden

Rule 3c: Pets Restrict Their Own Layers

# Pets can hide parts of themselves too
biology_layers.reject! { |layer| pet_restricted_zone_ids.include?(layer.zone_id) }

Example: The Wraith Uni has a horn asset, but its zone restrictions hide it.

Why this exists: Sometimes the pet data includes layers that shouldn't be visible, probably to enable the Neopets team to more easily manage certain kinds of appearance assets. This allows the pet's metadata to control which of its own layers render.

Step 4: Sort by Depth and Render

all_layers = biology_layers + item_layers
all_layers.sort_by(&:depth)
  • Pet layers first, then item layers (maintains order for same-depth assets)
  • Sort by zone depth: lower depths behind, higher depths in front
  • The sorted list is the final render order: first layer on bottom, last layer on top

Important: When a pet layer and item layer share the same zone (and thus the same depth), the item appears on top. This is achieved by putting item layers second in the concatenation and relying on sort stability.


Understanding Compatibility

Why body_id Is the Key

You might expect items to be compatible with a species (all Acaras) or a color (all Maraquan pets). But the system uses body_id instead.

Why?

  • Some colors share the same body shape across species (e.g., most "Blue" pets follow a standard shape)
  • Some colors have unique body shapes (e.g., Maraquan pets are aquatic and need different clothing)
  • Some species have unique shapes even in "standard" colors
  • Body ID captures the actual physical compatibility, regardless of color or species

Example:

  • Blue Acara (body_id: 93) and Red Acara (body_id: 93) → same body, share items
  • Blue Acara (body_id: 93) and Maraquan Acara (body_id: 112) → different bodies, different items
  • An item compatible with body 93 fits both Blue and Red Acaras

Body ID 0: Fits All

  • Items with body_id=0 assets fit every pet
  • Examples: backgrounds, foregrounds, trinkets
  • These are the only items compatible with alt styles

Standard vs. Nonstandard Bodies

  • Standard bodies: Follow typical species shapes (usually "Blue" or other basic colors)
  • Nonstandard bodies: Unusual shapes (Maraquan, Baby, Mutant, etc.)
  • This distinction helps with modeling predictions (more on that below)

How Compatibility Is Discovered

DTI doesn't know in advance which items fit which bodies. Instead, users contribute compatibility data through "modeling":

  1. A user loads their pet wearing an item
  2. DTI's backend calls Neopets APIs to fetch the pet's appearance data
  3. The response includes which asset IDs are present
  4. DTI records: "Item X has asset Y for body_id Z"
  5. Over time, the database learns which items fit which bodies

This crowdsourced approach is why DTI is "self-sustaining" - users passively contribute data just by using the site.


Modeling and Data Sources

The Modeling Process

"Modeling" is DTI's term for crowdsourcing appearance data from users:

  1. User submits a pet name, often to use as the base of a new outfit
  2. DTI makes an API request to Neopets using the legacy Flash/AMF protocol
  3. Response contains:
    • Pet's species, color, mood, gender
    • List of biology asset IDs (the pet's appearance)
    • List of object asset IDs (items the pet is wearing)
    • Metadata for each asset (zone, manifest URLs, etc.)
  4. DTI creates/updates records:
    • PetType (if new species+color combo)
    • PetState (if new pose/mood/gender combo)
    • SwfAsset records for each asset
    • ParentSwfAssetRelationship linking assets to pets/items

See app/models/pet/modeling_snapshot.rb for the full implementation.

Cached Fields

To avoid expensive queries, several models cache computed data:

  • Item:

    • cached_compatible_body_ids: Which bodies we've seen this item on
    • cached_occupied_zone_ids: Which zones this item's assets occupy
    • cached_predicted_fully_modeled: Whether we think we've seen all compatible bodies
  • PetState:

    • swf_asset_ids: Direct list of asset IDs (for avoiding duplicate states)

These fields are updated automatically when new modeling data arrives.

Prediction Logic

DTI tries to predict which bodies an item should work on, based on which bodies we've already seen it modeled on. This helps prioritize modeling work and estimate how "complete" an item's data is.

The Maraquan Mynci Problem

The core challenge is avoiding false positives. Consider: most Maraquan pets have unique, aquatic body shapes and wear special Maraquan-themed items. But the Maraquan Mynci shares the same body_id as basic Myncis - it can wear any standard Mynci item.

Naive approach: "We saw this item on a Maraquan pet, so predict it fits all Maraquan pets"

Problem: Most items fit the Maraquan Mynci but NOT other Maraquan pets!

The Solution: Unique Body Detection

For each item, we check whether this item's modeling data shows a color in a "modelable" way: the item must fit at least one body_id that ONLY that color uses (not shared with any other color).

Example 1 - Real Maraquan item:

  • Modeled on: Maraquan Acara (body 112), Maraquan Zafara (body 98)
  • Both bodies are unique to Maraquan (no basic pets share them)
  • Prediction: This fits all Maraquan pets ✓

Example 2 - Standard Mynci item:

  • Modeled on: Blue Mynci (body 47), Maraquan Mynci (body 47)
  • Body 47 is shared by basic and Maraquan
  • Maraquan has NO unique body for this item
  • Prediction: This is a standard item for Myncis, not a Maraquan item ✓

Example 3 - Maraquan item on Mynci:

  • Modeled on: Maraquan Acara (body 112), basic Mynci (body 47)
  • Body 112 is unique to Maraquan
  • Maraquan HAS a unique body (the Acara)
  • Prediction: This fits all Maraquan pets (including Mynci) ✓

Basic Color Handling

Basic colors (Blue, Red, Green, etc.) always share the same body IDs in practice, so they're treated as a group. An item is predicted to fit all basic pets if we find at least one basic body that doesn't also fit any of the modelable colors identified above.

This prevents false positives: if an item fits both a unique Maraquan body AND a basic/Maraquan shared body (like the Mynci), we treat it as a Maraquan item, not a universal basic item.

Edge Cases

The algorithm has several early exits:

  1. Manual override: If modeling_status_hint is set to "done", trust current data is complete
  2. Fits all (body_id=0): Item fits everyone, prediction complete
  3. Single body: Only one body seen - could be species-specific, stay conservative
  4. No data: No bodies yet - optimistically predict all basic bodies for recently-released items

Why This Works

This approach is self-sustaining: no manual color flagging needed. As users model items, the unique body pattern emerges naturally, and predictions improve automatically.

See Item#predicted_body_ids in app/models/item.rb:306-373 for the full implementation.

Flash → HTML5 Transition

Neopets originally used Flash (SWF files) for customization. Over time, they've migrated to HTML5 (Canvas/SVG).

SwfAsset fields:

  • url: Legacy SWF file URL (mostly unused now)
  • manifest_url: JSON manifest for HTML5 assets
  • has_image: Whether we generated a PNG from the SWF (legacy)

Modern rendering:

  1. Load the manifest JSON from manifest_url
  2. Parse it to find canvas library JS, SVG files, or PNG images
  3. Use Impress 2020 (the frontend) to render them

Fallbacks:

  • If manifest_url is missing or 404s, fall back to legacy PNG images
  • If those are missing too, the asset can't be rendered

Summary: Key Takeaways

  1. body_id is the compatibility key, not species or color
  2. Restrictions are subtractive: start with all layers, then hide some via restriction rules
  3. Restrictions are asymmetric: items hide pet layers, pets hide body-specific items
  4. Unconverted pets are special: they reject all body-specific items (and are no longer available on Neopets)
  5. Alt styles replace pet layers and only work with body_id=0 items
  6. Data is crowdsourced through user modeling, not pre-populated
  7. The system evolved over time: Flash→HTML5, UC→Alt Styles, etc.

Code References

  • Rendering logic: Outfit#visible_layers in app/models/outfit.rb
  • Item appearances: Item.appearances_for in app/models/item.rb
  • Modeling: Pet::ModelingSnapshot in app/models/pet/modeling_snapshot.rb
  • Body compatibility: Item#compatible_body_ids, Item#predicted_body_ids in app/models/item.rb
  • Pet state poses: PetState#pose, PetState.with_pose in app/models/pet_state.rb