Rewriting Paths EDIT: OHMYGOD IT WORKS!!!

MistSonata

Moderator
DISCLAIMER: This is a work in progress, and since I don't know everything about how NESmaker's engine updates tiles, I don't even know if this idea would even work. But if it does eventually work, it'll be fucking amazing.

I've been studying the path system for a little while, trying to understand how it works, and looking at the LoadMetaNametableWithPaths.asm file, it's actually kind of hard to comprehend. It's not laid out the way I thought it would be (or the way I thought it would in my mind), so I thought I'd sort of lay out how I'd write them if I were more experienced with 6502.

So, what the current handlePath script does is check the 8 metatiles around it to see if they match the path tile it's comparing to, and if it does match with the current path (stored in temp) it writes that into one of 8 variables prepared specifically (I think?) for the handlePath code.

My version would use one dedicated variable, and would store whether the neighboring tiles are paths (or should be considered paths, in the case of screen edges or "ignore path" tiles) and store them as bits, starting with bit 0 at the top center and going around clockwise from there.

I've found that, for each quadrant (NE, SE, SW, NW), they really only need to read 3 of those 8 bits. So once you've set all the bits, you can focus on one quadrant and only read the 3 bits it needs and determine which tile it should load.

newpathtilediagram.png

Now, as I plotted this out I noticed that the current path system has it so that each quadrant has 5 possible states that are split up between 8 different scenarios (shown above). And while I was thinking of how you would program it so that it reads both 000 and 010 as "exterior corner piece", I thought "why not use all 8 scenarios? It'd probably be easier".

So what if, for each path, we had NESmaker generate a table like this:
Code:
+---+---+---+---+---+---+---+---+---+
|   |000|001|010|011|100|101|110|111|
+---+---+---+---+---+---+---+---+---+
|NE:|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+
|SE:|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+
|SW:|   |   |   |   |   |   |   |   |   
+---+---+---+---+---+---+---+---+---+
|NW:|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+
With each slot being populated by a user-defined offset in the tool that picks which tile in the tileset to put in that quadrant for that specific situation.

This means that you could make a path with 32 unique tiles, but if you want to make it fewer than that, you can. You can make it behave exactly like the current paths do now, or you could make it so that you have two unique tiles for the left and top edges, but have the bottom and right edges use the same tile. You could have it so that instead of using an exterior corner piece in situation 010, you could have it connect diagonally to the path (the scenario for the SW quadrant in the path diagonal would also be 010). You could have it so that instead of using a straight edge in scenarios 110 and 011 you could have it start curving toward the direction the path is going. You could (I think?) have paths share tiles with each other while still behaving as though they're separate paths. You could even have all the offsets on the table point to the same tile so that it's just repeating the same tile over and over (not sure why you would, but you could is the point I'm making).

So I started trying to replicate the handlePath code, but do it so that it used the method described above.

Code:
handlePath:
	LDA #$00
	STA Path_Neighbors ;; This the variable that we're going to store the surrounding paths in.
	
	;; Bit 0 at the top center, and going clockwise around the tile from there.
	;; 1= Same Path in this slot, 0= Not the same path in this slot. Pretty simple, yo.
	
	;; +---+---+---+
	;; | 7 | 0 | 1 |
	;; +---+---+---+
	;; | 6 |   | 2 |
	;; +---+---+---+
	;; | 5 | 4 | 3 |
	;; +---+---+---+
	
	TYA
	STA nt_index_hold ;; nt_index_hold is how we can now restore y if we have to 
				;; corrupt it.
		

	;; checkTopEdgePath:
	CPY #$10
	BNE checkLeftEdgePath
	LDA #%10000011
	STA Path_Neighbors ;; If the path is along the top row, set the top bits to 1 so that the path connects to the next screen

checkLeftEdgePath:
	TYA
	AND #%00001111
	BNE checkRightEdgePath
	
	LDA Path_Neighbors
	ORA #%11100000
	STA Path_Neighbors
	CMP #%11100011
	BEQ checkCRPathNeighbor ;; If both top and left sides are on the edge, there's no sense checking the bottom or right edges or re-checking the top row

checkRightEdgePath:
	TYA
	AND #%00001111
	BNE checkBottomEdgePath ;; (btw I'm only assuming this and the left edge code checks the edges, I actually have no idea how this works)
	
	LDA Path_Neighbors
	ORA #%00001110
	STA Path_Neighbors
	CMP #%10001111
	BEQ checkBCPathNeighbor ;; If both top and right sides are on the edge, there's no sense checking the bottom edge or the top and right tiles

checkBottomEdgePath:
	LDA nt_index_hold
	CMP #$E0
	BCC checkTLPathNeighbor
	LDA Path_Neighbors
	ORA #%00111000
	STA Path_Neighbors ;; Now that edges are taken care of, we can fill in the blanks

checkTLPathNeighbor: ;bit 7 - Checking this first so that we go from top to bottom (and make it easier to skip if top edge was set)
	;; Check the Top Left (bit 7) metatile for path. If it's the same, set that bit to 1.

	LDA Path_Neighbors
	ORA #%10000000
	STA Path_Neighbors
checkTCPathNeighbor: ;bit 0
	;; Check the Top Center (bit 0) metatile for path. If it's the same, set that bit to 1.
	
	BNE checkTRPathNeighbor
	LDA Path_Neighbors
	ORA #%00000001
	STA Path_Neighbors

checkTRPathNeighbor: ;bit 1
	;; Check the Top Right (bit 1) metatile for path. If it's the same, set that bit to 1.

	LDA Path_Neighbors
	ORA #%00000010
	STA Path_Neighbors
checkCRPathNeighbor: ;bit 2
	;; Check the Center Right (bit 2) metatile for path. If it's the same, set that bit to 1.

	LDA Path_Neighbors
	ORA #%00000100
	STA Path_Neighbors
checkBRPathNeighbor: ;bit 3
	;; Check the Bottom Right (bit 3) metatile for path. If it's the same, set that bit to 1.
	
	LDA Path_Neighbors
	ORA #%00001000
	STA Path_Neighbors
	
	AND #%01110000
	CMP #%01110000
	BEQ skipNeighborCheck ;This'll save a little time if it triggered the bottom edge check
checkBCPathNeighbor: ;bit 4
	;; Check the Bottom Center (bit 4) metatile for path. If it's the same, set that bit to 1.
	
	LDA Path_Neighbors
	ORA #%00010000
	STA Path_Neighbors
checkBLPathNeighbor: ;bit 5
	;; Check the Bottom Left(bit 5 metatile for path. If it's the same, set that bit to 1.

	LDA Path_Neighbors
	ORA #%00100000
	STA Path_Neighbors
checkCLPathNeighbor: ;bit 6
	;; Check the Center Left (bit 6) metatile for path. If it's the same, set that bit to 1.

	LDA Path_Neighbors
	ORA #%01000000
	STA Path_Neighbors
skipNeighborCheck:

;; ======= Once we have our Path_Neighbors bits loaded, we can get the values for each quadrant and grab their offsets from a table =======
;; First we need to grab bits 0, 1, and 2 for the NE quadrant for the table offset (0-7) and then load the chr offset stored there into that quadrant.
;; LSR once, and if the carry is set, write 1 into bit 7 (this is important, as the NW quadrant needs the original value in bit 0) then LSR again, then do the same as above for the SE quadrant
;; LSR twice and record bits 0, 1, and 2 for the SW quadrant
;; Do the same as above for the NW quadrant (the original bit 0 should now be in bit 2)
;; You should now have all your quadrants set! :D

;; End handlePath

The code here is incomplete and I'm not even sure how parts of it work, but I'd love to hear feedback!

TL:DR - If my idea works, paths could be much more customizable with up to 32 unique tiles per path (the amount of tiles dedicated to paths would still be limited), opening up a wide range of versatility and the possibility for creative applications!
 

RadJunk

Administrator
Staff member
Hm - I mean, excellent work with playing with the path unpacking code! These are the sort of solutions I think users will end up coming up with, and offer up in the code section (once working) as optional things to add into the engine! So cool to see!

And the call to draw paths is pretty consistent, so if this single part of it (handle path) were to change in the way you're describing, it should still work without breaking anything. Obviously, this is advanced usage, and cool to see some of these ideas kicking around!
 

MistSonata

Moderator
So... I'm actually still incredibly stunned right now, and really hoping this isn't a dream, but I did it. It works.

Here's what the path looks like in the tool:
I1560tz.png

L5uJKYA.png


(This is what the path file looks like, keep in mind, it uses two path rows, but it's only one path)
ShY335X.png


And here's what it looks like when it's exported:
QYaGbyE.png


Here's the code I used to replace handlePaths:
Code:
handlePath:
	LDA #$00
	STA Path_Neighbors ;; This the variable that we're going to store the surrounding paths in.
	
	;; Bit 0 at the top center, and going clockwise around the tile from there.
	;; 1= Same Path in this slot, 0= Not the same path in this slot. Pretty simple, yo.
	
	;; +---+---+---+
	;; | 7 | 0 | 1 |
	;; +---+-+-+---+
	;; | 6 |-+-| 2 |
	;; +---+-+-+---+
	;; | 5 | 4 | 3 |
	;; +---+---+---+
	
	STY nt_index_hold ;; nt_index_hold is how we can now restore y if we have to 
				;; corrupt it.
		

	;; checkTopEdgePath:
	CPY #$10
	BNE checkLeftEdgePath
	LDA #%10000011
	STA Path_Neighbors ;; If the path is along the top row, set the top bits to 1 so that the path connects to the next screen

checkLeftEdgePath:
	TYA
	AND #%00001111
	BNE checkRightEdgePath
	
	LDA Path_Neighbors
	ORA #%11100000
	STA Path_Neighbors
	;CMP #%11100011
	;BEQ checkCRPathNeighbor ;; If both top and left sides are on the edge, there's no sense checking the bottom or right edges or re-checking the top row

checkRightEdgePath:
	INY
	TYA
	AND #%00001111
	BNE checkBottomEdgePath 
	
	LDA Path_Neighbors
	ORA #%00001110
	STA Path_Neighbors
	;CMP #%10001111
	;BEQ checkBCPathNeighbor ;; If both top and right sides are on the edge, there's no sense checking the bottom edge or the top and right tiles

checkBottomEdgePath:
	LDA nt_index_hold
	CMP #$E0
	BCC checkTLPathNeighbor
	LDA Path_Neighbors
	ORA #%00111000
	STA Path_Neighbors ;; Now that edges are taken care of, we can fill in the blanks

checkTLPathNeighbor: ;bit 7 - Checking this first so that we go from top to bottom (and make it easier to skip if top edge was set)
	LDA nt_index_hold 
	SEC
	SBC #$11 ; one row up, one value to the left.
	TAY
	LDA	(temp16),y
	CMP temp
	BNE checkTCPathNeighbor

	LDA Path_Neighbors
	ORA #%10000000
	STA Path_Neighbors
	
checkTCPathNeighbor: ;bit 0
	INY
	LDA (temp16),y ;; this is for top center.
	CMP temp
	BNE checkTRPathNeighbor
	
	LDA Path_Neighbors
	ORA #%00000001
	STA Path_Neighbors

checkTRPathNeighbor: ;bit 1
	INY
	LDA (temp16),y ;; this is for top center.
	CMP temp
	BNE checkCRPathNeighbor

	LDA Path_Neighbors
	ORA #%00000010
	STA Path_Neighbors
	
checkCRPathNeighbor: ;bit 2 - one of the edge checks skip to here
	LDY nt_index_hold
	INY
	LDA (temp16),y
	CMP temp
	BNE checkBRPathNeighbor

	LDA Path_Neighbors
	ORA #%00000100
	STA Path_Neighbors
	
checkBRPathNeighbor: ;bit 3
	TYA
	CLC
	ADC #$10 ;shift down one row
	TAY
	LDA (temp16),y
	CMP temp
	BNE checkBCPathNeighbor
	
	LDA Path_Neighbors
	ORA #%00001000
	STA Path_Neighbors
	
checkBCPathNeighbor: ;bit 4 - one of the edge checks skip to here
	AND #%01110000
	CMP #%01110000
	BEQ skipNeighborCheck ;This'll save a little time if it triggered the bottom edge check
	
	DEY
	LDA (temp16),y
	CMP temp
	BNE checkBLPathNeighbor
	
	LDA Path_Neighbors
	ORA #%00010000
	STA Path_Neighbors
	
checkBLPathNeighbor: ;bit 5
	DEY
	LDA (temp16),y
	CMP temp
	BNE checkCLPathNeighbor

	LDA Path_Neighbors
	ORA #%00100000
	STA Path_Neighbors
	
checkCLPathNeighbor: ;bit 6
	LDY nt_index_hold
	DEY
	LDA (temp16),y
	CMP temp
	BNE skipNeighborCheck

	LDA Path_Neighbors
	ORA #%01000000
	STA Path_Neighbors
	
skipNeighborCheck:
	LDA Path_Neighbors
	AND #%00000111
	TAY
	LDA PathFinder,y
	CLC
	ADC temp
	STA	updateTile_01
	
	
	LDA Path_Neighbors
	LSR
	BCC leaveBit7alone
	ORA #%10000000
leaveBit7alone:
	LSR
	STA Path_Neighbors
	AND #%00000111
	CLC
	ADC #$08
	TAY
	LDA PathFinder,y
	CLC
	ADC temp
	STA	updateTile_03

	LDA Path_Neighbors
	LSR
	LSR
	STA Path_Neighbors
	AND #%00000111
	CLC
	ADC #$10
	TAY
	LDA PathFinder,y
	CLC
	ADC temp
	STA	updateTile_02
	
	LDA Path_Neighbors
	LSR
	LSR
	AND #%00000111
	CLC
	ADC #$18
	TAY
	LDA PathFinder,y
	CLC
	ADC temp
	STA	updateTile_00
	
	LDY nt_index_hold

	RTS

And this is the database file I used called pathfinder.txt (I stuck it into the beginning of Bank00.asm and it seemed to work):
Code:
;;PathFinder
.db $04, $16, $14, $1C, $03, $0d, $13, $00 ;; NE
.db $0a, $09, $1a, $19, $06, $0e, $1e, $0f ;; SE
.db $07, $05, $17, $1d, $08, $0c, $18, $10 ;; SW
.db $01, $02, $11, $12, $15, $0b, $1b, $1f ;; NW

That's for the 32 tile path, though. If you still want to use the old path system with only 16 tiles, you could replace it with this:
Code:
;;PathFinder(original)
.db $04, $06, $04, $06, $03, $0d, $03, $00 ;; NE
.db $0a, $09, $0a, $09, $06, $0e, $06, $0f ;; SE
.db $07, $05, $07, $05, $08, $0c, $08, $00 ;; SW
.db $01, $02, $01, $02, $05, $0b, $05, $0f ;; NW

The great thing about this is that (I think) it makes the handlePath code smaller and more compact while expanding its customizability. I don't know how to count cycles or check how much space it takes up compared to the original code, but it uses fewer custom variables.

Now, the drawback to this is that EVERY path you make has to behave the same way. Making it so that it would change pathfinder files depending on which path you have in would be a new beast entirely, and I'm pretty sure it would require tweaking the tool itself instead of just the code.

However, even as it is right now, I think this is pretty damn amazing.


EDIT: Just checked, according to the demo.txt file, the handlePath subroutine itself (not considering variables or the inclusion of pathfinder.txt) saves 287 bytes. (both handlePath subroutines starting at 0EB58, mine ends at 0EC79 and the current one ends at 0ED99) I don't know how that translates to cycles though. If someone more knowledgeable wants to check that they can.

It also removes the need for all of the 12 path checker variables, as I don't think they're used for anything else but handlePath, and uses only the one byte Path_Neighbors variable.
 

Attachments

  • game_005.png
    game_005.png
    67.4 KB · Views: 3
  • fb5159787b99159521af47d06d5513f1.png
    fb5159787b99159521af47d06d5513f1.png
    42.1 KB · Views: 2

RadJunk

Administrator
Staff member
Simply fantastic! This is amazing to see.

Do me a favor - put this in the *CODE* section as well, and really give a full explanation (or even video demonstration) of how it works versus the original. It'll be great that, at launch, people can see how to use community created code to do cooler things!
 

stevenshepherd

New member
Oh man this is awesome! I have been thinking about how to create more gradient and tapered edges on things (mountains, crumbled brick walls, etc) without wasting a bunch of tiles. Between this and the fact that, if I remember correctly, the bank restrictions (i.e., all tilesets for a screen have to come from the same bank, meaning you would likely need to make tons of duplicates) are gone, this is huge.
 

darkhog

New member
Since it's obviously superior to the way paths works, could it be integrated into the base engine (either as a switchable option or fully integrated)?

Or, at the very least provide a way to render paths in the tool properly when used in this mode.
 

RadJunk

Administrator
Staff member
Darkhog - as with everything else, NESmaker has a vanilla solution. This is a solution that may work better for some. It's just a matter of copying and pasting it to the right place in code. Then it will work with NESmaker for anyone who wishes to use it this way. That's integration.

As for rendering 'in tool' these sorts of solutions, that might be trickier, as inevitably lots of these custom solutions will end up popping up, and it will be hard to keep up. Not impossible, but we have a laundry list a mile long of things to integrate, so it'd go in cure. I don't think what you see in August will have this sort of update in-tool.
 

Mihoshi20

Member
This is a very impressive analysis and demonstration of the flexibility in the pathing system and how it writes to the tables. Fantastic work!
 

darkhog

New member
TheNew8bitHeroes said:
Darkhog - as with everything else, NESmaker has a vanilla solution. This is a solution that may work better for some. It's just a matter of copying and pasting it to the right place in code. Then it will work with NESmaker for anyone who wishes to use it this way. That's integration.

As for rendering 'in tool' these sorts of solutions, that might be trickier, as inevitably lots of these custom solutions will end up popping up, and it will be hard to keep up. Not impossible, but we have a laundry list a mile long of things to integrate, so it'd go in cure. I don't think what you see in August will have this sort of update in-tool.

Or you could simply add an editor plugin system so the add-ons dev could write editor plugins to go along with them to make sure it's rendered in the tool properly as well.
 

MistSonata

Moderator
darkhog said:
TheNew8bitHeroes said:
Darkhog - as with everything else, NESmaker has a vanilla solution. This is a solution that may work better for some. It's just a matter of copying and pasting it to the right place in code. Then it will work with NESmaker for anyone who wishes to use it this way. That's integration.

As for rendering 'in tool' these sorts of solutions, that might be trickier, as inevitably lots of these custom solutions will end up popping up, and it will be hard to keep up. Not impossible, but we have a laundry list a mile long of things to integrate, so it'd go in cure. I don't think what you see in August will have this sort of update in-tool.

Or you could simply add an editor plugin system so the add-ons dev could write editor plugins to go along with them to make sure it's rendered in the tool properly as well.

I don't know if "simply" is the word here, having an "editor plugin system" would almost certainly be incredibly complicated, and besides there's not really anything stopping the creation of separate "editors", except for in cases where NESmaker overwrites files with its own data, but there's nothing you can really do about that right now anyway.
 

darkhog

New member
MistSonata said:
darkhog said:
TheNew8bitHeroes said:
Darkhog - as with everything else, NESmaker has a vanilla solution. This is a solution that may work better for some. It's just a matter of copying and pasting it to the right place in code. Then it will work with NESmaker for anyone who wishes to use it this way. That's integration.

As for rendering 'in tool' these sorts of solutions, that might be trickier, as inevitably lots of these custom solutions will end up popping up, and it will be hard to keep up. Not impossible, but we have a laundry list a mile long of things to integrate, so it'd go in cure. I don't think what you see in August will have this sort of update in-tool.

Or you could simply add an editor plugin system so the add-ons dev could write editor plugins to go along with them to make sure it's rendered in the tool properly as well.

I don't know if "simply" is the word here, having an "editor plugin system" would almost certainly be incredibly complicated, and besides there's not really anything stopping the creation of separate "editors", except for in cases where NESmaker overwrites files with its own data, but there's nothing you can really do about that right now anyway.

Yes, we could write entire new editor, but why reinvent the wheel? Yes, the plugin system would take some work and I don't expect it in 1.0, but the potential of it far outweighs the work needed to implement it.
 
Top Bottom