dev, computing and games

To make it so you don't need to water your plants in Harvest Moon for Super Nintendo, use the PAR codes

82A8ACEA
82A8ADEA

Explanation below.

The game 'Harvest Moon' for Super Nintendo has fun elements, but also repetitive ones. One of them that wore on me was having to water your plants. If you water at night, it's not even a skill-based action mechanic since you can restore your energy and night lasts forever. I wanted to experience the game and see its content, but not have the repetitive action of watering.

Idea: patch the game so that watering is not needed

First approach was to take memory dumps before and after watering a tile, and diff them. Seems ok. But, the diffs were too noisy and I didn't know what I was looking for. How were the farm tiles stored? Was it even one byte per tile, one byte per pair of tiles, or something else entirely? I didn't even know that part yet, so I gave up on it.

Instead, I tried something way dumber- keep watering a tile with a view of live memory open, and watch for changes. I had an idea of what the storage could be- one byte per tile, arranged sequentially, so I looked for that. And I planted some test plants in an observable pattern. This plus exploiting timing of changes made it way easier to discern signal from noise and I found it.

There were not one, but two places the tile data lived. For the tile I'm standing near, the first was at 7E1100 and that one I found first. When I left the farm and came back I saw that the data got wiped and restored, so that meant it's a cache and not the primary copy.

Looking for a dupe of it in memory, I found that at 7EAC30, more likely to be the primary copy.

From experimenting I found this out about the tile storage:

  • It is one byte per tile
  • Tiles are stored sequentially. Nothing too crazy.

And as for what tile value means what thing, I found

Tile ValueWhat It Means
0x00Untilled, unwatered ground
0x01Untilled, unwatered ground (different graphic from above)
0x07Tilled, unwatered ground
0x08Tilled, watered ground
0x58Unwatered potato seed
0x59Watered potato seed
0x68Unwatered turnip seed
0x69Watered turnip seed

(Not exhaustive.)

From the data points there are, watering a tile simply increments its value by 1. So when it rains, something must iterate through all the tiles and increment their value by 1.

To find out what that is, I set a break-on-write of 0x7EAC32, the location of the top-left plant, looking for the value to change from 0x58 to 0x59. This took me right into this code

(Paste below is multiple debugging transcripts spliced together so don't put too much stock in the reg values.)

void ProcessWeather()
...
$82/8390 8F 1E 1F 7F STA $7F1F1E[$7F:1F1E]   A:0400 X:0400 Y:0400 P:eNvMxdizc
$82/8394 22 D6 89 82 JSL $8289D6[$82:89D6]   A:0400 X:0400 Y:0400 P:eNvMxdizc

// Call UpdateFarmTiles, transcribed below
$82/8398 22 11 A8 82 JSL $82A811[$82:A811]   A:0400 X:0400 Y:0400 P:eNvMxdizc

$82/839C 22 09 82 82 JSL $828209[$82:8209]   A:0400 X:0400 Y:0400 P:eNvMxdizc
...


void UpdateFarmTiles() - $82/A811
// Preconditions:
//    Weather is stored at $7E0196. 0x0 means sunny, 0x2 means rainy.
//    Farm data is stored around $7EAC30.
// This function is called when you sleep, no matter the weather or if you save.

$82/A811 E2 20       SEP #$20                A:0007 X:000E Y:0002 P:envmxdizC
$82/A813 C2 10       REP #$10                A:0007 X:000E Y:0002 P:envMxdizC
$82/A815 A9 04       LDA #$04                A:0007 X:000E Y:0002 P:envMxdizC
$82/A817 8D 81 01    STA $0181  [$00:0181]   A:0004 X:000E Y:0002 P:envMxdizC
$82/A81A C2 20       REP #$20                A:0004 X:000E Y:0002 P:envMxdizC
$82/A81C A0 00 00    LDY #$0000              A:0004 X:000E Y:0002 P:envmxdizC
$82/A81F A2 00 00    LDX #$0000              A:01E0 X:0400 Y:01E0 P:eNvmxdizc

StartProcessingTile:
$82/A822 5A          PHY                     A:00D0 X:00D0 Y:01D0 P:eNvmxdizc
$82/A823 DA          PHX                     A:00D0 X:00D0 Y:01D0 P:eNvmxdizc
$82/A824 86 82       STX $82    [$00:0082]   A:00D0 X:00D0 Y:01D0 P:eNvmxdizc
$82/A826 84 84       STY $84    [$00:0084]   A:00D0 X:00D0 Y:01D0 P:eNvmxdizc
$82/A828 20 3C B1    JSR $B13C  [$82:B13C]   A:00D0 X:00D0 Y:01D0 P:eNvmxdizc
$82/A82B E2 20       SEP #$20                A:074D X:074D Y:01D0 P:envmxdizc

// Load the state for a tile in your farm. We look at just 1 byte
$82/A82D BF E6 A4 7E LDA $7EA4E6,x[$7E:AC33] A:074D X:074D Y:01D0 P:envMxdizc 

; Do various things for the different tile types.
$82/A831 D0 03       BNE $03    [$A836]      A:0758 X:074D Y:01D0 P:envMxdizc
$82/A836 C9 03       CMP #$03                A:0758 X:074D Y:01D0 P:envMxdizc
$82/A838 B0 03       BCS $03    [$A83D]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A83A 4C 1B A9    JMP $A91B  [$82:A91B]   A:0701 X:074F Y:01D0 P:eNvMxdizc
$82/A83D C9 A0       CMP #$A0                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A83F 90 03       BCC $03    [$A844]      A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A844 C9 06       CMP #$06                A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A846 D0 03       BNE $03    [$A84B]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A84B C9 07       CMP #$07                A:0758 X:074D Y:01D0 P:envMxdizC ; Is tilled soil?
$82/A84D F0 55       BEQ $55    [$A8A4]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A84F C9 08       CMP #$08                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A851 D0 03       BNE $03    [$A856]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A856 C9 1E       CMP #$1E                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A858 F0 4A       BEQ $4A    [$A8A4]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A85A C9 1F       CMP #$1F                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A85C D0 03       BNE $03    [$A861]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A861 C9 1D       CMP #$1D                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A863 D0 03       BNE $03    [$A868]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A868 C9 20       CMP #$20                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A86A B0 03       BCS $03    [$A86F]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A86F C9 39       CMP #$39                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A871 D0 03       BNE $03    [$A876]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A876 C9 53       CMP #$53                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A878 D0 03       BNE $03    [$A87D]      A:0758 X:074D Y:01D0 P:envMxdizC
$82/A87D C9 61       CMP #$61                A:0758 X:074D Y:01D0 P:envMxdizC
$82/A87F D0 03       BNE $03    [$A884]      A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A884 C9 6F       CMP #$6F                A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A886 D0 03       BNE $03    [$A88B]      A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A88B C9 79       CMP #$79                A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A88D D0 03       BNE $03    [$A892]      A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A892 C9 7C       CMP #$7C                A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A894 D0 03       BNE $03    [$A899]      A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A899 C9 70       CMP #$70                A:0758 X:074D Y:01D0 P:eNvMxdizc
$82/A89B B0 69       BCS $69    [$A906]      A:0758 X:074D Y:01D0 P:eNvMxdizc

$82/A89D 29 01       AND #$01                A:0758 X:074D Y:01D0 P:eNvMxdizc	; Mask 
$82/A89F F0 03       BEQ $03    [$A8A4]      A:0700 X:074D Y:01D0 P:envMxdiZc
...

$82/A8A4 C2 20       REP #$20                A:0700 X:074D Y:01D0 P:envMxdiZc

$82/A8A6 AD 96 01    LDA $0196  [$00:0196]   A:0700 X:074D Y:01D0 P:envmxdiZc	; Load weather
										; 0x0 means sunny.
										; 0x2 means rainy.

$82/A8A9 29 02 00    AND #$0002              A:0002 X:074D Y:01D0 P:envmxdizc

; If not rainy, skip ahead--
$82/A8AC F0 03       BEQ $03    [$A8B1]      A:0002 X:074D Y:01D0 P:envmxdizc	

; If it is rainy, goto IncrementTileValue to mark the tile as watered.					
$82/A8AE 4C 06 A9    JMP $A906  [$82:A906]   A:0002 X:074D Y:01D0 P:envmxdizc
...

IncrementTileValue:
; This is a common path for all kinds of tile incrementing, it's not just for rain.

$82/A906 E2 20       SEP #$20                A:0002 X:074D Y:01D0 P:envmxdizc
; Load early-out cond
$82/A908 AF 19 1F 7F LDA $7F1F19[$7F:1F19]   A:0002 X:074D Y:01D0 P:envMxdizc	
$82/A90C C9 03       CMP #$03                A:0000 X:074D Y:01D0 P:envMxdiZc	; 
$82/A90E F0 59       BEQ $59    [$A969]      A:0000 X:074D Y:01D0 P:eNvMxdizc	;

; Load tile value
$82/A910 BF E6 A4 7E LDA $7EA4E6,x[$7E:AC33] A:0000 X:074D Y:01D0 P:eNvMxdizc	

; Apply 'watered' status
$82/A914 1A          INC A                   A:0058 X:074D Y:01D0 P:envMxdizc	

WriteRainEffect:
; X=0x74C means just to the right of shipping bin.
; Value of 0x59 means 'watered'.
$82/A915 9F E6 A4 7E STA $7EA4E6,x[$7E:AC32] A:0059 X:074C Y:01D0 P:envMxdizc											
		
; Goto DoneProcessingTile.								
$82/A919 80 4E       BRA $4E    [$A969]      A:0059 X:074C Y:01D0 P:envMxdizc	
...

OnUntilledSoil:
$82/A91B E2 20       SEP #$20                A:0701 X:074F Y:01D0 P:eNvMxdizc
$82/A91D AF 19 1F 7F LDA $7F1F19[$7F:1F19]   A:0701 X:074F Y:01D0 P:eNvMxdizc
$82/A921 C9 02       CMP #$02                A:0700 X:074F Y:01D0 P:envMxdiZc
$82/A923 F0 44       BEQ $44    [$A969]      A:0700 X:074F Y:01D0 P:eNvMxdizc
$82/A925 C9 03       CMP #$03                A:0700 X:074F Y:01D0 P:eNvMxdizc
$82/A927 F0 40       BEQ $40    [$A969]      A:0700 X:074F Y:01D0 P:eNvMxdizc
$82/A929 AF 1B 1F 7F LDA $7F1F1B[$7F:1F1B]   A:0700 X:074F Y:01D0 P:eNvMxdizc
$82/A92D 29 03       AND #$03                A:0706 X:074F Y:01D0 P:envMxdizc
$82/A92F D0 38       BNE $38    [$A969]      A:0702 X:074F Y:01D0 P:envMxdizc	; Goto DoneProcessingTile
...

DoneProcessingTile:
$82/A969 C2 30       REP #$30                A:0059 X:074C Y:01D0 P:envMxdizc
$82/A96B FA          PLX                     A:0059 X:074C Y:01D0 P:envmxdizc
$82/A96C 7A          PLY                     A:0059 X:00C0 Y:01D0 P:envmxdizc
$82/A96D 8A          TXA                     A:0059 X:00C0 Y:01D0 P:envmxdizc
$82/A96E 18          CLC                     A:00C0 X:00C0 Y:01D0 P:envmxdizc
$82/A96F 69 10 00    ADC #$0010              A:00C0 X:00C0 Y:01D0 P:envmxdizc
$82/A972 AA          TAX                     A:00D0 X:00C0 Y:01D0 P:envmxdizc
$82/A973 E0 00 04    CPX #$0400              A:00D0 X:00D0 Y:01D0 P:envmxdizc
$82/A976 F0 03       BEQ $03    [$A97B]      A:00D0 X:00D0 Y:01D0 P:eNvmxdizc
$82/A978 4C 22 A8    JMP $A822  [$82:A822]   A:00D0 X:00D0 Y:01D0 P:eNvmxdizc

DoneProcessingField:
$82/A97B 98          TYA                     A:0400 X:0400 Y:01D0 P:envmxdiZC
$82/A97C 18          CLC                     A:01D0 X:0400 Y:01D0 P:envmxdizC
$82/A97D 69 10 00    ADC #$0010              A:01D0 X:0400 Y:01D0 P:envmxdizc
$82/A980 A8          TAY                     A:01E0 X:0400 Y:01D0 P:envmxdizc
$82/A981 C0 00 04    CPY #$0400              A:01E0 X:0400 Y:01E0 P:envmxdizc
$82/A984 F0 03       BEQ $03    [$A989]      A:01E0 X:0400 Y:01E0 P:eNvmxdizc
$82/A986 4C 1F A8    JMP $A81F  [$82:A81F]   A:01E0 X:0400 Y:01E0 P:eNvmxdizc
...

This provides enough information to understand how the 'watered' status gets applied. So to apply watered status irrespective of rain, you can just change the branch below

$82/A8A9 29 02 00    AND #$0002              A:0002 X:074D Y:01D0 P:envmxdizc

; If not rainy, skip ahead--
$82/A8AC F0 03       BEQ $03    [$A8B1]      A:0002 X:074D Y:01D0 P:envmxdizc	

; If it is rainy, goto IncrementTileValue to mark the tile as watered.					
$82/A8AE 4C 06 A9    JMP $A906  [$82:A906]   A:0002 X:074D Y:01D0 P:envmxdizc

to no ops. In other words, change

82A8AC: F0
82A8AD: 03

to

82A8AC: EA
82A8AD: EA

Expressing this as a PAR code, it looks like

82A8ACEA
82A8ADEA

See a demo of the change

It's a bit more fun this way. Enjoy

February 19th, 2022 at 5:29 am | Comments & Trackbacks (0) | Permalink

Finished the Community Center in Stardew Valley (PC)

Stardew Valley is like a "what could have been" if SNES Harvest Moon were transported into our post-Minecraft present day. It's really approachable and fun.

HM (left), Stardew(right)

Comparison is specific to SNES HM because of the similarity of gameplay and visual arrangement of things.

For Stardew I have some small gripes which are visual
• Colors burn retinas
• most of graphics uses low-color palette but lighting effects from lamps and torches have access to way more colors?
• Pixels do not align to pixel grid for fishing line and many animations (see image)

Stardew Valley is not the only offender in the category of "retro-style game that can't pick one video resolution". Shovel Knight and the Scott Pilgrim game also do this. Scott Pilgrim is probably the worst offender I have ever seen. Stardew Valley is not the worst but it's one where I have screenshots on hand.

Why is it bad? There are two parts to this, functional and aesthetic.

Functionally, it is capable of causing eye strain coming from the fact that it is visually confusing. Suppose we are looking at some 2D pixel art at a relatively coarse resolution. We use the same parts of our brains that could, say, appreciate mosaic tile artwork. The image lacks the full fidelity of the real world, but our eyes "fill in the missing information". Blocky edges become round in our mind's eye, detail is made out of nothing. When the resolution changes, it's like a tiled floor where big patches of tiles are different sizes. It's funhouse looking. It defies expectation. How can your brain interpolate things at a consistent rate, then? It's not altogether natural. Personally I experienced eye strain from this. While I am not a medical professional- just a person with a data set of size 1- I believe it could affect other people too since my eyesight is medically typical. I don't see anything necessarily causing my experience to be very unique. For people who aren't involved with graphics or low-res-looking games but play this one, they might experience eye strain yet not be to articulate why.

Aesthetically, it wrecks your scene composition. Here you've gone and standardized line width, balanced the colors of lines against the light source in your scene, taking into account the limited fidelity of the low resolution you've standardized on. But, wait a minute! You have this completely other part of the scene which is at a different video resolution. There's not an easy way to account for that in how you set up the scene. It will just look bad and I'm sad to say once you see it you can't un-see it.

The reason why the problem is possible is because these games are drawn at a much lower resolution than your native display resolution. When I run Stardew, for example, each 4x4 region of pixels tends to be (* minus the exceptions discussed here) of uniform color. On my 1920x1080 graphics setting, this implies the game is meant to run at 480x270. See if I were running the game somehow natively on a very old monitor, it wouldn't need to do anything 'special' in terms of scaling. Each of the TV's pixels is already the size of your face so whatever. But to fake a low resolution on higher resolution displays, upscaling is necessary. And that's perfectly ok. The problem is the way in which these games do the upscaling.

Most upsetting of all is that it's typically harder to get this wrong than to get it right. Yeah you read that correctly. Of course I don't have any game source code and I don't know how the developers have organized things, so I am making broad assumptions.

The supposed ease of implementation falls out of how geometry transforms and co-ordinate spaces work. If you keep a consistent low-resolution, you can set up things by:

  1. Allocate a low-resolution (e.g., 480x320) intermediate target and a full-resolution (e.g., 1920x1200) final target
  2. Draw your graphics to that intermediate. Move, flip, rotate sprites as necessary in that low-resolution space
  3. Draw the intermediate, scaled, to the final target

The complicated step for #2 isn't so bad since you only need to deal with one co-ordinate space. The scale at the end is really simple.

But instead, games decide to do this:

  1. Allocate one full-resolution (e.g., 1920x1200) final target
  2. Draw graphics directly (?) to that target.
    • Each sprite's transformed position needs to take into account the scale-from-lowres-space-to-final-target

The problem is in step #2. Even if your calculations are 100% correct for getting everything positioned correctly in the final target, this setup will cause resolution-mixing artifacts (e.g., if you rotate anything by non-45-degree increments) since it is baked right into the design.

So, why have game developers gone out of their way to get this resolution-mixing behavior?

Maybe to save perf or memory. That said, I'm not pursuaded a small (~480x320) intermediate and a scale will make or break you for these games. Maybe they don't care or they don't think people will notice.

As much as I like Minecraft I think a lot of people have been corrupted by Minecraft aesthetic. You have the low-res-looking, limited-pallette-looking voxel art, where perspective transform is putting boxes of different sizes in your face all the time until it seems visually normal to you.

As for whether mixing low resolutions is bad or even a problem is getting out of the technical and into the subjective world of art. I assert that it's bad. I assert that it's bad in the same way that the author of McMansion Hell puts forth a case against certain architectural styles. I put forth a case against mixing of low video resolutions here. Now I think this post is way too long for describing something so simple and obvious but it's written without any particular target audience in mind so maybe you can make something of it.

But wait, there's more! Pallette inconsistency.

How many colors are in the pallette? Answer: yes.

The color inconsistency, I can also sympathize with-- it's hard, and arguably more work to take the result of your lighting calculation and quantize it down to the pallette used for the rest of the sprite artwork. But I mean, is it that hard? It's something you really can do programmatically, it's not like you need new art assets. You can use a lookup table in a shader, a volume texture if you want. There are different options for fixing this, but they just don't for some reason. Stardew Valley has no shortage of ambitious stuff- a procedural dungeon, a multiplayer mode, lots of characters with real-time behavior-- so it's not like they want for time or talent, but the take-away is this isn't where they chose to spend their time.

All of this being said, I want to make it clear that I liked the game.

So back on the game itself-- what Stardew does better than Harvest Moon:
• Automation of tasks. The sprinklers and auto-machines to feed and care for animals are a huge ease off of grind, unlockable in the game.
• Controls are less frustrating by design. It's impossible to accidentally expend seeds on an ineligible place, or drop things on the floor. How many times I've attempted in HM to give someone a gift only to Throw It On the Ground
• You can step "through" most crops, allowing for more flexibility in how to organize them
• The Minecraft-isms are fun and thoughtful.
• You can customize where new buildings are placed
• You can marry a person regardless of gender (e.g., same gender)- a step forward. While the original 1996 Harvest Moon doesn't have this, the modern Harvest Moons (Story of Seasons) don't have it either
• More interesting characters. For example I liked Linus's storyline, where he lives as a homeless person but your role is not to "fix" or "save" him. Rather, your role is to be his friend
• The relationship system is less broken. The rate of heart levels increase is slowed as the number of allowed gifts is fixed
• There is an interesting variety of characters. They all have unique events for friendship, not just marriage candidates

The game doesn't have a conventional "ending" like HM where there is a long scene and then credits.

There is an "evaluation" of sorts on the 3rd year as well as two big milestones. An easier, sad-outcome one (completion of Joja Community Development project) or the difficult, happy-outcome one (renovation of the Community Center). I ended up making two farms where one got each outcome. Although I did these, there is still a lot of replayability potential.

Overall verdict: it's good! Krobus/10

January 24th, 2019 at 11:23 pm | Comments & Trackbacks (0) | Permalink

Japanese, USA and German respectively.

The Germans do not mess around with their box art

  

May 22nd, 2017 at 9:53 pm | Comments & Trackbacks (0) | Permalink