Skip to content

All Trees Permanently Get Cut

voloved edited this page Mar 25, 2024 · 3 revisions

By devolov
Goal: Make it so cutting a tree once will keep it cut for the rest of the game.

cuttree

Removing cut trees permanently is much harder in Red than Emerald. In Red, the trees are not overworld sprites, but rather tiles. Whenever you cut a tree, the game finds the exact tile in front of you and replaces it with a cut version of that tile via a look-up-table.
In order to permanently cut, after loading a map but before drawing it, all of the trees on must be checked if they've been cut before, and if so, they should be replaced with their non-cut tile.

Create the file remove_cut_trees.asm in engine/overworld and find a place to put it in a bank that's not overstuffed in main.

I'm working off Red++ V3, so I placed it in field moves, but the vanilla ROM won't have that, so you're on your own to find a place for it.

----------------------------------- main.asm ---------------------------------
SECTION "field moves", ROMX,BANK[$38]

INCLUDE "engine/overworld/field_moves.asm"
INCLUDE "engine/wonder_trade.asm"
INCLUDE "engine/overworld/automatic_repel.asm"
INCLUDE "scripts/DayCareManScript.asm"
INCLUDE "engine/overworld/ferry_script.asm"
INCLUDE "engine/battle/physical_special_split.asm"
INCLUDE "scripts/move_deleter.asm"
INCLUDE "scripts/move_relearner.asm"
INCLUDE "scripts/move_tutor.asm"
+INCLUDE "engine/overworld/remove_cut_trees.asm"

Fill remove_cut_trees with this content. For any tree that exists in the game, its location is checked to CutTreeLocations, so if you have more trees, add them to that array and make the wCutTrees larger if you surpass 24 trees (the game has 23 trees by default).

------------------- engine/overworld/remove_cut_trees.asm -------------------
CutTreeLocations:
; first byte = The map the tree is on
; second byte = The Y coordinate of the block
; third byte = The X coordinate of the block
	db VIRIDIAN_CITY, 2, 7
	db VIRIDIAN_CITY, 11, 4
	db ROUTE_2, 11, 7
	db ROUTE_2, 26, 6
	db ROUTE_2, 30, 6
	db ROUTE_2, 34, 6
	db PEWTER_CITY, 2, 13
	db CERULEAN_CITY, 14, 9
	db ROUTE_8, 5, 20
	db ROUTE_8, 6, 14
	db ROUTE_9, 4, 2
	db VERMILION_CITY, 9, 7
	db ROUTE_10, 9, 4
	db ROUTE_10, 10, 4
	db ROUTE_10, 11, 4
	db ROUTE_10, 12, 4
	db CELADON_CITY, 10, 23
	db ROUTE_13, 2, 17
	db ROUTE_14, 16, 5
	db ROUTE_14, 13, 2
	db ROUTE_14, 21, 1
	db ROUTE_16, 4, 17
	db ROUTE_25, 1, 13
	db $FF ; list terminator

SetCutTreeFlags::
	ld a, [wYCoord]
	sra a
	ld d, a ; d holds the Y block loc
	ld a, [wXCoord]
	sra a
	ld e, a ; e holds the X block loc
	ld a, [wSpriteStateData1 + 9] ; player sprite's facing direction
	and a
	jr z, .down
	cp SPRITE_FACING_UP
	jr z, .up
	cp SPRITE_FACING_LEFT
	jr z, .left
; right	
	ld a, [wXBlockCoord]
	and a
	jr z, .findMapLoc
	inc e
	jr .findMapLoc
.down
	ld a, [wYBlockCoord]
	and a
	jr z, .findMapLoc
	inc d
	jr .findMapLoc
.up
	ld a, [wYBlockCoord]
	and a
	jr nz, .findMapLoc
	dec d
	jr .findMapLoc
.left
	ld a, [wXBlockCoord]
	and a
	jr nz, .findMapLoc
	dec e
.findMapLoc
	ld a,[wCurMap]
	ld b, a 
	ld hl, CutTreeLocations ; d = Y loc ; e = X loc ; b = map loc
	ld c, 0
	jr .loopfirst
.loopinctwo
	inc hl
.loopincone
	inc hl
	inc c
.loopfirst ; find the matching tile block in the array
	ld a, [hl]
	inc hl
	cp $ff
	ret z ; Not in list; return with a cleared carry flag
	cp b ; Compare map
	jr nz, .loopinctwo
	ld a, [hl]  ; hl +1 (Y loc)
	inc hl
	cp d
	jr nz, .loopincone
	ld a, [hl]  ; hl +2 (X loc)
	cp e
	jr nz, .loopincone
	ld b, 1
	ld hl, wCutTrees
	ld a, c
	cp 8
	jr c, .setByte
	; second byte of wCutTrees
	sub 8
	inc hl
	ld c, a
.setByte
	predef FlagActionPredef
	ret

RemoveAlreadyCutTrees::
	ld hl, CutTreeLocations
	ld c, 0
	jr .loopfirst
.loopinctwo
	inc hl
.loopincone
	inc hl
	inc c
.loopfirst ; find the matching tile block in the array
	ld a,[wCurMap]
	ld b, a
	ld a, [hl]
	inc hl
	cp $ff
	ret z ; Not in list; return with a cleared carry flag
	cp b ; Compare map
	jr nz, .loopinctwo
	
	ld d, [hl]  ; hl +1 (Y loc)
	inc hl
	ld e, [hl]  ; hl +2 (X loc)
	push hl
	ld hl, wCutTrees
	ld a, c
	ld [wTempCoins1], a ; temporarily store the current iteration
.iterByte
	cp 8
	jr c, .checkByte
	sub 8
	inc hl
	ld c, a
	jr .iterByte
.checkByte
	ld b, 2
	predef FlagActionPredef
	ld a, c
	and a
	ld a, [wTempCoins1]
	ld c, a
	pop hl
	jr z, .loopincone
	ld b, d
	ld c, e
	push hl
	predef FindTileBlock ; hl holds block ID at X,Y coord on the map
	ld a, [hl]
	ld [wNewTileBlockID], a
	push hl
	farcall FindTileBlockReplacementCut
	pop hl
	ld a, [wNewTileBlockID]
	ld [hl], a
	ld a, [wTempCoins1]
	ld c, a
	pop hl
	jr .loopincone

Change cut.asm so that every time cut is called, the flag for the tree being cut is set. Also, some logic is modified to allow for remove_cut_trees to read the CutTreeBlockSwaps look-up-table.

--------------------------- engine/overworld/cut.asm ---------------------------
index b4c8112d..6dacb7d8 100755
@@ -58,8 +58,9 @@ Cut2:: ; added for Field Move hack
 	ld [wUpdateSpritesEnabled], a
 	call InitCutAnimOAM
 	ld de, CutTreeBlockSwaps
 	call ReplaceTreeTileBlock
+	farcall SetCutTreeFlags
 	call RedrawMapView
 	callba AnimCut
 	ld a, $1
 	ld [wUpdateSpritesEnabled], a
@@ -243,19 +244,30 @@ ReplaceTreeTileBlock:
 .next
 	pop de
 	ld a, [hl]
 	ld c, a
+	call LoopForTileReplacement
+	ld [hl], a
+	ret
+
+LoopForTileReplacement: ; find the matching tile block in the array
.loop ; find the matching tile block in the array
 	ld a, [de]
 	inc de
 	inc de
 	cp $ff
 	ret z
 	cp c
	jr nz, .loop
 	dec de
 	ld a, [de] ; replacement tile block from matching array entry
-	ld [hl], a
+	ret
+
+FindTileBlockReplacementCut::
+	ld de, CutTreeBlockSwaps
+	ld a, [wNewTileBlockID]
+	ld c, a
+	call LoopForTileReplacement
+	ld [wNewTileBlockID], a
 	ret
 
 CutTreeBlockSwaps:
 ; first byte = tileset block containing the cut tree

The modifications to update_map are done to make it so we can now find the X,Y tile coordinates of our current location. Be careful here, as this is located in a Bank that's fairly filled.

----------------------- engine/overworld/update_map.asm -----------------------
index 8577b9e7..f5d84bc9 100644
@@ -2,30 +2,9 @@
 ; and redraws the map view if necessary
 ; b = Y
 ; c = X
 ReplaceTileBlock:
-	call GetPredefRegisters
-	ld hl, wOverworldMap
-	ld a, [wCurMapWidth]
-	add $6
-	ld e, a
-	ld d, $0
-	add hl, de
-	add hl, de
-	add hl, de
-	ld e, $3
-	add hl, de
-	ld e, a
-	ld a, b
-	and a
-	jr z, .addX
-; add width * Y
-.addWidthYTimesLoop
-	add hl, de
-	dec b
-	jr nz, .addWidthYTimesLoop
-.addX
-	add hl, bc ; add X
+	call FindTileBlock
 	ld a, [wNewTileBlockID]
 	ld [hl], a
 	ld a, [wCurrentTileBlockMapViewPointer]
 	ld c, a
@@ -123,4 +102,33 @@ CompareHLWithBC:
 	ret nz
 	ld a, l
 	sub c
 	ret
+
+; loads the current tile block into in [wNewTileBlockID]
+; b = Y
+; c = X
+; ret = hl = the ID of the currently loaded tile
+FindTileBlock:
+	call GetPredefRegisters
+	ld hl, wOverworldMap
+	ld a, [wCurMapWidth]
+	add $6
+	ld e, a
+	ld d, $0
+	add hl, de
+	add hl, de
+	add hl, de
+	ld e, $3
+	add hl, de
+	ld e, a
+	ld a, b
+	and a
+	jr z, .addX
+; add width * Y
+.addWidthYTimesLoop
+	add hl, de
+	dec b
+	jr nz, .addWidthYTimesLoop
+.addX
+	add hl, bc ; add X
+	ret

Add FindTileBlock into predefs.asm so your registers won't reset when calling it.

------------------------------ engine/predefs.asm ------------------------------
index abfaffd6..9751e99c 100755
@@ -151,4 +151,5 @@ PredefPointers::
 	add_predef DrawHP2
 	add_predef DisplayElevatorFloorMenu
 	add_predef OaksAideScript
 	add_predef TryFieldMove
+	add_predef FindTileBlock

In overworld, after the LoadTileBlockMap calls, add in RemoveAlreadyCutTrees.

------------------------------ home/overworld.asm ------------------------------
index 69da9692..50f53b62 100755
@@ -727,8 +727,9 @@ CheckMapConnections::
 ; Since the sprite set shouldn't change, this will just update VRAM slots at
 ; $C2XE without loading any tile patterns.
 	callba InitMapSprites
 	call LoadTileBlockMap
+	farcall RemoveAlreadyCutTrees
 	jp OverworldLoopLessDelay
 
 .didNotEnterConnectedMap
 	jp OverworldLoop
@@ -2191,8 +2192,9 @@ LoadMapData::
 	call LoadTextBoxTilePatterns
 	call LoadMapHeader
 	callba InitMapSprites ; load tile pattern data for sprites
 	call LoadTileBlockMap
+	farcall RemoveAlreadyCutTrees
 	call LoadTilesetTilePatternData
 	call LoadCurrentMapView
 
 	ld b, SET_PAL_OVERWORLD

Finally, add the wCutTrees flags into a part of wram that saves into sram. The amount of bytes needs to be at least as large as the amount of the trees in your hack divided by 8, rounded up.

----------------------------------- wram.asm -----------------------------------
index 88f0fc7e..2b88ebe2 100755
@@ -3363,10 +3367,13 @@ ENDU
 
 wTrainerHeaderPtr:: ; da30
 	ds 2
 
+wCutTrees::
+; Check CutTreeLocations for the indexes
+	ds 3
 ; unused?
-	ds 6
+	ds 3
 
 wOpponentAfterWrongAnswer:: ; da38
 ; the trainer the player must face after getting a wrong answer in the Cinnabar
 ; gym quiz

Route 9 Tree Popping

If all went well, you should be done. The only issue I found was that the tree to the right of Cerulean City in Route 9 is so close, that it can be seen when you're in Cerulean City and it'll pop out of existence when you walk to it. That's because the first three tiles of connected maps get loaded when you're on a map. An easy fix for this is to move the tree by one tile in Polished Map.

Here's what the issue looks like without a fix:
route9issue

Before: before

After: after

In CutTreeLocations, change the location accordingly:

CutTreeLocations:
; first byte = The map the tree is on
; second byte = The Y coordinate of the block
; third byte = The X coordinate of the block
	db VIRIDIAN_CITY, 2, 7
	db VIRIDIAN_CITY, 11, 4
	db ROUTE_2, 11, 7
	db ROUTE_2, 26, 6
	db ROUTE_2, 30, 6
	db ROUTE_2, 34, 6
	db PEWTER_CITY, 2, 13
	db CERULEAN_CITY, 14, 9
	db ROUTE_8, 5, 20
	db ROUTE_8, 6, 14
-	db ROUTE_9, 4, 2
+	db ROUTE_9, 4, 3
	db VERMILION_CITY, 9, 7
	db ROUTE_10, 9, 4
	db ROUTE_10, 10, 4
	db ROUTE_10, 11, 4
	db ROUTE_10, 12, 4
	db CELADON_CITY, 10, 23
	db ROUTE_13, 2, 17
	db ROUTE_14, 16, 5
	db ROUTE_14, 13, 2
	db ROUTE_14, 21, 1
	db ROUTE_16, 4, 17
	db ROUTE_25, 1, 13
	db $FF ; list terminator
Clone this wiki locally