Author Topic: Snowboard / Submarine Timer  (Read 2550 times)


  • Banned
  • *
  • Posts: 11008
    • View Profile
Snowboard / Submarine Timer
« on: 2013-09-11 19:05:57 »
Working with NFITC1 to make these 2 games (Submarine should be dead easy), use frame based timers, and get rid of timegettime altogether.  Problem with the timers as they are is that they work in realtime.  If you get any changes in frame rate in play, it doesn't affect the timer, leading to unfair times.

There are 4 main ways to address the snowboard timer:

1. Change the calculation to use floating point addition for the counter (+16.66667)
2. Make it so the millisecond display is actually a frame counter (I have done this below to some extent)
3. Instead of conventional time, make the timer based entirely on frame count with 5 digits.
4. Add on 16, 16, 18 in repeating pattern.  I decided to go with this one in the end.

NFITC1 was just in the process of explaining how number 3 would work...

This is where I am with number 2.


{in play time format
9568A0 = 00 00 1A 00 00 1B 00 00 00
{time format
9568B0 = 1F 1F 1A 1F 1F 1B 1F 1F 00
{best format
9568D0 = 2D 2D 27 2D 2D 22 2D 2D 00

{Increment by 1
72D11D = BA 01 00 00 00 90

{time calcs
72D4D8 = B9 3C 00 00 00
72D495 = B9 10 0E 00 00
72D4C6 = 6B C9 3C 90 90 90
72D50B = 6B C9 3C 90 90 90
72D51E = 6B C9 3C 90 90 90

{write to 1st/2nd digit
72D458 = 88 4A 06
72D473 = 88 50 07
« Last Edit: 2013-09-12 01:40:08 by DLPB »


  • *
  • Posts: 2933
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Re: Snowboard / Submarine Timer
« Reply #1 on: 2013-09-11 20:04:52 »
Number 3:

A structure pointed to at 0x926290 (0xDD83B8) contains a dword value of a timer that traditionally stores the timer in seconds that is incremented by a delta of two timegettime function calls (which return the system time in milliseconds). This is accomplished by storing timegettime at the beginning in this structure then storing timegettime in another section. Bypassing all this is easily possible.

Code: [Select]
.text:0072D0E6                 call    ds:timeGetTime
.text:0072D0EC                 mov     ecx, off_926290
.text:0072D0F2                 mov     [ecx+60h], eax
.text:0072D0F5                 mov     edx, off_926290
.text:0072D0FB                 cmp     dword ptr [edx+64h], 0
.text:0072D0FF                 jnz     short loc_72D112
.text:0072D101                 mov     eax, off_926290
.text:0072D106                 mov     ecx, off_926290
.text:0072D10C                 mov     edx, [ecx+60h]
.text:0072D10F                 mov     [eax+64h], edx
.text:0072D112 loc_72D112:                             ; CODE XREF: sub_72D0C0+3Fj
.text:0072D112                 mov     eax, off_926290
.text:0072D117                 mov     ecx, off_926290
.text:0072D11D                 mov     edx, [eax+60h]
.text:0072D120                 sub     edx, [ecx+64h]
.text:0072D123                 mov     eax, off_926290
.text:0072D128                 mov     ecx, [eax+10h]
.text:0072D12B                 add     ecx, edx
.text:0072D12D                 mov     edx, off_926290
.text:0072D133                 mov     [edx+10h], ecx
.text:0072D136                 mov     eax, off_926290
.text:0072D13B                 cmp     dword ptr [eax+10h], 5999999
.text:0072D142                 jbe     short loc_72D151
.text:0072D144                 mov     ecx, off_926290
.text:0072D14A                 mov     dword ptr [ecx+10h], 5999999
.text:0072D151 loc_72D151:                             ; CODE XREF: sub_72D0C0+82j
.text:0072D151                 mov     edx, off_926290
.text:0072D157                 mov     eax, off_926290
.text:0072D15C                 mov     ecx, [eax+60h]
.text:0072D15F                 mov     [edx+64h], ecx

That's the code it traditionally uses. Reducing it to use only frames as its counter it becomes:

Code: [Select]
.text:0072D0E6                 mov     eax, off_926290
.text:0072D128                 mov     ecx, [eax+10h]
.text:0072D12B                 inc     ecx
.text:0072D12D                 mov     edx, off_926290
.text:0072D133                 mov     [edx+10h], ecx
.text:0072D136                 mov     eax, off_926290
.text:0072D13B                 cmp     dword ptr [eax+10h], 99999
.text:0072D142                 jbe     short loc_72D162
.text:0072D144                 mov     ecx, off_926290
.text:0072D14A                 mov     dword ptr [ecx+10h], 99999

Tada! Now it will count per frame with an upper limit of 99999 frames (max time of 27:46.65. If you take this long you're doing something HORRIBLY wrong). That doesn't solve everything, however as now we have to change the display. It's located at 0x72D485 which is a long function that I'm about to overwrite. This function is used in four places so it's probably holding times for the other mini-games as well. Specifically the highway chase and submarine games.

Code: [Select]
.text:0072D485 ; int __stdcall formatTimeString(int TimerValue, int DestinationMemory)
.text:0072D485 formatTimeString      proc near               ; CODE XREF: sub_72994E+4Ep
.text:0072D485                                         ; sub_72D207+2Ep ...
.text:0072D485 var_10          = dword ptr -10h
.text:0072D485 var_C           = dword ptr -0Ch
.text:0072D485 var_8           = dword ptr -8
.text:0072D485 var_4           = dword ptr -4
.text:0072D485 TimerValue      = dword ptr  8
.text:0072D485 DestinationMemory= dword ptr  0Ch
.text:0072D485                 push    ebp
.text:0072D486                 mov     ebp, esp
.text:0072D488                 sub     esp, 10h
.text:0072D48B                 mov     [ebp+var_10], ecx ;This is a pointless? line
.text:0072D48E                 mov     eax, [ebp+TimerValue]
.text:0072D493                 xor     edx, edx
.text:0072D495                 mov     ecx, 10
.text:0072D49A                 div     ecx
.text:0072D4B0                 mov     ecx, [ebp+DestinationMemory]
.text:0072D4B3                 mov     [ecx+10h], edx ;TimerValue % 10
.text:0072D49C                 mov     [ebp+Result], eax ;now a tenth of what it was before
.text:0072D49F                 mov     eax, [ebp+Result] ;This seems a little ridiculous too, but it's actually not
.text:0072D493                 xor     edx, edx
.text:0072D495                 mov     ecx, 10
.text:0072D49A                 div     ecx
.text:0072D4B0                 mov     ecx, [ebp+DestinationMemory]
.text:0072D4B3                 mov     [ecx+0Ch], edx ;(TimerValue / 10) % 10
.text:0072D49C                 mov     [ebp+Result], eax ;now a tenth of what it was before
.text:0072D49F                 mov     eax, [ebp+Result]
.text:0072D493                 xor     edx, edx
.text:0072D495                 mov     ecx, 10
.text:0072D49A                 div     ecx
.text:0072D4B0                 mov     ecx, [ebp+DestinationMemory]
.text:0072D4B3                 mov     [ecx+8h], edx ;(TimerValue / 100) % 10
.text:0072D49C                 mov     [ebp+Result], eax ;now a tenth of what it was before
.text:0072D49F                 mov     eax, [ebp+Result]
.text:0072D493                 xor     edx, edx
.text:0072D495                 mov     ecx, 10
.text:0072D49A                 div     ecx
.text:0072D4B0                 mov     ecx, [ebp+DestinationMemory]
.text:0072D4B3                 mov     [ecx+4h], edx ;(TimerValue / 1000) % 10
.text:0072D49C                 mov     [ebp+Result], eax ;now a tenth of what it was before
.text:0072D49F                 mov     eax, [ebp+Result]
.text:0072D493                 xor     edx, edx
.text:0072D495                 mov     ecx, 10
.text:0072D49A                 div     ecx
.text:0072D4B0                 mov     ecx, [ebp+DestinationMemory]
.text:0072D4B3                 mov     [ecx], edx ;(TimerValue / 10000) % 10
.text:0072D577                 mov     esp, ebp
.text:0072D579                 pop     ebp
.text:0072D57A                 retn    8
.text:0072D57A sub_72D485      endp

Because this is accessed in other places, it'd be safest to write this to some unused memory block. Now we have the digits stored in order in the DestinationMemory block in big-endian decimal. So we need to force this into the display string:

Code: [Select]
.text:0072D3EA ; int __stdcall sub_72D3EA(int DestinationString, int FormattedTimeValue, char FF7TextOffset)
.text:0072D3EA sub_72D3EA      proc near               ; CODE XREF: sub_72D207+7Bp
.text:0072D3EA                                         ; sub_72D333+60p ...
.text:0072D3EA var_4           = dword ptr -4
.text:0072D3EA DestinationString= dword ptr  8
.text:0072D3EA FormattedTimeValue= dword ptr  0Ch
.text:0072D3EA FF7TextOffset   = byte ptr  10h
.text:0072D3EA                 push    ebp
.text:0072D3EB                 mov     ebp, esp
.text:0072D3ED                 push    ecx
.text:0072D3EE                 mov     [ebp+var_4], ecx ;I have no idea what purpose this serves
.text:0072D3F1                 movsx   eax, [ebp+FF7TextOffset]
.text:0072D3F5                 mov     ecx, [ebp+FormattedTimeValue]
.text:0072D3F8                 mov     edx, [ecx]
.text:0072D3FA                 add     edx, eax
.text:0072D3FC                 mov     eax, [ebp+DestinationString]
.text:0072D3FF                 mov     [eax], dl
.text:0072D401                 movsx   ecx, [ebp+FF7TextOffset]
.text:0072D405                 mov     edx, [ebp+FormattedTimeValue]
.text:0072D408                 mov     eax, [edx+4]
.text:0072D40B                 add     eax, ecx
.text:0072D40D                 mov     ecx, [ebp+DestinationString]
.text:0072D410                 mov     [ecx+1], al
.text:0072D413                 movsx   edx, [ebp+FF7TextOffset]
.text:0072D417                 mov     eax, [ebp+FormattedTimeValue]
.text:0072D41A                 mov     ecx, [eax+8]
.text:0072D41D                 add     ecx, edx
.text:0072D41F                 mov     edx, [ebp+DestinationString]
.text:0072D422                 mov     [edx+2], cl
.text:0072D425                 movsx   eax, [ebp+FF7TextOffset]
.text:0072D429                 mov     ecx, [ebp+FormattedTimeValue]
.text:0072D42C                 mov     edx, [ecx+0Ch]
.text:0072D42F                 add     edx, eax
.text:0072D431                 mov     eax, [ebp+DestinationString]
.text:0072D434                 mov     [eax+3], dl
.text:0072D437                 movsx   ecx, [ebp+FF7TextOffset]
.text:0072D43B                 mov     edx, [ebp+FormattedTimeValue]
.text:0072D43E                 mov     eax, [edx+10h]
.text:0072D441                 add     eax, ecx
.text:0072D443                 mov     ecx, [ebp+DestinationString]
.text:0072D446                 mov     [ecx+4], al
.text:0072D449                 movsx   edx, [ebp+FF7TextOffset]
.text:0072D44D                 mov     eax, [ebp+FormattedTimeValue]
.text:0072D450                 mov     ecx, [eax+14h]
.text:0072D453                 add     ecx, edx
.text:0072D455                 mov     edx, [ebp+DestinationString]
.text:0072D458                 mov     [edx+5], cl
.------------------------------------------------------------    chunk erased
.text:0072D47F                 mov     esp, ebp
.text:0072D481                 pop     ebp
.text:0072D482                 retn    0Ch
.text:0072D482 sub_72D3EA      endp

FF7TextOffset in this case should be 10h because that's where the '0' character is in its text map.
This function, again, is referenced in other locations so it might be best to write this somewhere else too.

So the formatting string needs to be changed to:
.data:009568A0 TimerText       db '00000', 00 ; DATA XREF: sub_72D207+73o

Just a null-terminated string of 5 zeroes. This forces the display of the time to have leading zeroes, but that's good for the leaderboard for all the text to be aligned.


  • Banned
  • *
  • Posts: 11008
    • View Profile
Re: Snowboard / Submarine Timer
« Reply #2 on: 2013-09-11 20:32:47 »
Impressive.  Most impressive.  G-Bike doesnt have a timer though!  :)
« Last Edit: 2013-09-11 20:40:28 by DLPB »


  • *
  • Posts: 2933
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Re: Snowboard / Submarine Timer
« Reply #3 on: 2013-09-11 21:33:53 »
G-Bike doesnt have a timer though!  :)

Does it not? Then why is it referenced in FIVE different places? The only minigame I could think of that uses timers is snowboard and submarine. Why are there three other invocations?

That's probably not the end of that conversation. I don't know how the rankings and leaderboards are displayed and it will probably need some editing as well. Make those changes and tell me what's wrong and I can fix it.
« Last Edit: 2013-09-11 21:39:14 by NFITC1 »


  • Banned
  • *
  • Posts: 11008
    • View Profile
Re: Snowboard / Submarine Timer
« Reply #4 on: 2013-09-11 21:42:53 »
I'm probably gonna try using floating point to get this done so that we don't need to mess about with display and what not, but that might prove difficult.  I'm looking at how Delphi compiles it...

The gbike may have once been intended to use a timer, and maybe it does internally, but there is no display for it in game.

The frame count code above is interesting nonetheless!

As for the ranks, they are just a small table with 4 byte millisecond times for each rank on each course. (with 1 that is not used per course... was removed outside of Japan). If I use the above way, I'll fix it up!

International difficulty... If I got it right.
00926470 = 20 CB 00 00 F0 D2 00 00 C0 DA 00 00 60 EA 00 00
00926480 = E8 FD 00 00 E0 28 01 00 90 5F 01 00 FF FF FF FF
00926490 = D0 01 01 00 70 11 01 00 28 1D 01 00 E0 28 01 00
009264A0 = 80 38 01 00 A0 86 01 00 C0 D4 01 00 FF FF FF FF
009264B0 = 70 11 01 00 F8 24 01 00 80 38 01 00 08 4C 01 00
009264C0 = 18 73 01 00 B0 AD 01 00 D0 FB 01 00 FF FF FF FF

This might help me.
« Last Edit: 2013-09-11 23:02:36 by DLPB »


  • Banned
  • *
  • Posts: 11008
    • View Profile
Re: Snowboard / Submarine Timer
« Reply #5 on: 2013-09-11 22:35:41 »
Another way to do this, which is a bit "ugh" but I'm gonna use it:

Use integer values and cheat.  Have it log which loop it is on and add as follows

loop 1: add 16
loop 2: add 16
loop 3: add 18 and loop now equals 1 again.  Repeats process.

16+16+18= 50

16.6666*3 = 50.

This makes sure that every 3rd loop the correct millisecond value is being written.


And here it is.  Give it a check for me if you have time and see if it can be made simpler. 

72D0E6 = A1 90 62 92 00 8B 48 10 8A 15 AF 68 95 00 80 FA 01 7F 07 83 C1 10 FE C2 EB 05 83 C1 12 B2 00 EB 2B 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 88 15 AF 68 95 00 89 48 10 81 78 10 7F 8D 5B 00 76 1E

Run through

Code: [Select]
mov eax,[00926290]
mov ecx,[eax+10] // move current timer to ecx
mov dl,[009568AF] // move current flag to dl
cmp dl,01
jg 0072D100  //if flag is not 0 or 1
   add ecx,10 // else add 16
   inc dl  // and increment flag
   jmp 0072D105 // and jump to mov [009568AF],dl
add ecx,12 // add 18 if flag is 2
  mov dl,00  // reset flag

mov [009568AF],dl  //place flag value to memory.
mov [eax+10],ecx  // finally place new time to memory

This will zero the flag on game start.
726029 = C7 40 10 00 00 00 00 C6 05 AF 68 95 00 00 90 90 90 90 90 C6 40 68 00
« Last Edit: 2013-09-12 12:05:17 by DLPB »


  • Banned
  • *
  • Posts: 11008
    • View Profile
Re: Snowboard / Submarine Timer
« Reply #6 on: 2013-09-12 12:26:11 »
OK so pending any fixes that NFITC1 notices need making with the above code, the snowboard timer is fixed.  Next is the submarine timer, which runs on much the same idea (except this time it already runs based on frame count), and starts


A lot of this seems to be redundant, but I am really not sure about it..

Firstly, the minigame isn't running at 30fps (probably due to the main game timer bug that Dziugo was rewriting and then got busy.  Really hope he is still around to fix that.), so even when you get the code perfect, the timer isn't going to run at a true speed until it is.

In any case, this is what I have so far:

Code: [Select]
mov ecx,[00E7476C]  // flag for pause and end of game
cmp ecx,00
jg // skip adding 2 to ebp-3c if game is paused or ended
mov [ebp-3C],00000002
mov ecx,[00980DC8]
add ecx,[ebp-3C]
mov [00980DBC],X // needed for game over screen timer.  Not sure what value it needs. Probably 2.
mov [00980DC8],ecx
mov edx,[00980DD0]
sub edx,[00980DC8]
mov [00980DCC],edx
mov eax,[00980DC8]

Honestly, I don't know what's going on further down.  I can't understand why a value of 2 seems to be correct either (it might not be!).  The game is running at 30fps (which is correct for this minigame.  On PSX the frames just get doubled) though this time and not 60, so that probably has something to do with it.  Help needed.  :-D

The parsing calculation seems to be at 0x7928E9

AH GOOD!!  I've finally found where that damn delay is!  I need my weapon mod to have subs able to fire straight away.  The delay before they can fire is set at 0x77E794


Sub game seems to use frame count from the get go... so that makes this far less complicated.  I think this timer is fixed pending any corrections.  And there should be some because I cobbled together this fix and have no idea if I've totally buggered something up somewhere.
« Last Edit: 2013-09-12 16:04:01 by DLPB »