Skip to content

Wall to wall carpeting in your room

MajorAgnostic edited this page Sep 17, 2024 · 9 revisions

There are many kinds of decorations available for your room (although most can only be obtained via Mystery Gift): beds, posters, dolls both big and small, game consoles, potted plants, and carpets.

Screenshot

The carpets are kind of limited. They only cover a portion of the room, and each kind of carpet needs its own map blocks, as we can see in Polished Map:

Screenshot

Let's say we want wall-to-wall carpeting, covering every square inch tile of the floor. Even if we defined more map blocks for each kind of carpet, we would still see the floorboards underneath the desk, table, and potted plant: those use their own tiles, and we can't change the individual pixels to match the carpet.

...Unless we can.

(This feature was inspired by Crystal Clear.)

Contents

  1. Define a map callback to run after loading tileset graphics
  2. Don't change blocks to place the carpet
  3. Define pixel masks for each tile to cover with carpet
  4. Associate each carpet decoration with its tile graphic
  5. Process the pixel masks to place carpet over certain tiles
  6. Use the carpet color for the floor tile

1. Define a map callback to run after loading tileset graphics

All the graphics for tiles and sprites are stored in video memory, or VRAM. This RAM can only be written to at certain times (namely during H-blank, V-blank, or when the LCD display is disabled); but all we need to know is that at some point, the tileset graphics get copied from ROM to RAM, one byte at a time. That's when we can (a) overwrite the floor tile with the carpet tile, and (b) overwrite specific pixels of the desk, table, and plant tiles, so it looks like the carpet texture is peeking out from under them.

Edit wram.asm:

 wHPPals:: ds PARTY_LENGTH
 wCurHPPal:: db

-	ds 7
+	ds 4
+
+wCarpetTile:: db
+wFloorTile:: db
+wCoveredTile:: db

 wSGBPals:: ds 48 ; cda9

We'll need those three bytes later, so we've defined them in some unused space in WRAM0.

Edit constants/map_setup_constants.asm:

 ; callback types
 	const_def 1
 	const MAPCALLBACK_TILES
 	const MAPCALLBACK_OBJECTS
 	const MAPCALLBACK_CMDQUEUE
 	const MAPCALLBACK_SPRITES
 	const MAPCALLBACK_NEWMAP
+	const MAPCALLBACK_GRAPHICS

Edit home/map.asm:

 LoadTilesetGFX::
 	...

 .load_roof
 	farcall LoadMapGroupRoof

 .skip_roof
 	xor a
 	ldh [hTileAnimFrame], a
+	ld [wCarpetTile], a
+	ld [wFloorTile], a
+
+	ld a, MAPCALLBACK_GRAPHICS
+	call RunMapCallback
 	ret

And edit maps/PlayersHouse2F.asm:

 PlayersHouse2F_MapScripts:
 	def_scene_scripts

 	def_callbacks
 	callback MAPCALLBACK_NEWMAP, .InitializeRoom
 	callback MAPCALLBACK_TILES, .SetUpTileDecorations
+	callback MAPCALLBACK_GRAPHICS, .RenderCarpet

 .DummyScene: ;unreferenced
 	end

 .InitializeRoom:
 	special ToggleDecorationsVisibility
 	setevent EVENT_TEMPORARY_UNTIL_MAP_RELOAD_8
 	checkevent EVENT_INITIALIZED_EVENTS
 	iftrue .SkipInitialization
 	jumpstd InitializeEventsScript
 	endcallback

 .SkipInitialization:
 	endcallback

 .SetUpTileDecorations:
 	special ToggleMaptileDecorations
 	endcallback

-	db 0, 0, 0 ; filler
+.RenderCarpet:
+	special CoverTilesWithCarpet
+	endcallback

Each type of callback is called at a specific point in the map setup process. If a map defines a MAPCALLBACK_TILES callback, it gets called after the blocks are all loaded, and has the opportunity to change them. Here, the .SetSpawn callback needs to run at that point in order to change blocks, like replacing the bed or adding a potted plant. It's also responsible for changing the carpet blocks, but of course we're about to edit that.

We just defined a new type of callback that runs right after the tileset graphics have all been loaded; and the player's room is using that type of callback to run special CoverTilesWithCarpet. When we get around to defining that, it will be like special ToggleMaptileDecorations, but for just the carpet instead of all the other tile-based decorations.

LoadTilesetGFX initializes [wCarpetTile] and [wFloorTile] to 0, and the CoverTilesWithCarpet will also update those values. (We'll see what they're used for later.)

Before that, let's remove the current method of placing the carpet.

2. Don't change blocks to place the carpet

As we saw earlier, the carpet took up three blocks of the map. We don't want to change those any more, since we'll be applying the carpet at graphics level.

Edit engine/overworld/decorations.asm:

 ToggleMaptileDecorations:
 	; tile coordinates work the same way as for changeblock
 	lb de, 0, 4 ; bed coordinates
 	ld a, [wDecoBed]
 	call SetDecorationTile
 	lb de, 7, 4 ; plant coordinates
 	ld a, [wDecoPlant]
 	call SetDecorationTile
 	lb de, 6, 0 ; poster coordinates
 	ld a, [wDecoPoster]
 	call SetDecorationTile
 	call SetPosterVisibility
-	lb de, 0, 0 ; carpet top-left coordinates
-	call PadCoords_de
-	ld a, [wDecoCarpet]
-	and a
-	ret z
-	call _GetDecorationSprite
-	ld [hl], a
-	push af
-	lb de, 0, 2 ; carpet bottom-left coordinates
-	call PadCoords_de
-	pop af
-	inc a
-	ld [hli], a ; carpet bottom-left block
-	inc a
-	ld [hli], a ; carpet bottom-middle block
-	dec a
-	ld [hl], a ; carpet bottom-right block
 	ret

That's the same ToggleMaptileDecorations routine that the player's room runs via MAPCALLBACK_TILES. It still changes blocks to place the bed, plant, and poster; but we've removed the way it placed three blocks for the carpet.

We're still not quite ready to define its new MAPCALLBACK_GRAPHICS counterpart, CoverTilesWithCarpet. Let's design our data structures first. If we start with a good data structure, the code to process that data will be easier to figure out.

3. Define pixel masks for each tile to cover with carpet

Create data/decorations/carpet_covered_tiles.asm:

+CarpetCoveredTiles:
+	; tile id, pixel mask
+	dbw $01, .Floor
+	dbw $30, .TableLeft
+	dbw $31, .TableMiddle
+	dbw $32, .TableRight
+	dbw $46, .PotLeft
+	dbw $56, .PotRight
+	dbw $07, .JumboPlantTopLeft
+	dbw $08, .JumboPlantTopRight
+	dbw $17, .JumboPlantBottomLeft
+	dbw $18, .JumboPlantBottomRight
+	dbw $27, .MagnaPlantTopLeft
+	dbw $28, .MagnaPlantTopRight
+	dbw $37, .MagnaPlantBottomLeft
+	dbw $38, .MagnaPlantBottomRight
+	dbw $47, .TropicPlantTopLeft
+	dbw $48, .TropicPlantTopRight
+	dbw $57, .TropicPlantBottomLeft
+	dbw $58, .TropicPlantBottomRight
+	db -1 ; end
+
+.Floor:
+	db %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111
+
+.TableLeft:
+	db %00000000, %00000000, %00000000, %00000001, %00000001, %10000001, %10000011, %11111111
+
+.TableMiddle
+	db %00000000, %00000000, %00000000, %11111111, %11111111, %11111111, %11111111, %11111111
+
+.TableRight
+	db %00000000, %00000000, %00000000, %10000000, %10000000, %10000001, %11000001, %11111111
+
+.PotLeft
+	db %11000000, %11000000, %11100000, %11100000, %11100000, %11110000, %11111000, %11111111
+
+.PotRight
+	db %00000011, %00000011, %00000111, %00000001, %00000000, %00000000, %00000001, %11111111
+
+.JumboPlantTopLeft
+	db %11111110, %11111100, %11111100, %11111000, %11100000, %11000000, %11000000, %11000000
+
+.JumboPlantTopRight
+	db %01111111, %00111111, %00111111, %00011111, %00000111, %00000011, %00000011, %00000011
+
+.JumboPlantBottomLeft
+	db %10000000, %10000000, %11000000, %10000000, %00000000, %00000000, %10000000, %11000000
+
+.JumboPlantBottomRight
+	db %00000001, %00000001, %00000011, %00000001, %00000000, %00000000, %00000001, %00000011
+
+.MagnaPlantTopLeft
+	db %10001100, %10000000, %10000000, %11000000, %10000000, %11000000, %10000000, %10000000
+
+.MagnaPlantTopRight
+	db %10100011, %00000001, %00000000, %00000000, %00000001, %00000001, %00000000, %00000001
+
+.MagnaPlantBottomLeft
+	db %00000000, %10000000, %10000000, %00000000, %10000000, %10000000, %10000000, %11100000
+
+.MagnaPlantBottomRight
+	db %00000000, %00000000, %00000000, %00000001, %00000001, %00000000, %00000000, %00000000
+
+.TropicPlantTopLeft
+	db %10011111, %10000111, %10000001, %00000000, %10000000, %10000000, %00000000, %00000000
+
+.TropicPlantTopRight
+	db %11111000, %11100000, %10000000, %00000001, %00000001, %00000000, %00000000, %00000011
+
+.TropicPlantBottomLeft
+	db %10000000, %10000000, %00000000, %10000000, %10000000, %00000000, %00000000, %11100000
+
+.TropicPlantBottomRight:
+	db %00000000, %00000000, %00000001, %00000001, %00000000, %00000000, %00000000, %00000011

This defines CarpetCoveredTiles, a table pairing up tile IDs with pixel masks. The masks are written on one line each for brevity, but their purpose is clearer when each byte gets its own line.

For example, we have this table entry:

	dbw $46, .PotLeft

And this pixel mask:

.PotLeft
	db %11000000
	db %11000000
	db %11100000
	db %11100000
	db %11100000
	db %11110000
	db %11111000
	db %11111111

Compare that with tile $46 in the players_room tileset:

Screenshot

The 1s correspond to floorboard pixels, and the 0s to potted plant pixels. We want to overwrite the pixels that correspond to 1s with carpet pixels instead.

This could actually be done with graphics instead! If we wrote:

.PotLeft
INCBIN "gfx/tilesets/carpet-masks/pot-left.1bpp"

And created gfx/tilesets/carpet-masks/pot-left.png:

gfx/tilesets/carpet-masks/pot-left.png

Then it would build exactly the same eight bytes of pixel mask data. Black is 1, white is 0.

4. Associate each carpet decoration with its tile graphic

Edit data/decorations/attributes.asm:

 DecorationAttributes:
 ; entries correspond to deco constants
 	...
-	decoration DECO_CARPET,  RED_CARPET,      SET_UP_CARPET,     EVENT_DECO_CARPET_1,                $08
-	decoration DECO_CARPET,  BLUE_CARPET,     SET_UP_CARPET,     EVENT_DECO_CARPET_2,                $0b
-	decoration DECO_CARPET,  YELLOW_CARPET,   SET_UP_CARPET,     EVENT_DECO_CARPET_3,                $0e
-	decoration DECO_CARPET,  GREEN_CARPET,    SET_UP_CARPET,     EVENT_DECO_CARPET_4,                $11
+	decoration DECO_CARPET,  RED_CARPET,      SET_UP_CARPET,     EVENT_DECO_CARPET_1,                $0d
+	decoration DECO_CARPET,  BLUE_CARPET,     SET_UP_CARPET,     EVENT_DECO_CARPET_2,                $1d
+	decoration DECO_CARPET,  YELLOW_CARPET,   SET_UP_CARPET,     EVENT_DECO_CARPET_3,                $2d
+	decoration DECO_CARPET,  GREEN_CARPET,    SET_UP_CARPET,     EVENT_DECO_CARPET_4,                $3d
 	...

Originally, each carpet was associated with a first block ID, and ToggleMaptileDecorations placed those blocks into the map. Now they're associated with tile IDs—as we saw in Polished Map, $0d is the red carpet tile, $1d is blue, and so on.

Now we can implement the special routine CoverTilesWithCarpet.

5. Process the pixel masks to place carpet over certain tiles

Edit data/special_pointers.asm:

 SpecialsPointers::
 	add_special WarpToSpawnPoint ; $0
 
 	...
 	add_special ActivateFishingSwarm ; $48
 	add_special ToggleMaptileDecorations
+	add_special CoverTilesWithCarpet
 	add_special ToggleDecorationsVisibility
 	...

Now that special CoverTilesWithCarpet script command will do the right thing.

Edit engine/overworld/decorations.asm again, adding this to the end of the file:

+CoverTilesWithCarpet::
+; Check if a carpet decoration is being used
+	ld a, [wDecoCarpet]
+	and a
+	ret z
+
+; [wCarpetTile] = the carpet tile ID from DecorationAttributes
+	ld c, a
+	call GetDecorationSprite
+	ld a, c
+	ld [wCarpetTile], a
+
+; [wFloorTile] = $01
+; This tile will use the palette of [wCarpetTile] instead
+	ld a, $01
+	ld [wFloorTile], a
+
+; Cover each tile listed in CarpetCoveredTiles 
+	ld hl, CarpetCoveredTiles
+.loop
+; Stop when we reach -1
+	ld a, [hli]
+	cp -1
+	ret z
+; [wCoveredTile] = the tile ID to cover with carpet
+	ld [wCoveredTile], a
+; bc = the mask for which pixels to cover
+	ld a, [hli]
+	ld c, a
+	ld a, [hli]
+	ld b, a
+; Copy the carpet pixels over the covered pixels
+	push hl
+	call CoverCarpetTile
+	pop hl
+	jr .loop
+
+CoverCarpetTile:
+; Copy pixels from tile #[wCarpetTile] to tile #[wCoveredTile]
+; based on the bitmask in bc.
+; Both tile IDs must be less than $80 (i.e. in bank 0).
+
+	push bc
+
+; de = covered tile in VRAM (destination)
+	ld a, [wCoveredTile]
+	ld hl, vTiles2
+	ld bc, 1 tiles
+	call AddNTimes
+	ld d, h
+	ld e, l
+
+; hl = carpet tile in VRAM (source)
+	ld a, [wCarpetTile]
+	ld hl, vTiles2
+	ld bc, 1 tiles
+	call AddNTimes
+
+	pop bc
+
+; bc = one byte before the pixel mask
+	dec bc
+
+; Cover all 8 rows of the tile
+rept TILE_WIDTH - 1
+	call .CoverRow
+endr
+.CoverRow:
+	inc bc ; advance to the next 1bpp mask byte
+	call .CoverHalfRow
+.CoverHalfRow:
+	push hl
+; h = carpet byte
+	ld a, [hl]
+	ld h, a
+; l = covered byte
+	ld a, [de]
+	ld l, a
+; h = carpet & mask
+	ld a, [bc]
+	and h
+	ld h, a
+; l = covered & ~mask
+	ld a, [bc]
+	cpl
+	and l
+	ld l, a
+; covered = (carpet & mask) | (covered & ~mask) = if mask then carpet else covered
+	or h
+	ld [de], a
+	pop hl
+	inc hl ; advance to the next 2bpp carpet byte
+	inc de ; advance to the next 2bpp covered byte
+	ret
+
+INCLUDE "data/decorations/carpet_covered_tiles.asm"

The comments already explain that code, but here's an overview. If the player's room has a carpet, it gets the tile associated with that carpet, and then processes the CarpetCoveredTiles table. Each table entry has a covered tile ID and a pixel mask; the carpet tile, covered tile, and pixel mask are all 8x8 pixels. They're all combined one row at a time: if the mask has a 1 bit, it uses the corresponding carpet pixel; otherwise it keeps the corresponding covered tile's pixel.

It also saves the tile ID $01 in [wFloorTile], but doesn't use it—yet. The covered tiles all have their own colors: the table is brown, potted plants are green, etc. But the floor tile was completely replaced by carpet (its mask is all 1s), and it should use whatever color the carpet tile is. That's our final step.

6. Use the carpet color for the floor tile

Edit engine/tilesets/map_palettes.asm:

 _SwapTextboxPalettes::
 	hlcoord 0, 0
 	decoord 0, 0, wAttrmap
 	ld b, SCREEN_HEIGHT
 .loop
 	push bc
 	ld c, SCREEN_WIDTH
 .innerloop
+	ld a, [wFloorTile]
+	cp [hl] ; if this tile is [wFloorTile]...
 	ld a, [hl]
+	jr nz, .not_floor
+	ld a, [wCarpetTile] ; ...use the palette of [wCarpetTile] instead
+.not_floor
 	push hl
 	srl a
 	jr c, .UpperNybble
 	...
 _ScrollBGMapPalettes::
 	ld hl, wBGMapBuffer
 	ld de, wBGMapPalBuffer
 .loop
+	ld a, [wFloorTile]
+	cp [hl] ; if this tile is [wFloorTile]...
 	ld a, [hl]
+	jr nz, .not_floor
+	ld a, [wCarpetTile] ; ...use the palette of [wCarpetTile] instead
+.not_floor
 	push hl
 	srl a
 	jr c, .UpperNybble
 	...

This is pretty self-explanatory. The _SwapTextboxPalettes and _ScrollBGMapPalettes routines both have loops over a series of tile IDs. If a tile ID is equal to [wFloorTile], we use the ID in [wCarpetTile] instead.

(The PRIORITY tiles tutorial edits these two routines, refactoring them into a single GetBGMapTilePalettes routine. That change is easily compatible with this carpet feature: there's just one ld a, [hl] operation to edit instead of two. The X and Y flip attribute tutorial removes these routines entirely; supporting the carpet tile color along with extended tile attributes is left as an exercise for the reader.)

So let's see our new wall-to-wall carpeting: type make, cheat in some decorations, and play!

Screenshot

Clone this wiki locally