1 /***************************************************************************
2     Best Outrunners Name Entry & Display.
3     Used in attract mode, and at game end.
4 
5     Copyright Chris White.
6     See license.txt for more details.
7 ***************************************************************************/
8 
9 #include "main.hpp"
10 #include "engine/ohud.hpp"
11 #include "engine/oinputs.hpp"
12 #include "engine/ostats.hpp"
13 #include "engine/outils.hpp"
14 #include "engine/ohiscore.hpp"
15 
16 OHiScore ohiscore;
17 
OHiScore(void)18 OHiScore::OHiScore(void)
19 {
20 }
21 
~OHiScore(void)22 OHiScore::~OHiScore(void)
23 {
24 }
25 
26 // Clear score variables (not scores themselves)
27 //
28 // Source: 0xCE74
init()29 void OHiScore::init()
30 {
31     //ostats.score = 0x04100000; // hack
32 
33     best_or_state     = 0;
34     state             = STATE_GETPOS;
35     score_pos         = -1;
36     initial_selected  = 0;
37     letter_selected   = 0;
38     acc_curr          = 0;
39     acc_prev          = 0;
40     flash             = 0;
41     score_display_pos = 0;
42     dest_total        = 0;
43 }
44 
45 // Setup palette for Best Outrunners High Score Entry
46 // This is the shaded red background for the hi-score entry
47 // Source: 0x360C
setup_pal_best()48 void OHiScore::setup_pal_best()
49 {
50     uint32_t src = PAL_BESTOR;
51     uint32_t dst = 0x120F00;
52 
53     for (int i = 0; i <= 0x1F; i++)
54         video.write_pal32(&dst, roms.rom0.read32(&src));
55 }
56 
57 // Setup road colour for Best Outrunners High Score Entry
58 // This is a pure black road for the hi-score entry
59 // Source: 0x3624
setup_road_best()60 void OHiScore::setup_road_best()
61 {
62     uint32_t dst = 0x120800;
63 
64     for (int i = 0; i <= 0x1F; i++)
65         video.write_pal32(&dst, 0);
66 }
67 
68 // Initalize Default Score Table
69 // Source: 0xD17A
init_def_scores()70 void OHiScore::init_def_scores()
71 {
72     uint32_t adr = DEFAULT_SCORES;
73 
74     for (int i = 0; i < NO_SCORES; i++)
75     {
76         // Read default score
77         scores[i].score = roms.rom0.read32(&adr);
78 
79         // Read initials
80         uint32_t initials = roms.rom0.read32(&adr);
81         scores[i].initial1 = (initials >> 24) & 0xFF;
82         scores[i].initial2 = (initials >> 16) & 0xFF;
83         scores[i].initial3 = (initials >> 8) & 0xFF;
84 
85         // Read default time
86         scores[i].time = roms.rom0.read16(&adr);
87         //scores[i].time = (i & 1) ? 0x4321 : 0x1234; // hack to display 4m 43 51 or 1m 16 56
88         // Read map tiles
89         scores[i].maptiles = roms.rom0.read32(&adr);
90         //scores[i].maptiles = 0xe5c8c2d1; // hack to populate map tiles for testing
91     }
92 }
93 
94 // Hi Score Processing Logic
95 //
96 // Source: 0xD1C4
tick()97 void OHiScore::tick()
98 {
99     switch (state & 3)
100     {
101         // Detect Score Position, Insert Score, Init Table
102         case STATE_GETPOS:
103             get_score_pos();
104 
105             // New High Score
106             if (score_pos != -1)
107             {
108                 osoundint.queue_sound(sound::PCM_WAVE);
109                 osoundint.queue_sound(sound::MUSIC_LASTWAVE);
110                 insert_score();
111             }
112             // Not a High Score
113             else
114             {
115                 ostats.time_counter = 5;
116             }
117             set_display_pos();
118             acc_prev = -1;
119             state = STATE_DISPLAY;
120             video.enabled = true;
121             break;
122 
123         // Display Basic High Score Table
124         case STATE_DISPLAY:
125             display_scores();
126             if (best_or_state >= 2)
127                 state = STATE_ENTRY; // Only allow name entry when minicars have animation finished
128             break;
129 
130         // Init Name Entry
131         case STATE_ENTRY:
132             check_name_entry();
133             break;
134 
135         // Score Done
136         case STATE_DONE:
137             return;
138     }
139 }
140 
141 // Calculate high score position.
142 // Source: D318
get_score_pos()143 void OHiScore::get_score_pos()
144 {
145     for (int i = 0; i < NO_SCORES; i++)
146     {
147         if (ostats.score > scores[i].score)
148         {
149             score_pos = i;
150             set_display_pos();
151             return;
152         }
153     }
154 
155     score_pos = -1; // Not a new high-score
156 }
157 
158 // - Insert Score Entry
159 // - Move Other Entries Down In Memory
160 // - Calculate Completed Time
161 // - Setup Appropriate Minimap Tiles
162 //
163 // Source: 0xD2C0
insert_score()164 void OHiScore::insert_score()
165 {
166     // Move entries down in memory
167     for (int i = NO_SCORES - 1; i > score_pos; i--)
168     {
169         scores[i] = scores[i-1];
170     }
171 
172     scores[score_pos].score    = ostats.score;
173     scores[score_pos].initial1 = 0x20;
174     scores[score_pos].initial2 = 0x20;
175     scores[score_pos].initial3 = 0x20;
176 
177     // Calculate total time if game completed. Store result in $20
178     if (ostats.game_completed)
179     {
180         const uint8_t entries = outrun.cannonball_mode == Outrun::MODE_ORIGINAL ? 5 : 15;
181 
182         scores[score_pos].time = 0;
183 
184         for (int i = 0; i < entries; i++)
185             scores[score_pos].time += ostats.stage_counters[i];
186     }
187     else
188     {
189         scores[score_pos].time = 0;
190         ostats.game_completed = false;
191     }
192 
193     // Setup Appropriate Minimap Tiles
194     scores[score_pos].maptiles = roms.rom0.read32(ohud.setup_mini_map());
195 }
196 
197 // Set Table Position To Display Score From. Store Result in $26
198 // Source: 0xD298
set_display_pos()199 void OHiScore::set_display_pos()
200 {
201     if (score_pos < 0)
202     {
203         score_display_pos = 13;
204     }
205     else
206     {
207         score_display_pos = score_pos - 3;
208 
209         if (score_display_pos < 0)
210             score_display_pos = 0;
211         else if (score_display_pos > 13)
212             score_display_pos = 13;
213     }
214     //score_display_pos = 0; // HACK!
215 }
216 
217 // Check whether to perform name entry.
218 // Print alphabet and other stuff if necessary.
219 // Source: 0xD252
check_name_entry()220 void OHiScore::check_name_entry()
221 {
222     // No High Score
223     if (score_pos == -1)
224     {
225         ohud.blit_text1(TEXT1_YOURSCORE);
226         ohud.draw_score(0x110BDA, ostats.score, 3); // Select font 3 and print score
227         state = STATE_DONE;
228     }
229     else
230     {
231         // Get text ram address of score to blit
232         uint32_t score_adr = get_score_adr();
233         // Blit Alphabet. Highlight selected letter red.
234         blit_alphabet();
235         // Flash current initial that is being entered
236         flash_entry(score_adr);
237         // Draw big red countdown timer
238         const uint16_t BIG_RED_FONT = 0x8080;
239         ohud.draw_timer2(ostats.time_counter, 0x1101EC, BIG_RED_FONT);
240         // Input from controls
241         do_input(score_adr);
242 
243         // Save new score info
244         if (state == STATE_DONE)
245             config.save_scores(outrun.cannonball_mode == Outrun::MODE_ORIGINAL);
246     }
247 }
248 
249 // Get Address in text ram at which to output score
250 // Source: 0xD542
get_score_adr()251 uint32_t OHiScore::get_score_adr()
252 {
253     if (score_pos < 3)
254         return 0x110452 + (score_pos << 8); // top 3 positions
255     if (score_pos >= 17)
256         return 0x110A52 + ((score_pos - 19) << 8); // last 3 positions
257 
258     return 0x110752; // middle positions
259 }
260 
261 // Blit Alphabet. Highlight selected letter red.
262 // Source: 0xD45A
blit_alphabet()263 void OHiScore::blit_alphabet()
264 {
265     // Print Text: "ABCDEFGHIJK..."
266     ohud.blit_text2(TEXT2_ALPHABET);
267 
268     // Address in text ram for characters
269     uint32_t adr = 0x110BF0;
270 
271     video.write_text16(&adr,       0x8D00); // Full Stop
272     video.write_text16(adr + 0x7E, 0x8D01);
273     video.write_text16(&adr,       0x8D04); // Arrow
274     video.write_text16(adr + 0x7E, 0x8D05);
275     video.write_text16(&adr,       0x8D02); // ED
276     video.write_text16(adr + 0x7E, 0x8D03);
277 
278     // Colour selected tile red
279     const uint16_t RED = 0x80;
280     adr = 0x110BBC + (letter_selected << 1);
281     video.write_text8(adr,        (video.read_text8(adr) & 1) | RED);
282     video.write_text8(adr + 0x80, (video.read_text8(adr + 0x80) & 1) | RED);
283 }
284 
285 // Flash current initial that is being entered
286 //
287 // Takes address of score entry as input
288 //
289 // Source: 0xD42C
flash_entry(uint32_t adr)290 void OHiScore::flash_entry(uint32_t adr)
291 {
292     uint16_t tile = 0x20; // Default blank tile
293     flash++; // Increment flashing counter
294 
295     if (flash & BIT_3)
296     {
297         tile = (roms.rom0.read8(letter_selected + TILES_ALPHABET) & 0xFF) | 0x8600;
298     }
299 
300     video.write_text16(adr + (initial_selected << 1), tile);
301 }
302 
303 // High Score Input
304 //
305 // Source: 0xD33A
do_input(uint32_t adr)306 void OHiScore::do_input(uint32_t adr)
307 {
308     // Read Steering Left / Right & Denote Letter To Be Highlighted
309 
310     const static uint8_t ENTRIES = 28; // 28 Possible entries we can select from
311     const static uint8_t DELETE = ENTRIES - 1;
312 
313     int16_t position = read_controls() + letter_selected;
314 
315     if (position > ENTRIES)
316         letter_selected = position = 0;
317     else if (position < (initial_selected == 3 ? DELETE : 0))
318         letter_selected = position = ENTRIES;
319     else
320         letter_selected = position;
321 
322     // Check accelerator for press and depress
323     if (!acc_curr || !(acc_prev ^ acc_curr)) return;
324 
325     // End option selected
326     if (letter_selected == ENTRIES)
327     {
328         video.write_text16(adr + (initial_selected << 1), 0x20); // Write blank tile to ram
329         ostats.frame_counter = 0;
330         ostats.time_counter = 0;
331         state = STATE_DONE;
332     }
333     // Delete option selected
334     else if (letter_selected == DELETE)
335     {
336         // Delete if not at first position
337         if (initial_selected != 0)
338         {
339             if (initial_selected == 1)
340                 scores[score_pos].initial2 = 0x20;
341             else if (initial_selected == 2)
342                 scores[score_pos].initial3 = 0x20;
343 
344             video.write_text16(adr + (initial_selected << 1), 0x20); // Write blank tile to ram
345 
346             initial_selected--;
347         }
348     }
349     // Normal character selected
350     else
351     {
352         uint8_t tile = roms.rom0.read8(TILES_ALPHABET + letter_selected);
353 
354         // Store initial to score structure
355         if (initial_selected == 0)
356             scores[score_pos].initial1 = tile;
357         else if (initial_selected == 1)
358             scores[score_pos].initial2 = tile;
359         else if (initial_selected == 2)
360         {
361             scores[score_pos].initial3 = tile;
362             letter_selected = ENTRIES;
363         }
364 
365         video.write_text16(adr + (initial_selected << 1), tile | 0x8600); // Write initial tile to ram
366 
367         // Final Initial
368         // Note we have optional functionality to delete the final entry here
369         if (++initial_selected >= (config.engine.hiscore_delete ? 4 : 3))
370         {
371             state = STATE_DONE;
372             ostats.frame_counter = ostats.frame_reset;
373             ostats.time_counter  = 2;
374             // code to enable easter egg if YU. is inputted goes here.
375         }
376     }
377 }
378 
379 // Read controls for high score input screen
380 //
381 // Output:
382 //  0 = No Movement
383 // -1 = Left
384 //  1 = Right
385 //
386 // Source: 0xD4DA
read_controls()387 int8_t OHiScore::read_controls()
388 {
389     // Determine when accelerator has been pressed then depressed
390     if (oinputs.input_acc < 0x30)
391     {
392         acc_prev = acc_curr;
393         acc_curr = 0;
394     }
395     else if (oinputs.input_acc < 0x60)
396     {
397         acc_curr = acc_prev;
398     }
399     else
400     {
401         acc_prev = acc_curr;
402         acc_curr = -1;
403     }
404 
405     // Check Steering Wheel
406     int8_t movement = 1; // default to right
407     int16_t steering = (oinputs.input_steering & 0xFF) - 0x80;
408     if (steering < 0)
409     {
410         steering = -steering;
411         movement = -1; // left
412     }
413 
414     // Set increment to potentially advance to next letter.
415     // This depends on how far the steering wheel is turned.
416     if (steering >= 0x30)
417         steer += 5;
418     else if (steering >= 0x10)
419         steer += 1;
420 
421     if (steer >= 0x14)
422         steer = 0;
423     else
424         movement = 0; // no movement
425 
426     return movement;
427 }
428 
429 // Display Best Outrunners in attract mode and name entry screen
430 //
431 // Source: 0xCE84
display_scores()432 void OHiScore::display_scores()
433 {
434     switch (best_or_state)
435     {
436         // Init
437         case 0:
438             video.clear_text_ram();
439             setup_minicars();
440             blit_score_table();
441             best_or_state = 1; // Set State to TICK
442             break;
443 
444         // Tick
445         case 1:
446             tick_minicars();
447             // Have all mini-cars reached their destination?
448             if (dest_total >= 7)
449                  best_or_state = 2; // Set State to DONE
450             break;
451 
452         // Return
453         case 2:
454             return;
455     }
456 }
457 
458 // ------------------------------------------------------------------------------------------------
459 //                                       Mini car Movement
460 // ------------------------------------------------------------------------------------------------
461 
462 // Setup minicars before they move across screen
463 // Source: 0xCED2
setup_minicars()464 void OHiScore::setup_minicars()
465 {
466     for (int i = 0; i < NO_MINICARS; i++)
467     {
468         minicars[i].pos         = 0x100;
469         minicars[i].dst_reached = 0;
470         minicars[i].speed       = (outils::random() & 0x180) | 0xF0;
471         minicars[i].base_speed  = (outils::random() & 0x7) | 0x01;
472     }
473 }
474 
475 // Move minicars across screen on text ram layer
476 // Source: 0xCF0E
tick_minicars()477 void OHiScore::tick_minicars()
478 {
479     // Destination in text ram
480     uint32_t dst = 0x11047C;
481 
482     // Source tile data
483     uint32_t tiles_adr = TILES_MINICARS1;
484 
485     // There are seven lines / entries to blit
486     for (int i = 0; i < NO_MINICARS; i++)
487     {
488         minicar_entry* minicar = &minicars[i];
489 
490         // Minicar is on-screen
491         if (!minicar->dst_reached & BIT_0)
492         {
493             // Minicar has reached destination position (off-screen)
494             if ((minicar->pos >> 8) >= 0x5A)
495             {
496                 minicar->dst_reached |= BIT_0;
497                 dest_total++; // Increment total minicars that have reached destination
498             }
499 
500             minicar->speed += minicar->base_speed;
501 
502             if (minicar->speed >= 0x200)
503                 minicar->speed = 0x180;
504 
505             minicar->pos += minicar->speed;
506 
507             setup_minicars_pal(minicar);
508 
509             // Masked off the lower bit
510             int16_t pos = (minicar->pos >> 8) & 0xFFFE;
511 
512             // Get final address in text ram for minicar based on position
513             uint32_t textram_adr = dst - pos;
514 
515             // Address for following smoke tiles
516             uint32_t tiles_smoke_adr = TILES_MINICARS2;
517 
518             // The minicar is two tiles wide.
519             // Two versions of routine, one that only blits the car in two tiles
520             if ((minicar->pos >> 8) & BIT_0)
521             {
522                 video.write_text32(&textram_adr, roms.rom0.read32(tiles_adr)); // blit car in 2 tiles
523                 video.write_text32(&textram_adr, roms.rom0.read32(&tiles_smoke_adr)); // smoke trail tile 1
524                 video.write_text16(&textram_adr, roms.rom0.read16(&tiles_smoke_adr)); // smoke trail tile 2
525             }
526             // Blit at an offset
527             // The second blits the mini-car at an offset halfway into the tile (and hence takes 3 tiles)
528             else
529             {
530                 video.write_text32(&textram_adr, roms.rom0.read32(4 + tiles_adr)); // blit car in 3 tiles
531                 video.write_text16(&textram_adr, roms.rom0.read16(8 + tiles_adr)); // blit car in 3 tiles
532                 video.write_text32(&textram_adr, roms.rom0.read32(&tiles_smoke_adr)); // smoke trail tile 1
533                 video.write_text16(&textram_adr, roms.rom0.read16(&tiles_smoke_adr)); // smoke trail tile 2
534             }
535 
536             // Erase Minicar tiles (0xCFB2)
537             // Reveal info from tile ram by copying to text ram
538 
539             // Bottom Line
540             uint16_t tile_bits = video.read_tile8(textram_adr - 0x2000 + 1) | minicar->tile_props;
541             video.write_text16(textram_adr, tile_bits);
542             // Top Line
543             tile_bits = video.read_tile8(textram_adr - 0x2000 - 0x7F) | minicar->tile_props;
544             video.write_text16(textram_adr - 0x80, tile_bits);
545         }
546 
547         dst += 0x100; // Advance to next row in text ram
548         tiles_adr += 0x0A; // Advance to next block of minicar data
549     }
550 }
551 
552 // Setup palette and priority data for the copied tiles behind the minicar.
553 // The palette & priority used for the text depends on the position.
554 // Source: 0xCFCC
setup_minicars_pal(minicar_entry * minicar)555 void OHiScore::setup_minicars_pal(minicar_entry* minicar)
556 {
557     uint8_t pos = minicar->pos >> 8;
558 
559     // Lap Time Tile Properties
560     minicar->tile_props = 0x8400;
561     if (pos <= 0x20) return; // Was 0x1F in original: Changed to handle longer times
562 
563     // Route Tile Properties
564     minicar->tile_props = 0x8B00;
565     if (pos <= 0x2D) return;
566 
567     // Initial Tile Properties
568     minicar->tile_props = 0x8200;
569     if (pos <= 0x39) return;
570 
571     // Score Tile Properties
572     minicar->tile_props = 0x8400;
573     if (pos <= 0x4A) return;
574 
575     // 1.2.3. Tile Properties
576     minicar->tile_props = 0x8600;
577 }
578 
579 // ------------------------------------------------------------------------------------------------
580 //                                     Score Table Rendering
581 // ------------------------------------------------------------------------------------------------
582 
583 // Source: 0xD00C
blit_score_table()584 void OHiScore::blit_score_table()
585 {
586     // Clear tile table ready for High Score Display
587     uint32_t tile_addr = 0x10E000; // Tile Table 15
588     for (int i = 0; i <= 0x3FF; i++)
589         video.write_tile32(&tile_addr, 0x200020);
590 
591     ohud.blit_text2(TEXT2_BEST_OR);   // Print "BEST OUTRUNNERS"
592     ohud.blit_text1(TEXT1_SCORE_ETC); // Print Score, Name, Route, Record
593     blit_digit();                     // Blit 1. 2. 3. etc.
594     blit_scores();                    // Blit list of scores
595     blit_initials();                  // Blit initials attached to those scores
596     if (outrun.cannonball_mode != Outrun::MODE_CONT)
597         blit_route_map();            // Blit Mini Route Map
598     blit_lap_time();
599 }
600 
601 // Blit 7x single digit at start of score table (1. 2. 3. 4. 5. 6. 7.)
602 // Source: 0xD03A
blit_digit()603 void OHiScore::blit_digit()
604 {
605     // Destination in tile ram for digit
606     uint32_t dst = 0x10E438;
607 
608     // Starting display position
609     int16_t pos = score_display_pos + 1;
610 
611     // Display numbers 1 to 7
612     for (int i = 0; i < 7; i++)
613     {
614         int32_t tile = (pos / 10) | ((pos % 10) << 16);
615 
616         // Draw blank
617         if (!(tile & 0xFFFF))
618         {
619             tile = (tile & 0xFFFF0000) | 0x20;
620             outils::swap32(tile);
621             tile |= 0x30;
622         }
623         // Draw tile
624         else
625         {
626             outils::swap32(tile);
627             tile |= 0x300030;
628         }
629 
630         video.write_tile32(dst, tile);      // Output number digit
631         video.write_tile16(4 + dst, 0x5B);  // Output full stop following digit
632 
633         dst += 0x100; // Advance to next text row
634         pos++;
635     }
636 }
637 
638 // Blit High Scores
639 //
640 // Source: 0xD078
blit_scores()641 void OHiScore::blit_scores()
642 {
643     // Destination in tile ram for digit
644     uint32_t dst = 0x10E43E;
645 
646     // Starting display position
647     int16_t pos = score_display_pos;
648 
649     // Display scores 1 to 7
650     for (int i = 0; i < 7; i++)
651     {
652         ohud.draw_score_tile(dst, scores[pos++].score, 0);
653         dst += 0x100; // Advance to next text row
654     }
655 }
656 
657 // Blit Initials
658 //
659 // Source: 0xD0A4
blit_initials()660 void OHiScore::blit_initials()
661 {
662     // Destination in tile ram for digit
663     uint32_t dst = 0x10E452;
664 
665     // Starting display position
666     int16_t pos = score_display_pos;
667 
668     // Write 3 initials for entries 1 to 7
669     for (int i = 0; i < 7; i++)
670     {
671         video.write_tile8(dst + 1, scores[pos].initial1);
672         video.write_tile8(dst + 3, scores[pos].initial2);
673         video.write_tile8(dst + 5, scores[pos].initial3);
674         pos++;
675         dst += 0x100; // Advance to next text row
676     }
677 }
678 
679 // Blit mini route map
680 //
681 // Source: 0xD0D8
blit_route_map()682 void OHiScore::blit_route_map()
683 {
684     // Destination in tile ram for digit
685     uint32_t dst = 0x10E45E;
686 
687     // Starting display position
688     int16_t pos = score_display_pos;
689 
690     // Write 7 map entries
691     for (int i = 0; i < 7; i++)
692     {
693         uint32_t tiles = scores[pos++].maptiles;
694 
695         // eg e5 c8 c2 d1 (4 tile indexes of route map)
696         video.write_tile8(dst - 0x7F, (tiles >> 24) & 0xFF);
697         video.write_tile8(dst - 0x7D, (tiles >> 16) & 0xFF);
698         video.write_tile8(dst + 0x01, (tiles >> 8) & 0xFF);
699         video.write_tile8(dst + 0x03, tiles & 0xFF);
700 
701         dst += 0x100; // Advance to next text row
702     }
703 }
704 
705 // Blit laptime
706 //
707 // Source: 0xD112
blit_lap_time()708 void OHiScore::blit_lap_time()
709 {
710     // Destination in tile ram for digit
711     uint32_t dst = 0x10E46A;
712 
713     // Starting display position
714     int16_t pos = score_display_pos;
715 
716     // Write 7 lap entries
717     for (int i = 0; i < 7; i++)
718     {
719         uint16_t time = scores[pos++].time;
720 
721         if (time)
722         {
723             convert_lap_time(time);
724 
725             // Write laptime
726             if (laptime[0] != TILE_PROPS)
727             {
728                 video.write_tile16(dst - 0x2, laptime[0]); // Minutes Digit 1
729             }
730 
731             video.write_tile16(0x0 + dst, laptime[1]); // Minutes Digit 2
732             video.write_tile16(0x2 + dst, 0x5E);       // '
733             video.write_tile16(0x4 + dst, laptime[2]); // Seconds Digit 1
734             video.write_tile16(0x6 + dst, laptime[3]); // Seconds Digit 2
735             video.write_tile16(0x8 + dst, 0x5F);       // '
736             video.write_tile16(0xA + dst, laptime[4]); // Milliseconds Digit 1
737             video.write_tile16(0xC + dst, laptime[5]); // Milliseconds Digit 2
738         }
739 
740         dst += 0x100; // Advance to next text row
741     }
742 }
743 
744 // Convert laptime to tile data and store in laptime array.
745 // Enhanced routine to handle minutes > 9
746 //
747 // Source: 0x806C
convert_lap_time(uint16_t time)748 void OHiScore::convert_lap_time(uint16_t time)
749 {
750     const uint16_t MINUTE = 3600;
751 
752     int32_t src_time = time; // laptime copy [d0]
753     int16_t minutes = -1;     // Store number of minutes
754 
755     // Calculate Minutes
756     do
757     {
758         src_time -= MINUTE;
759         minutes++;
760     }
761     while (src_time >= 0);
762 
763     src_time += MINUTE;
764     minutes = outils::convert16_dechex(minutes);
765 
766     // Store Millisecond Lookup
767     uint16_t ms_lookup = src_time & 0x3F;
768 
769     // Calculate Seconds
770     uint16_t seconds   = src_time >> 6;   // Store Seconds
771 
772     uint16_t s1 = seconds & 0xF; // First digit [d1]
773     uint16_t s2 = seconds >> 4;  // Second digit [d2]
774 
775     if (s1 > 9)
776         seconds += 6;
777 
778     s2 = outils::bcd_add(s2, s2);
779     int16_t d3 = s2;
780     s2 = outils::bcd_add(s2, s2);
781     s2 = outils::bcd_add(s2, d3);
782     seconds = outils::bcd_add(s2, seconds);
783 
784     // Output Milliseconds
785     laptime[5] = (ostats.lap_ms[ms_lookup] & 0xF) | TILE_PROPS;
786     laptime[4] = ((ostats.lap_ms[ms_lookup] & 0xF0) >> 4) | TILE_PROPS;
787 
788     // Output Seconds
789     laptime[3] = (seconds & 0xF) | TILE_PROPS;
790     laptime[2] = ((seconds & 0xF0) >> 4) | TILE_PROPS;
791 
792     // Output Minutes
793     laptime[1] = (minutes & 0xF) | TILE_PROPS;
794     laptime[0] = ((minutes & 0xF0) >> 4) | TILE_PROPS;
795 }