Metatile Attributes Reference
Every metatile in a tileset carries an attribute value: a small packed integer that tells the game engine various metadata facts about that metatile. The most important piece is the behavior (tall grass, water, a ledge, a door), but the attribute value also stores the layer type and, depending on the game, extra fields like terrain and encounter type.
In a Porytiles-managed tileset, you edit attributes in a plain text file, porytiles_src/attributes.csv.
When you compile, Porytiles packs that CSV into the metatile_attributes.bin binary that Porymap and the game read.
This page covers the full system:
the CSV format, the layer type rules, the attribute schema that defines which fields exist,
and the configuration options that let you customize all of it.
For metatile basics (what a metatile is, how layers work), read How the GBA + Gen III Decomp Tileset System Works first.
Note
TODO: insert screencap of Porymap’s Tileset Editor showing the metatile behavior and layer type fields
What’s an attribute value?
Here’s a fun fact: the GBA’s video hardware never sees metatile attributes.
Tiles, palettes, and tilemaps are hardware concepts; attributes are pure game-engine data.
The engine looks up a metatile’s attribute value and extracts fields from it with bit masks and shifts
(see GetMetatileAttributeFromRawMetatileBehavior and friends in src/fieldmap.c).
The format is just an agreement between metatile_attributes.bin and the engine’s field-map code.
Different games pack it differently, and you can modify your project to pack it however you want.
That is why Porytiles treats the layout as configurable data.
One thing we need to clarify at the outset:
collision and elevation are not metatile attributes.
Those live in each map’s block data, packed next to the metatile ID
(MAPGRID_COLLISION_MASK and MAPGRID_ELEVATION_MASK in include/global.fieldmap.h),
and you edit them per map square in Porymap. Porytiles doesn’t manage these values.
Stock layouts
Ruby, Sapphire, and Emerald use a 2-byte attribute per metatile:
Bits |
Mask |
Field |
|---|---|---|
0-7 |
|
behavior ( |
8-11 |
|
unused |
12-15 |
|
layer type |
FireRed and LeafGreen use a 4-byte attribute and pack in more fields:
Bits |
Mask |
Field |
|---|---|---|
0-8 |
|
behavior ( |
9-13 |
|
terrain ( |
14-17 |
|
unused ( |
18-23 |
|
unused ( |
24-26 |
|
encounter type ( |
27-28 |
|
unused ( |
29-30 |
|
layer type |
31 |
|
unused ( |
These tables come straight from each decomp’s source:
the masks are declared in include/global.fieldmap.h and src/fieldmap.c.
Caution
Recent pokeemerald-expansion versions declare both layouts in their source tree,
but a built ROM only ever uses one of them.
Running make produces an Emerald-format game and make firered a produces FRLG-format one.
The build filters out every map layout belonging to the other format.
Porytiles handles that case with a little manual fiddling, see Expansion’s two build flavors.
Attention
Good news: if you haven’t modified your project’s attribute layout, i.e. you have the stock Emerald or stock FireRed attributes, you can stop reading here. Porytiles handles everything for you correctly, automatically. The rest of this guide covers how to configure Porytiles if you have significantly overhauled your project’s metatile attributes.
The attribute schema
Porytiles describes an attribute layout with a schema: an ordered list of named fields. Each field has:
name: the
attributes.csvcolumn header, for examplebehaviororterrain.mask: the bits the field occupies in the packed attribute value. A mask must be a single contiguous run of bits, and fields must not overlap.
default: the value used for metatiles that have no row in
attributes.csv(0 unless configured otherwise).provider (optional): where the field’s named constants live. A provider points at a C header and a name prefix, for example
include/constants/metatile_behaviors.hwith prefixMB_. A field with a provider takes constant names in its CSV column. A field without one, i.e. a raw field, takes plain integers.role (optional): marks a field whose value Porytiles itself manages, so it doesn’t need its own column in
attributes.csv. You can still manually set values for a role-bearing field if you need to. More on that later. Currently, the only supported role islayer_type.
The attribute size itself (1, 2, or 4 bytes) is normally not something you declare in the Porytiles config.
Porytiles derives it from the resolved field masks:
the smallest of 1, 2, or 4 bytes that covers every mask
(Emerald’s 0xF000 layer mask needs 2 bytes; FireRed’s masks reach bit 31, so 4).
This width is a project-level fact, shared by every tileset:
they all feed the same metatileAttributes pointer in struct Tileset,
and the engine reads every tileset’s attributes at the same width.
Porymap models it the same way (metatile_attributes_size in porymap.project.cfg),
so your Porytiles settings should always agree with your Porymap setting.
But there’s a catch: masks can only prove a minimum width.
So Porytiles cross-checks the element type of struct Tileset’s metatileAttributes member,
and without an explicitly specified attribute size, the two must agree exactly.
Suppose the declared element type is wider than the masks need
(e.g., low-byte-only masks in a const u32 * array), or narrower than they need.
That means the project’s own sources contradict each other.
And since a project may legitimately read a different word size than it declares
(Expansion’s FRLG flavor reads 4-byte entries from const u16 arrays),
Porytiles cannot tell which width the project actually reads.
The same applies when there is no usable declaration at all: the masks alone can’t be the full picture.
Either way, Porytiles errors out and asks you to manually set fieldmap.metatile_attribute_size.
When inference is impossible or wrong, set fieldmap.metatile_attribute_size
(or pass --metatile-attribute-size) to pin the width explicitly.
An explicit size always wins over inference.
The main case that requires it is pokeemerald-expansion,
which declares two divergent attribute layouts in one source tree.
No project file records which build flavor you target (it’s a make argument),
so Porytiles cannot infer the size there and stops with an error naming this key.
See Expansion’s two build flavors.
When pinning an attribute size, you get some additional safety checks:
A field mask that needs more bits than the pinned width is an error. A common way to hit this is by pasting a FireRed mask (for example the
0x60000000layer-type mask) into an Emerald-width project. Porytiles asks you to narrow the mask, or raisemetatile_attribute_sizeif the project really uses a wider attribute.A pinned size wider than both the masks and the scanned
struct Tilesetdeclaration is allowed, matching Porymap (the unused high bits simply stay zero), but Porytiles warns, since nothing in the project backs up the extra width.
The declaration size
There is a second, related width: the C element type of the gMetatileAttributes_* array declarations
Porytiles generates in src/data/tilesets/metatiles.h (const u16 / INCBIN_U16 and so on).
On most projects it equals the attribute size, and you never need to worry about it.
Porytiles infers it from the pointed-to type of struct Tileset’s metatileAttributes member in include/global.fieldmap.h
(const u16 * on pokeemerald and Expansion, const u32 * on pokefirered).
If that declaration is missing, unreadable, or declared with anything other than a pointer to u8, u16, or u32,
Porytiles errors out rather than assuming it matches the attribute size.
In that case, you must specify it manually by setting fieldmap.metatile_attribute_declaration_size
(or passing --metatile-attribute-declaration-size).
As mentioned above, pokeemerald-expansion is one common case where the declaration size and the attribute size differ.
Expansion’s FRLG flavor attribute entries are 4 bytes,
but the arrays are still declared const u16 (the engine casts the pointer at runtime).
Porytiles gets that right automatically via the struct inference described above.
Where the schema comes from
Here’s the nice part: you almost never have to write a schema yourself. Porytiles resolves one schema per project, in this order:
Explicit config. If
fieldmap.metatile_attribute_fieldsis set in your Porytiles YAML config, it wins, and inference is never consulted for the field content. When the project’s source still declares a usable layout that disagrees with the explicit list, Porytiles warns so you know you are overriding your project’s own declarations.Inference from your project. Otherwise, Porytiles reads the mask layouts your decomp code already declares (details below). A single declared layout is used directly. A project that declares more than one (like
pokeemerald-expansion) must setfieldmap.metatile_attribute_size, and the size selects the matching layout.Error. If neither produces fields, compilation stops with an error telling you to either add a
metatile_attribute_fieldslist or restore the engine declarations so inference can succeed. There is no silent fallback.
In every case, fieldmap.metatile_attribute_field_overrides is then merged on top,
so you can adjust individual fields without redeclaring the whole layout.
Both keys are documented in Customizing the schema.
Schema inference
For a vanilla or lightly modified project, inference produces the right schema with zero configuration. It scans:
File |
What Porytiles reads from it |
|---|---|
|
the |
|
the |
|
the |
The table names are matched exactly: a table renamed or copied under a different name is not read.
Exactly four define spellings are recognized:
METATILE_ATTR_<FIELD>_MASK and METATILE_ATTR_<FIELD>_MASK_FRLG for masks,
METATILE_ATTR_<FIELD>_SHIFT and METATILE_ATTR_<FIELD>_SHIFT_FRLG for their bit offsets.
A METATILE_ATTR_ define spelled any other way ignored by inference,
and Porytiles emits a warning naming the defines it passed over.
If you have invented a spelling of your own, either rename the table/macro or declare the layout explicitly.
Field names come from the enum:
METATILE_ATTRIBUTE_BEHAVIOR becomes behavior,
METATILE_ATTRIBUTE_TERRAIN becomes terrain,
and a numbered entry like METATILE_ATTRIBUTE_2 becomes the raw field attribute_2.
METATILE_ATTRIBUTE_LAYER_TYPE becomes the field layer_type carrying the layer_type role.
Inference also attaches providers:
behaviorgets theMB_constants frominclude/constants/metatile_behaviors.h(skippingMB_INVALID, which is a sentinel rather than a real behavior). A behaviors header that is missing, unreadable, or declares noMB_name is an error: Porytiles never silently falls back to raw integer values for a field that should have named constants.Other named fields are searched against the
global.fieldmap.henums by prefix:terrainfindsTILE_TERRAIN_*,encounter_typefindsTILE_ENCOUNTER_*. A named field whose prefix matches no enum member generates an error naming the prefix it tried to find.Numbered fields (
attribute_2and friends) stay raw, and that is fine: a numeric enum entry is the project stating the field has no name and no constants.Layer type doesn’t get a provider, since its values are managed by Porytiles.
If your project generates provider-related errors, they’re easily solved:
a metatile_attribute_field_overrides entry that states the field’s provider will silence the error,
and provider: null allows you to say that the field really does use raw values.
Most projects declare exactly one mask layout, so inference finds it automatically.
A second layout can come from one specific spelling:
METATILE_ATTR_*_MASK_FRLG defines sitting alongside the bare METATILE_ATTR_*_MASK ones.
That is how pokeemerald-expansion writes it.
Its bare defines describe the 2-byte Emerald layout,
while its *_MASK_FRLG defines and sMetatileAttrMasks table describe the 4-byte FRLG layout.
The _MASK_FRLG suffix is the only signal Porytiles reads as a second layout,
so two is the most it will ever infer.
With two layouts, resolution stops with an error unless fieldmap.metatile_attribute_size is set:
the size selects the layout whose required width matches it
(a layout that merely fits inside a wider configured size is accepted when it is the only one that fits),
so on Expansion the explicit size doubles as the layout selector.
Two layouts needing the same width cannot be separated this way,
so Porytiles will ask you to declare metatile_attribute_fields instead.
See Expansion’s two build flavors.
Every field needs a mask declared somewhere in the source.
A field whose mask cannot be determined is a fatal error,
with a message listing your options
(restore sMetatileAttrMasks[], add a METATILE_ATTR_<NAME>_MASK define, or declare the layout in Porytiles config).
A project that declares no attribute masks at all fails the same way:
there is nothing to infer from, so Porytiles asks you to restore the mask declarations
or write a metatile_attribute_fields list.
When the project’s own sources disagree with each other, that is a fatal error too:
A
METATILE_ATTR_*_MASKdefine disagreeing with the field’ssMetatileAttrMasksentry means the project states two different masks for the same field, so it does not in fact state one.A declared shift that does not match its mask’s bit position, whether from a
METATILE_ATTR_*_SHIFTdefine or ansMetatileAttrShiftsentry, means the engine would unpack values from a different offset than Porytiles packs them to, corrupting every attribute in the tileset.
Each error asks you to make the sources agree,
or to state the field’s mask in metatile_attribute_field_overrides, which settles both kinds of dispute.
Checking the resolved schema
dump-attribute-schema prints the schema Porytiles resolved,
after config, inference, overrides, and mask-layout selection have all been applied.
Run it whenever you are unsure which fields your CSV needs:
porytiles dump-attribute-schema gTileset_General
Against a vanilla pokefirered project, the output looks like this:
Resolved Metatile Attribute Schema
==================================
Attribute size: 4 bytes (inferred from the sMetatileAttrMasks table (./src/fieldmap.c))
Declaration size: 4 bytes (const u32, inferred from struct Tileset's metatileAttributes member (./include/global.fieldmap.h))
Fields from: the sMetatileAttrMasks table
Fields:
behavior mask=0x1FF offset=0 width=9 default=0 provider=include/constants/metatile_behaviors.h (MB_)
terrain mask=0x3E00 offset=9 width=5 default=0 provider=include/global.fieldmap.h (TILE_TERRAIN_)
attribute_2 mask=0x3C000 offset=14 width=4 default=0
attribute_3 mask=0xFC0000 offset=18 width=6 default=0
encounter_type mask=0x7000000 offset=24 width=3 default=0 provider=include/global.fieldmap.h (TILE_ENCOUNTER_)
attribute_5 mask=0x18000000 offset=27 width=2 default=0
layer_type mask=0x60000000 offset=29 width=2 default=0 role=layer_type
attribute_7 mask=0x80000000 offset=31 width=1 default=0
The layer_type row carries role=layer_type:
it is part of the packed layout, but Porytiles fills its value in itself,
so it gets no data column in attributes.csv.
When no schema can be resolved
(stock Expansion without fieldmap.metatile_attribute_size, for example),
the command prints the reason instead of a schema.
To see the config values feeding into resolution,
dump-tileset-config shows each fieldmap.* key with the full provenance chain that produced it.
See Inspecting the resolved configuration.
The attributes.csv file
attributes.csv lives in each tileset’s porytiles_src/ directory,
next to the layer PNGs.
Porytiles reads it on compile and writes it on decompile/import.
The format:
Header row (required).
id, then one column per schema field in schema order. For stock Emerald it isid,behavior. For stock FireRed it isid,behavior,terrain,attribute_2,attribute_3,encounter_type,attribute_5,attribute_7. The header is checked against the resolved schema, and a mismatch generates an error that prints the expected header. Notice thatlayer_typeis missing. That’s because it’s a role-based field. As we said earlier, Porytiles computes it automatically. If you want to pin it in your config, see Pinning withrole_pins.Every data row must match the header. A row with too few cells, or with extra cells past the last header column, is an error. A CSV written for a wider schema throws an error instead of silently dropping the extra values.
idcolumn. The metatile index the row applies to. Row order does not matter, it’s theidvalue that decides the metatile to which the row applies. Decimal, hex (0x10), and octal (leading0) notations are all supported. Duplicate ids are an error.Missing rows are fine. A metatile with no row gets every field’s default value and an inferred layer type. When Porytiles writes the file, it omits all-default rows, so a mostly-plain tileset produces a short CSV.
Fields with a provider take full constant names, exactly as declared in the header:
MB_TALL_GRASS,TILE_TERRAIN_GRASS. Names are case-sensitive, and raw integers are not accepted in these columns. An unknown name generates an error pointing at the offending line.Raw fields (no provider) take unsigned integers, again in decimal, hex, or octal. Values are range-checked against the field’s bit width.
Cells are trimmed of surrounding whitespace, blank lines are skipped, and there is no comment syntax. Empty cells are only allowed in a trailing pin column.
A stock Emerald example:
id,behavior
0x00,MB_NORMAL
0x01,MB_TALL_GRASS
0x0E,MB_POND_WATER
0x22,MB_JUMP_SOUTH
A stock FireRed example:
id,behavior,terrain,attribute_2,attribute_3,encounter_type,attribute_5,attribute_7
0x01,MB_TALL_GRASS,TILE_TERRAIN_GRASS,0,0,TILE_ENCOUNTER_LAND,0,0
0x10,MB_POND_WATER,TILE_TERRAIN_WATER,0,0,TILE_ENCOUNTER_WATER,0,0
Layer types
The layer type controls which screen layers a dual-layer metatile’s two tile layers render on.
How the GBA + Gen III Decomp Tileset System Works explains the three values (normal, covered, split) and what they look like in game.
This section covers how Porytiles decides which one each metatile gets.
Inference
By default there is no layer type column in attributes.csv at all.
Porytiles infers each metatile’s layer type from which of your three layer PNGs
(bottom.png, middle.png, top.png) contain non-transparent content for that metatile:
Layers with content |
Inferred layer type |
|---|---|
bottom only, or bottom + middle |
|
bottom + top |
|
anything else (middle only, middle + top, top only, empty) |
|
In other words, you just draw each metatile on the layers where it belongs,
and the layer type follows automatically. Pretty convenient, right?
Triple-layer tilesets skip all of this:
in triple-layer mode every metatile renders all three layers and the stored layer type is always normal.
Note
If the resolved schema has no layer_type role field
(your project’s source declares no layer-type mask,
or your explicit metatile_attribute_fields list omits the field),
layer types are disabled: every metatile is treated as normal and no layer-type bits are written.
This matches Porymap’s behavior when its metatile_layer_type_mask is 0.
Note
TODO: insert screencap of a metatile drawn across two layer PNGs and the resulting layer type in Porymap
Pinning with role_pins
As mentioned before, a schema field can have an optional role,
which lets Porytiles manage that field’s values automatically.
Currently, the only role is layer_type.
A schema field with this role has its value filled via Porytiles’s familiar layer type inference.
For the rare case where you want a specific layer type that inference would not pick,
pin the layer_type role with a fieldmap.role_pins entry:
fieldmap:
role_pins:
- role: layer_type
With the role pinned:
attributes.csvgains a trailing pin column headedpin::layer_type, and Porytiles writes one row per metatile so every metatile has a cell to fill.A filled cell (
normal,covered, orsplit, case-insensitive) pins that metatile’s layer type. The pinned value overrides inference.A blank cell means “infer as usual”, and stays blank when Porytiles rewrites the file, so pins survive compile/decompile round trips.
The header is fixed at pin::<role> and is not configurable.
That is what lets Porytiles tell a pin column from an attribute field column by name alone,
without having to guess from the column’s position and your current config.
Turning the pin back off does not invalidate the file you already have.
When the layer_type role is not pinned (the default), a pin::layer_type column is still recognized
as a pin column and simply ignored:
Porytiles emits a one-time role-pin-column warning and infers as usual.
Delete the column when you no longer want it, or re-add the role_pins entry to start reading it again.
If a pinned (or inferred) layer type drops a layer that actually contains visible tiles,
Porytiles warns (tag dual-layer-drop) that those tiles will be discarded.
That warning usually means the pin is wrong, or content is drawn on a layer it should not be.
This is a per-tileset kind of setting,
so you should prefer setting it in porytiles/tilesets/<tileset_name>/config.yaml rather than project-wide
(see Configuration Reference for config file locations and layering).
Round-tripping the pin column
Import and decompile fill the pin column explicitly, so it survives a round trip:
Import, or decompile with no
attributes.csv: every row is pinned to the layer type decoded frommetatile_attributes.bin. A fresh import therefore hands you a fully-pinned column you can edit.Decompile with
attributes.csvpresent:If the pin column is missing, Porytiles adds it and pins every row from the bin (same as a fresh import).
If the pin column is present, Porytiles preserves each row’s state: a blank cell stays blank (inference is trusted), and a filled cell stays pinned with its value refreshed from the bin.
A row that was deleted from the file comes back unpinned (blank), like any other row you left blank.
Triple-layer content in a dual-layer tileset
A metatile with non-transparent content on all three layers can only render in a triple-layer tileset. In a dual-layer compile that is an error by default: Porytiles points at the offending subtiles and suggests either enabling triple-layer metatiles or the escape hatch below.
Setting tileset.ignore_triple_layer_content: true (or --ignore-triple-layer-content)
demotes the error to a warning and compiles anyway, dropping one layer group per offending metatile.
Which group is dropped follows that metatile’s effective layer type.
A fieldmap.role_pins layer_type pin, if you set one, chooses the group directly.
Without a pin there is nothing to infer from all-three-layers content,
so it falls back to normal, which drops the bottom layer.
Note that this fallback is a fixed rule rather than a real inference:
a genuinely triple metatile has no discernible layer type,
so unpinned it always resolves to normal.
Expansion’s two build flavors
The pokeemerald-expansion decomp base can be built in two flavors:
make produces a game with Emerald-format metatile attributes,
and make firered one with FRLG-format attributes.
The source tree permanently declares both mask layouts
(the bare METATILE_ATTR_*_MASK defines for Emerald,
the *_MASK_FRLG defines and sMetatileAttrMasks table for FRLG),
and each map layout in data/layouts/layouts.json carries a layout_version tag.
At build time, the map-data generator skips every layout whose tag disagrees with the flavor being built,
so a single ROM only ever links one attribute format.
Because the flavor is a make argument, no project file records which one you target,
and Porytiles cannot infer the attribute size on Expansion.
You must set it explicitly, and the size doubles as the layout selector:
# porytiles/config.yaml on pokeemerald-expansion
fieldmap:
metatile_attribute_size: 2 # emerald flavor; use 4 for the firered flavor
With 2, Porytiles selects the Emerald mask layout: behavior (mask 0x00FF)
plus the layer_type role field (mask 0xF000), packed into 2-byte entries.
With 4, it selects the FRLG layout: the full eight-field set with masks reaching bit 31,
including the layer_type role field at 0x60000000, and 4-byte entries.
Without the key, Porytiles stops with an error.
One Expansion quirk Porytiles handles automatically:
pokefirered declares its attribute arrays const u32, so declaration and entry width agree,
but Expansion declares all its attribute arrays const u16, FRLG flavor included,
and src/fieldmap.c casts the pointer to const u32 * at runtime for FRLG layouts.
So on the FRLG flavor, metatile_attributes.bin entries are 4 bytes
(Expansion’s included general_frlg/metatile_attributes.bin is 2560 bytes, 640 metatiles at 4 bytes each)
while the generated INCBIN declarations stay const u16.
Porytiles reads that convention from struct Tileset (see The declaration size),
so the generated declarations match the surrounding code without any extra configuration.
Note
pokeemerald-expansion documents this setup in its own
docs/tutorials/how_to_frlg.md,
and its
frlg_metatile_behavior_converter.py
migration script reads and writes 4-byte entries.
As elsewhere, dump-attribute-schema shows what’s going on.
On a stock Expansion project,
porytiles dump-attribute-schema gTileset_General --metatile-attribute-size 2 yields:
Resolved Metatile Attribute Schema
==================================
Attribute size: 2 bytes (CLI)
Declaration size: 2 bytes (const u16, inferred from struct Tileset's metatileAttributes member (./include/global.fieldmap.h))
Fields from: the bare METATILE_ATTR_*_MASK defines
Fields:
behavior mask=0xFF offset=0 width=8 default=0 provider=include/constants/metatile_behaviors.h (MB_)
layer_type mask=0xF000 offset=12 width=4 default=0 role=layer_type
Running it with --metatile-attribute-size 4 instead reports Attribute size: 4 bytes (CLI),
a declaration size still of 2 bytes (const u16),
and all eight FRLG fields, matching the pokefirered output shown in
Checking the resolved schema
(except for the declaration size, which is 4 bytes there).
Customizing the schema
Two YAML-only config keys control the schema:
metatile_attribute_fields and metatile_attribute_field_overrides.
The schema is a project-level property (one attribute layout per project),
so set those two in porytiles/config.yaml: see Configuration Reference.
A third key, role_pins, appears alongside them below but does not control the schema.
It only controls the CSV pin columns, never the binary layout, so it makes sense per tileset.
All three are YAML-only, there is no CLI flag.
YAML key |
CLI flag |
Default |
Description |
|---|---|---|---|
|
YAML-only |
inferred |
Declare the full field list yourself, replacing inference |
|
YAML-only |
empty |
Adjust individual fields on top of the global baseline |
|
YAML-only |
empty |
Emit and honor a trailing pin column per role (see above) |
Overriding individual fields
metatile_attribute_field_overrides lets you make small adjustments.
It merges onto the resolved baseline, inferred or explicit,
so you can keep the inferred values and change only what you explicitly want overridden.
Each entry may set mask, default, provider, and role;
anything you do not set falls through to the baseline.
fieldmap:
metatile_attribute_field_overrides:
# Repurpose FireRed's unused attribute_3 bits and give them a default
attribute_3:
default: 1
# Enter terrain as raw integers instead of TILE_TERRAIN_ names
terrain:
provider: null
Provider overrides are themselves partial:
you can set just prefix, just header, skipped, or format,
and the rest of the values will carry over from the global default.
provider: null removes the provider entirely, turning the field into a raw integer column.
Note that a skipped list replaces the baseline skip list wholesale rather than adding to it.
Overriding a field that does not exist in the baseline is an error
(the message lists the fields that do exist).
The role member works the same way:
role: layer_type puts the layer-type role on a field, and role: null clears it.
Overrides apply per field, so to move the role you need both
role: null on the field that has it and role: layer_type on the new one
(two fields carrying the role at once is an error).
Clearing the role without assigning it elsewhere disables layer types.
Declaring fields from scratch
If your project’s attribute layout is customized enough that inference cannot describe it,
declare the whole layout explicitly with metatile_attribute_fields.
Each entry takes name, mask, optional default, optional role, and an optional provider map
with header, prefix, optional skipped, and optional format
(defines-only, enums-only, or either; the default is either).
Here is a custom 4-byte layout for a project that added a light level and a room type to its attributes:
fieldmap:
metatile_attribute_fields:
- name: behavior
mask: 0x000001FF
provider:
header: include/constants/metatile_behaviors.h
prefix: MB_
skipped: [ MB_INVALID ]
- name: light_level
mask: 0x00000E00
default: 0
- name: room_type
mask: 0x0F000000
provider:
header: include/constants/room_types.h
prefix: ROOM_
format: defines-only
- name: layer_type
mask: 0x60000000
role: layer_type
The explicit list is the complete layout, taken exactly as declared. Two consequences of that:
Omitting the layer_type role field disables layer types, with no error or warning; the resolved-schema remark simply reports “layer type disabled”.
If the project’s source still declares a usable mask layout that differs from your list, Porytiles warns with a summary of the differences (fields present on only one side, masks that disagree, role mismatches). The explicit fields win, but the warning is a heads-up that your config disagrees with your own engine declarations.
Porytiles validates the result the same way regardless of where it came from:
every mask must be a single contiguous run of bits
masks must not overlap each other
field names must be unique
at most one field may carry
role: layer_type, and a role field takes nodefaultand noprovidera field may be named
layer_typeonly if it carries the role (the bare name is reserved for the CSV pin column)every default must fit its field’s width
every named constant a provider finds must fit the field’s width; the error shows the offending constant, and you must fix it by widening the field’s mask or adding the constant to the provider’s
skippedlist
Remember that the schema only controls how Porytiles packs the binary.
If you change the layout, your decomp codebase’s src/fieldmap.c must agree with it,
or the game will read garbage.
The safest workflow is the reverse:
change the decomp code first, then let inference pick up the change.
metatile_attributes.bin encoding
For completeness, the exact binary format Porytiles writes:
One attribute value per metatile, in the same order as
metatiles.binEach value is 1, 2, or 4 bytes (the resolved attribute size), stored in little-endian byte order
Each field’s value is shifted to its mask position and masked in; the layer type value (
normal= 0,covered= 1,split= 2) is packed through thelayer_typerole field’s mask like any other fieldOn decompile, Porytiles extracts exactly the schema fields (the role field decodes into the metatile’s layer type rather than a CSV column); bits covered by no field are dropped, not preserved
When parsing an existing binary, the file size must be a multiple of the attribute size, and the layer-type bits must decode to 0, 1, or 2
Common behaviors
Behaviors are the field you will touch most,
so here is a quick tour of frequently used constants from vanilla pokeemerald.
The authoritative list is always your own project’s include/constants/metatile_behaviors.h.
Names (and numeric values) vary between games and between Expansion versions,
which is why attributes.csv stores names instead of raw values.
Constant |
Effect |
|---|---|
|
No special behavior. The default for every metatile without a CSV row. |
|
Wild encounters, grass rustle animation. |
|
Taller grass variant with its own animation. |
|
Surfable water. |
|
Surfable deep water; Dive can be used here. |
|
Climbable with the Waterfall field move. |
|
Splash effect when walked through. |
|
Leaves footprints. |
|
Player slides across. |
|
Ledges; hop in the named direction. |
|
Blocks movement in the named direction. |
|
Door with an open/close animation on entry. |
|
Ladder; triggers the climbing warp animation. |
Tip
When you want to know what a behavior actually does, grep the decomp code for it.
MB_TALL_GRASS in src/ leads you straight to the encounter and animation code that checks the value.