Chapter 17
The campaign¶
Chapters 13 to 16 took one mission apart. This chapter is about what holds the missions together and what outlives a mission, a game and a page reload. By its end you will know in which order the maps come, when it is night, and why the game never ends; what a won mission and a promotion change; how the high scores are kept; what a saved game holds and how the port reads one from a real Amiga; and how the game records and replays its own demo. The port and its instruments are gathered at the end.
Seven ranks, fifteen maps¶
The manual offers seven naval ranks (page 4); to the code a rank is where a campaign starts, and only the later ranks' maps carry enemy ships and airfields (chapter 13). A campaign is the run of missions from the rank chosen to the game's end, through the maps from that rank's first on, in the order of their letters. The rank is the campaign's stage, 0 to 6: the player chooses the first, and a promotion moves it on. The mission number counts the missions within a rank from 1.
| Rank | Missions | Maps |
|---|---|---|
| 0 | 3 | a, b, c |
| 1 | 3 | d, e, f |
| 2 | 2 | g, h |
| 3 | 2 | i, j |
| 4 | 1 | k |
| 5 | 1 | l |
| 6 | 3 | m, n, o |
The rank selection lists the seven ranks and, below them, an item that loads a saved game; choosing a rank sets the mission number to 1. Two tables turn the pair into a map. map_load, chapter 13's loader, needs the file's name, whose index is a running count: the missions of the ranks below plus the mission number less one. The other readers, the night's choice and the island's bonus among them, need only the map's number, which a second table gives by rank and mission with no count to make.
A won mission leads to the next mission, or first to a promotion; the last Hellcat or the carrier lost, to the game over and the high scores. A saved game enters at a mission; the attract demo returns without the high scores.
A mission won¶
A mission is won when the last island is neutralised with no enemy ship left, which the pass finds at the last soldier's death and the tick at the last pillbox's, or when the last enemy ship sinks with no island left (chapters 15 and 16). Each calls mission_won, which counts the mission number on and compares it with the rank's count of missions. Within the count, the ticker gets the next mission's message. Beyond it comes the promotion, the step from a rank's last mission to the next rank's first: the mission number back to 1, the rank one up but never past 6, the promotion's flag, balloons_on, set, and a message of its own.
The message is appended to what the ticker, chapter 11's message line, holds, which is always the island's bonus or the sunk ship's name. On the left, tst.b (a2)+ walks to the end of that text and leaves the pointer one past its NUL, the zero byte that ends a text; subq.w #2 steps back over the NUL and the character before it, so the new message begins where the old one's last character stood. Both branches then point the ticker at the text and set the won flag, a word only this routine sets, which says that the mission is won.
; re/Wings.lst 0x015694-0x01570E: mission_won [asm]
mission_won:
015694 48e780e0 movem.l d0/a0-a2, -(a7)
015698 526ca3c2 addq.w #$1, -$5c3e(a4) ; mission_number
01569c 41eca54a lea.l -$5ab6(a4), a0 ; missions_per_rank
0156a0 302ca3c0 move.w -$5c40(a4), d0 ; rank_played
0156a4 d040 add.w d0, d0
0156a6 30300000 move.w (a0, d0.w), d0
0156aa b06ca3c2 cmp.w -$5c3e(a4), d0 ; mission_number
0156ae 6c000038 bge.w $156e8
0156b2 397c0001a3c2 move.w #$1, -$5c3e(a4) ; mission_number
0156b8 526ca3c0 addq.w #$1, -$5c40(a4) ; rank_played
0156bc 0c6c0006a3c0 cmpi.w #$6, -$5c40(a4) ; rank_played
0156c2 6f000008 ble.w $156cc
0156c6 397c0006a3c0 move.w #$6, -$5c40(a4) ; rank_played
loc_0156cc:
0156cc 50eca35f st.b -$5ca1(a4) ; balloons_on
0156d0 41ec8a5c lea.l -$75a4(a4), a0 ; msg_promotion
0156d4 93c9 suba.l a1, a1
0156d6 45ecc16c lea.l -$3e94(a4), a2 ; ticker_text
loc_0156da:
0156da 4a1a tst.b (a2)+
0156dc 66fc bne.b $156da
0156de 554a subq.w #$2, a2
0156e0 4ebaf996 jsr $15078(pc) ; format_to
0156e4 60000016 bra.w $156fc
loc_0156e8:
0156e8 41ec89a0 lea.l -$7660(a4), a0 ; msg_next_mission
0156ec 93c9 suba.l a1, a1
0156ee 45ecc16c lea.l -$3e94(a4), a2 ; ticker_text
loc_0156f2:
0156f2 4a1a tst.b (a2)+
0156f4 66fc bne.b $156f2
0156f6 554a subq.w #$2, a2
0156f8 4ebaf97e jsr $15078(pc) ; format_to
loc_0156fc:
0156fc 297c0002716aa7b8 move.l #$2716a, -$5848(a4) ; ticker_message | ticker_text
015704 397cffffa3be move.w #$ffff, -$5c42(a4) ; g_0253bc
01570a 4cdf0701 movem.l (a7)+, d0/a0-a2
01570e 4e75 rts
/* src/targets.c, lines 806-835 */
/* orig 0x015694 - the map's last island neutralised: the next mission, and after the rank's
* last mission (missions_per_rank, 0x025548) the first of the next rank, at most rank 6,
* with balloons_on set (the promotion's balloons over the carrier and a life more at the
* next mission's start, 0x010154); its message is appended to ticker_text over the last
* character before the NUL (`subq.w #2` behind the NUL), the ticker shows ticker_text at
* once, and 0x0253BC says the mission is won: the next time the weapon menu is up, main
* goes on to the next mission (0x010132, M7's). */
void wof_mission_won(void)
{
uint16_t end = 0;
uint32_t format;
wof_g.mission_number++;
if ((int16_t)word_at(0x025548u + 2u * (uint32_t)(uint16_t)wof_g.rank_played) >=
(int16_t)wof_g.mission_number) {
format = 0x02399Eu;
} else {
wof_g.mission_number = 1;
wof_g.rank_played++;
if ((int16_t)wof_g.rank_played > 6)
wof_g.rank_played = 6;
wof_g.balloons_on = 0xFF;
format = 0x023A5Au;
}
while (end < sizeof wof_g.ticker_text && wof_g.ticker_text[end])
end++;
wof_ticker_format(format, 0, (uint16_t)(end - 1u));
wof_g.ticker_message = 0x02716Au;
wof_g.g_0253bc = -1;
}
Nothing here touches the score: the manual's bonus for a whole mission (page 10) is the last island's bonus or the last ship's score, paid just before the call (chapters 15 and 16).
The next mission¶
The won flag waits for an aircraft in the hold: main tests it at the head of its inner loop with the weapon menu, which is up only there. So the next mission begins with the winner if it landed, or with the next aircraft if it was lost; if the winner was the last Hellcat and is lost, the game is over instead.
The steps come in a fixed order: the flags cleared, the sound slots emptied, the ticker stopped, both views faded out, the mission's assets freed. If the promotion's flag is set, the lives get one more, the addq.b on the left. Then the night is chosen, the new map loaded and its briefing shown, where Control-R goes back to the rank selection. After the briefing come the dashboard's pictures, the day's or the night's as just chosen, the display's setup, the shapes, the lists and the sounds; then bra.w jumps into the mission's reset, which ends at step S, where a mission's loop begins. On the right, the port's coroutine with its goto.
; re/Wings.lst 0x010132-0x01018A: main [asm], a part of 0x010006-0x01020D
010132 426ca3be clr.w -$5c42(a4) ; g_0253bc
010136 422ca366 clr.b -$5c9a(a4) ; weapon_menu_up
01013a 4eac8062 jsr -$7f9e(a4) ; -> sound_slots_clear
01013e 42aca7b8 clr.l -$5848(a4) ; ticker_message
010142 4eba6a78 jsr $16bbc(pc) ; ticker_clear
010146 3f3c0001 move.w #$1, -(a7)
01014a 4eba729a jsr $173e6(pc) ; fade_out_pair
01014e 544f addq.w #$2, a7
010150 4eba10e2 jsr $11234(pc) ; free_mission_assets
010154 4a2ca35f tst.b -$5ca1(a4) ; balloons_on
010158 67000006 beq.w $10160
01015c 522ca35e addq.b #$1, -$5ca2(a4) ; lives
loc_010160:
010160 4eba109a jsr $111fc(pc) ; choose_night
010164 4eac8272 jsr -$7d8e(a4) ; -> dashboard_invalidate
010168 4eac808c jsr -$7f74(a4) ; -> map_load
01016c 4eac81be jsr -$7e42(a4) ; -> mission_briefing
010170 4a00 tst.b d0
010172 6600fef2 bne.w $10066
010176 4eba63c4 jsr $1653c(pc) ; load_dash_assets
01017a 4eac81ca jsr -$7e36(a4) ; -> mission_display_setup
01017e 4eac8098 jsr -$7f68(a4) ; -> load_ship_shapes
010182 4eba51d6 jsr $1535a(pc) ; build_master_lists
010186 4eac809e jsr -$7f62(a4) ; -> sounds_load
01018a 6000ff46 bra.w $100d2
/* src/front.c, lines 804-832, a part of mission (lines 753-847) */
if (wof_g.g_025364 && wof_g.g_0253bc) {
/* 0x010132: the mission is won (0x0253BC, set only by mission_won 0x015694) and
* the aircraft is back in the hold with the weapon menu up: the campaign's next
* mission. mission_won has already counted the mission on, or the rank with the
* promotion, whose balloons_on gives the extra Hellcat here (the manual, page 8). */
wof_g.g_0253bc = 0;
wof_g.g_025364 = 0;
wof_sound_slots_clear(); /* orig 0x011F4E */
wof_g.ticker_message = 0;
wof_ticker_clear();
CO_CALL(c, &wof_f.co_fade, wof_fade_out_pair());
wof_free_mission_assets();
if (wof_g.balloons_on)
wof_g.lives++; /* 0x01015C: addq.b */
wof_choose_night(); /* orig 0x0111FC */
wof_dashboard_invalidate();
wof_map_load();
CO_CALL(c, &wof_f.co_stage, mission_briefing());
if (wof_f.briefing_result) { /* 0x010172: tst.b d0 */
wof_f.mission_end = 2;
CO_RETURN(c);
}
wof_load_dash_assets();
wof_trace_add("mission", wof_g.rank_played, wof_g.mission_number, 0, 0, 0, 0);
CO_CALL(c, &wof_f.co_setup, wof_mission_display_setup());
wof_load_ship_shapes();
wof_build_master_lists();
wof_sounds_load();
goto next_mission; /* 0x01018A: bra 0x0100D2 */
There is no rank selection and no campaign's reset: the score, the enemy plane counter and the lives carry over. Day and night are chosen here, as chapter 11 told: maps a to g are always flown by day, h to o by the top bit of a fourth draw of the beam, and nothing clears the night flag between campaigns.
The briefing shows the rank's name, the mission number and the manual's objectives (page 10), two counts from the map's first walk (chapter 13, whose table gave the ships): the islands that hold a target, only two of map l's three, and the enemy ships, a count that goes down as they sink, so a loaded game's briefing names the ships still to sink.
| Map | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| islands | 1 | 2 | 3 | 2 | 3 | 2 | 3 | 3 | 3 | 0 | 3 | 2 | 4 | 3 | 3 |
| ships | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 2 | 3 |
Three Hellcats begin a campaign (page 10); each aircraft lost costs one (chapter 14), each promotion adds one, and the dashboard's drum shows at most nine.
The promotion, and no end¶
From the next pass the promotion's flag lets the free records of the Balloons pool rise over the carrier (chapter 15). And it is still set at the next mission, which gives the Hellcat the manual promises (pages 8 and 10) before the mission's reset clears it. The balloons themselves are never cleared: with the flag off, the records in use are neither drawn nor moved, and in the script promote_a nineteen of the twenty stood unseen through map d's flight. They fly on at the next promotion.
After map o, the last rank's last mission, the promotion runs once more: the rank stays 6 and the mission number becomes 1, so the next map is m. The campaign goes round m, n and o for ever, each round with a promotion's message, balloons and a Hellcat more. There is no end sequence: a game ends only when the last Hellcat is lost, when the carrier has sunk, or when the player quits.
The game over follows the last Hellcat's loss, after chapter 14's wait, or the carrier's sinking. If the aircraft is aboard when the carrier is 33 rows down (chapter 16), it goes into the sea with every life, and a count of 100 passes, run down by the routine that draws the game-over picture, ends the game; a flying aircraft is the last, since none follows a sunk carrier. main fades out and, unless a demo was played, shows the high scores; then the rank selection begins a new campaign.
The high scores¶
The high-score file, highscore, chapter 3's, is 360 bytes: ten rows of 36, best first. A row holds a score, the rank reached and a name of 30 bytes, of which at most 16 are ever typed. The high-score screen reads the file and sorts it every time, a bubble sort that swaps whole rows until a sweep swaps nothing. Only a score greater than the tenth's makes the screen ask for a name; the new row replaces the tenth, the lowest after the sort, and the table is sorted again and written. Nothing else writes the file, and the screen comes only at the outer loop's end, so it is written at most once a game.
A missing file reads as ten empty rows: score 0, rank 0 and a name of twelve spaces. The disk's own file holds exactly that in its last three rows, so it had been played into seven times before this copy of the disk was made. Control-C in flight deletes the file without a question; the manual lets it work only after Control-D, which this executable lacks, as chapter 1 told (page 12). A developer's reset that writes ten rows of 5,000 points is there too, and nothing calls it. The port keeps the file in the browser's storage (chapter 23).
The saved game¶
Control-G saves only while the aircraft is on the carrier (page 11); the dialog puts wof. in front of the name typed. Chapter 3 described the result.
Writing and reading share the saved game's walker, a routine that goes through the file's pieces in a fixed order and hands each to a callback, a routine passed to it to be called back: the writer's callback writes the piece, the reader's reads it. A piece is direct, the memory at an address, or pointed, the block whose address is stored there. Since the write and the read run the same walk, the read finds every piece where the write put it.
| # | Piece | Kind | Length |
|---|---|---|---|
| 1 | the raw part | direct | 0x84A, 2,122 bytes |
| 2 | the map's length | direct | 2 bytes |
| 3 | the map's records | pointed | the map's length |
| 4 | a gun list for each enemy ship | pointed | 14 bytes a gun |
| 5 | the pillboxes | pointed | 14 bytes a record |
| 6 | the soldiers | pointed | 8 bytes a record |
| 7 | the dug-outs | pointed | 16 bytes a record |
| 8 | the barracks | pointed | 16 bytes a record |
The lengths are worked out in 16 bits, as the compiled C works them. On the left, a call of a routine that does nothing, which no note explains; the pillboxes' count, a byte widened with its sign by ext.w and multiplied by 14 with mulu.w; and the soldiers' count, a word shifted three places. The maps' counts stay far below 128, where a byte's sign would matter; the port keeps the widths all the same, so its file is the original's even for a crafted count.
; re/Wings.lst 0x015F9A-0x015FE2: save_walk [C], a part of 0x015EC2-0x016031
015f9a 4ebafdc6 jsr $15d62(pc) ; save_nothing
015f9e 3f3c0001 move.w #$1, -(a7)
015fa2 102ca387 move.b -$5c79(a4), d0 ; target_count_f
015fa6 4880 ext.w d0
015fa8 c0fc000e mulu.w #$e, d0
015fac 7200 moveq #$0, d1
015fae 3200 move.w d0, d1
015fb0 2f01 move.l d1, -(a7)
015fb2 486ca506 pea.l -$5afa(a4) ; target_records_f
015fb6 2f2d000c move.l $c(a5), -(a7)
015fba 206d0008 movea.l $8(a5), a0
015fbe 4e90 jsr (a0)
015fc0 4fef000e lea.l $e(a7), a7
015fc4 3f3c0001 move.w #$1, -(a7)
015fc8 302ca3c6 move.w -$5c3a(a4), d0 ; soldier_count
015fcc e740 asl.w #$3, d0
015fce 7200 moveq #$0, d1
015fd0 3200 move.w d0, d1
015fd2 2f01 move.l d1, -(a7)
015fd4 486ca502 pea.l -$5afe(a4) ; soldier_records
015fd8 2f2d000c move.l $c(a5), -(a7)
015fdc 206d0008 movea.l $8(a5), a0
015fe0 4e90 jsr (a0)
015fe2 4fef000e lea.l $e(a7), a7
/* src/dialog.c, lines 185-210 */
/* orig 0x015EC2 - the walker of the saved game, for the write and the read alike: the
* memory from object_records up to target_records_4, map_length, the map's record list,
* the gun list of every ship whose +4 and +0x12 are set, and the four tables of map_scan,
* each as long as its count says. After the map it sets map_extent and map_records_end
* from map_length, the latter to the list's end where map_load leaves it a word before, so
* a save changes the running game there. */
static void save_walk(void (*put)(uint32_t addr, uint32_t len, uint16_t flag))
{
put(0x024CAEu, 0x0254F8u - 0x024CAEu, 0);
put(0x0253C6u, 2u, 0);
put(0x024628u, (uint32_t)(int32_t)(int16_t)wof_g.map_length, 1); /* ext.l */
wof_g.map_extent = (uint16_t)(wof_g.map_length << 2); /* asl.w #2 */
wof_m.map_records_end[0].off = (uint32_t)(int32_t)(int16_t)wof_g.map_length;
for (uint32_t i = 0; i < 5u; i++) {
const wof_ship_t *ship = &wof_m.ship_records[i];
if (ship->present != 0 && ship->w12 != 0) /* +4, +0x12 */
put(0x025460u + 0x1Eu * i + 6u,
(uint16_t)((uint32_t)(uint16_t)ship->gun_count * 14u), 1); /* mulu.w */
}
save_nothing();
put(0x025504u, (uint16_t)((uint32_t)(uint16_t)(int16_t)(int8_t)wof_g.target_count_f * 14u), 1);
put(0x025500u, (uint16_t)(wof_g.soldier_count << 3), 1);
put(0x0254FCu, (uint16_t)((uint16_t)(int16_t)(int8_t)wof_g.target_count_3 << 4), 1);
put(0x0254F8u, (uint16_t)((uint16_t)(int16_t)(int8_t)wof_g.target_count_4 << 4), 1);
}
A gun list goes in for each ship present whose record holds a score; the player's carrier scores 0 (chapter 16), so it is never in the file. The raw part, the first piece, memory as it stands from the object records on, holds every count the later pieces need: a file describes its own layout, and tools/savegame.py decodes one by that alone. The walker also writes: it sets the map's end pointer two bytes past where map_load leaves it, so a mission's first saving moves that pointer of the running game, as the port's does too.
The figures
| The figures | |
|---|---|
| The raw part and the map's length | 2,124 bytes |
| The smallest saved game, map a | 4,258 bytes |
| Chapter 3's saved game, map c | 6,866 bytes |
| The largest saved game, map m | 11,516 bytes |
A saved game's size follows from its map alone. Chapter 3's file was saved on a real Amiga in map c, the first rank's third mission.

Chapter 3's saved game to scale, its pieces in the walker's order, and the raw part enlarged: its regions at the port's named fields, the bytes of no field dark, the four pointer fields in gold.
The raw part holds the object records, the player's record, the ships' parked aircraft, the enemy aircraft records, the airfields, the score, the lives, the flags, the rank and mission number, the islands, the five ship records, and two options, the vertical flip and the music switch. 75 of its bytes belong to no variable the port keeps, nothing in a mission writes them, and in chapter 3's file all are 0. The sixteenth object record, where an enemy torpedo runs, lies outside it, so no enemy torpedo comes back with a loaded game; so do the pools, the sound slots and the ticker's text.
Four kinds of field in the raw part hold addresses in the machine that saved: the player's shape, a shape the tick keeps for level flight, the torpedo's shape, and each ship's gun list. These are the pointer fields.
Loading a game¶
The reader's callback puts a direct piece back where it lies; for a pointed piece it allocates a new block, reads into it and stores the block's address, only when the read delivered the whole length. A file that cannot be opened makes the program exit.
Two paths lead there. From the rank selection, the load item opens the dialog, a cancel going back to the menu; the music stops, the map is freed, the file read, the briefing shown, and main runs the mission's setup without the mission's reset, so the file's state becomes the mission's. In flight, Control-L, refused while a demo runs, frees the dashboard's shapes, the sounds and the demo's buffer, loads, shows the briefing and runs one tick, and the inner loop goes on in the loaded game's mission, without step S.
The pointer fields come back as the saving machine left them. The player's and the torpedo's shapes do no harm, since the next tick sets both from the frame's name before anything reads them, and the gun lists are replaced by the new blocks. The one that matters is the shape kept for level flight: the player's drawing reads it in the hold before any level tick sets it again. What the original draws from another machine's pointer, no run has shown.
The port derives all four. In its own files that field holds a shape handle, the port's small number for a shape, at most four hex digits; from a real Amiga it holds an address far larger. The frame's name says which shape the player's pointer beside it names. Where that shape's record lies in its container, whose layout chapter 3 showed, places the container in the saving machine's memory. The other pointer's distance from there names its record: for chapter 3's file wh0f of the Hellcat's shapes, the one the original's own saved games hold. A pointer that names no record draws nothing until a level tick sets the field. In the C, the last loop compares each record's place with the pointer:
/* src/dialog.c, lines 353-394 */
/* The shape of hellcat.shp that a pointer of the saving machine names (0x02541A): the
* pointer is the long the file holds, `player` the player's shape pointer beside it, whose
* shape the frame name gives. In the port's own file both are shape handles, and the
* handle is taken when it names a shape of hellcat.shp. In a machine's file both are
* addresses of that machine's memory: the player's pointer and its shape's place in the
* container (6 + 8 x count + the record's offset, as load_file leaves a PPkc file) give the
* container's address there, and the pointer's distance from it gives the record it names.
* Anything else names no shape. */
static uint16_t saved_hellcat_shape(uint32_t pointer, uint32_t player, uint32_t frame)
{
const wof_container_t *c = &wof_assets.c[WOF_C_HELLCAT];
uint32_t mark, len = 0, base, at;
const uint8_t *file;
uint16_t count, found = WOF_SHAPE_NONE;
int16_t own;
if (pointer == 0)
return WOF_SHAPE_NONE;
if (pointer < 0x10000u || player < 0x10000u) {
if (pointer >= 0x10000u || (pointer >> 11) != (uint32_t)WOF_C_HELLCAT + 1u ||
(pointer & 0x7FFu) >= c->count)
return WOF_SHAPE_NONE;
return (uint16_t)pointer;
}
own = wof_shape_find(c, frame);
if (own < 0)
return WOF_SHAPE_NONE;
mark = wof_arena_mark();
file = wof_load_file(c->file, &len);
if (file && len >= 6 && file_long(file) == 0x50506B63u) { /* 'PPkc' */
count = (uint16_t)(file[4] << 8 | file[5]);
at = 6u + 4u * count; /* the offsets */
if (len >= at + 4u * count && (uint16_t)own < count) {
base = player - (6u + 8u * count + file_long(file + at + 4u * (uint32_t)own));
for (uint16_t i = 0; i < count; i++)
if (base + 6u + 8u * count + file_long(file + at + 4u * i) == pointer)
found = wof_shape_handle(WOF_C_HELLCAT, (int16_t)i);
}
}
wof_arena_release(mark);
return found;
}
The file also brings back the vertical flip, and the player's remembered preference wins (chapter 1). The port refuses a file it cannot hold before the load begins: one missing, shorter than its own counts ask, or with counts beyond the port's tables. The original's program exits on the first and reads the others into blocks of any size; the port keeps each block at a fixed place of a fixed size, so its dialog leaves as a cancel and the game goes on.
The demo¶
The manual promises a self-running demo after loading (pages 2 and 3), chapter 3's attract demo, which this disk does not carry. main looks at its argument count once, the lines chapter 4 read: started with an argument, the program records every game from the rank chosen on. The rank selection then allocates a buffer of 0x1388 bytes, 5,000, seeds the random generator from the beam, the only place the program seeds it, and stores the rank in entry 0, the buffer's first byte.
From step S the VBlank server stores the input byte of each input sample in the next entry until the pass has two, and run_queued_ticks, whose first lines chapter 7 left to this chapter, waits VBlank by VBlank until they are in. So a demo's pass has two ticks whatever it costs, and the ticks repeat whatever the machine's speed. A recording runs more where an aircraft is lost: the restart's input samples, taken inside a tick, are queued and run but not kept, while a playback queues nothing then. A full buffer, 4,998 input bytes, ends the game, and at its end demo_end writes 0xFF after the last entry and saves the buffer as wofdemo.

The port's recording kept for its replay test: the rank, then input bytes, 0x04 for right, 0x05 right and forward, one 0x24 with a tap, one an input sample; the end after 260 of them, and zeros.
Left alone for 1,800 rounds of its menu, a round being one VBlank, the rank selection asks for the demo. It loads wofdemo, and would read a shorter file past its end; without the file a normal game starts at the rank under the cursor. It seeds the generator from the beam, takes the rank from entry 0, and from step S the server takes the demo's bytes in place of the stick, two a pass. The 0xFF ends the playback, as does a full buffer; on the left, a byte taken only while the pass still wants one, and beside it the port's.
; re/Wings.lst 0x011786-0x0117D0: vblank_server [asm], a part of 0x011754-0x011965
011786 0c6c0001bd4e cmpi.w #$1, -$42b2(a4) ; demo_mode
01178c 66000046 bne.w $117d4
011790 4a6cbd56 tst.w -$42aa(a4) ; demo_step_s
011794 670000ac beq.w $11842
011798 4a6cbd46 tst.w -$42ba(a4) ; demo_bytes_owed
01179c 670000a4 beq.w $11842
0117a0 536cbd46 subq.w #$1, -$42ba(a4) ; demo_bytes_owed
0117a4 206cbd50 movea.l -$42b0(a4), a0 ; demo_buffer_ptr
0117a8 322cbd54 move.w -$42ac(a4), d1 ; demo_index
0117ac 7000 moveq #$0, d0
0117ae 10301000 move.b (a0, d1.w), d0
0117b2 b03c00ff cmp.b #$ff, d0
0117b6 67000010 beq.w $117c8
0117ba 5241 addq.w #$1, d1
0117bc 3941bd54 move.w d1, -$42ac(a4) ; demo_index
0117c0 b27c1386 cmp.w #$1386, d1
0117c4 65000006 bcs.w $117cc
loc_0117c8:
0117c8 50eca3c4 st.b -$5c3c(a4) ; quit_flag
loc_0117cc:
0117cc 3940c368 move.w d0, -$3c98(a4) ; input_byte
0117d0 60000006 bra.w $117d8
/* src/input.c, lines 311-333, a part of wof_vblank (lines 259-370) */
if (wof_g.demo_mode == 1) {
/* 0x011790: a playback takes the byte from the demo, and only while step S has set
* 0x026D54 and run_queued_ticks' count 0x026D44 still wants one: nothing is queued
* otherwise. A 0xFF byte ends the playback and is queued itself; so is the entry
* that brings the index to 0x1386. */
uint16_t d1;
if (!wof_g.demo_step_s || !wof_g.demo_bytes_owed) {
wof_vblank_ticker();
return;
}
wof_g.demo_bytes_owed--;
d1 = wof_g.demo_index;
d0 = demo_byte(d1);
if (d0 == 0xFFu) {
wof_g.quit_flag = 0xFF;
} else {
d1++;
wof_g.demo_index = d1;
if (d1 >= WOF_DEMO_ENTRIES)
wof_g.quit_flag = 0xFF;
}
wof_g.input_byte = d0;
Fire ends a playback too; after a playback a flag skips the high scores, and a pause takes no byte.
The game has one random routine, and every draw is a constant exclusive-ored with the beam's position at the moment of the call (chapter 6). A demo's seeding sets only that constant, from the beam itself, so on a real machine neither the constant nor any draw repeats, and a playback follows its recording only until a draw matters. Two values run on from mission to mission besides, and are not in the file: the swell's phase, which rocks the carrier and its lift (chapter 13), and the night flag.
The port's milestone asks that a recorded demo replay identically after a page reload. So beside the demo it writes a seed file, wofdemo.seed, 12 bytes: the state of the entropy stream that serves every beam value, the seed's included, a hash of the demo, the swell's phase and the night flag. A playback whose demo matches the hash starts from all three, so after a reload it runs as the first playback did, and as the recording did if that lost no aircraft. A wofdemo without it, one from a real Amiga, plays with everything as it stands. The port records with a development key (chapter 23).
The figures
| The times, on PAL, derived | |
|---|---|
| The rank selection left alone, 1,800 VBlanks | 36 seconds |
| The longest demo, 4,998 input bytes, one a tick | about 400 seconds |
What the port made of it¶
The next mission runs in the mission's coroutine, in the original's order, with a goto into the mission's reset (chapter 22). The port reads mission_won's table by address, and choose_night's with the original's signed index, not modulo the table's length, which would differ for a rank past the table, as the oracle's random states give.
The write callback takes each byte by its address from the port's registered state, the variables kept under their original addresses, from the block behind a pointer for a pointed piece, and from the executable's image where nothing writes. A pointer field gets a shape handle, or 1 where a ship's list is set, since the port keeps no address there. The file goes to the file system in one piece, of at most 12,412 bytes, the largest saved game the port's tables allow.
Two stand-ins that had marked values the port did not yet produce fell: the score's text and the ticker's message. The score is formatted as the system's formatter, RawDoFmt, does with %07ld, the sign inside the zeros, so a negative score, which a file can bring, reads 000-123; the ticker's message stays in the registered state. The demo's buffer is registered too, so every byte recorded or played is compared. And the save script found the play screen left switched off after a dialog, the fault chapters 4 and 9 told.
How it is held¶
Six mission scripts of the campaign run in the closed loop from the program's start, every tick and pass compared through the win and the next mission's first ticks, and through a game saved in the hold; three run only in the suite's long run. A poke of the mission number and the rank reaches the promotion and the cap.
Maps k and l, the two one-mission ranks, are beyond the autopilot, so the chain through them uses pokes: one script sets each enemy ship's hits and the islands left to 0 at every mission's reset, eight missions and seven wins. Two wins in a row by play were not reached, the dug-outs' fire taking the oil in the low hunts while the soldiers the bombs let out kept coming, so the islands held; and no script saves with a weapon in flight, since the autopilot's landing takes longer than any weapon flies. The chain and the save script also hold at one and three VBlanks a pass, where the save script found the upper word chapter 9 told; the open loop finds no step that differs, and the completeness list accounts for every address the scripts write.
The saved file is compared byte for byte with the one the headless original wrote; every differing byte is named by its field, and only the pointer fields may differ:
# tests/test_campaign.py, lines 131-160
@pytest.mark.parametrize('name', ['save_a'])
def test_the_saved_file_is_the_originals_but_for_its_pointers(ported, name):
"""The file save_a's save in the hold writes (wof.save, map f) is the one the headless
original wrote in the same run, byte for byte, except where the raw part holds a pointer
to the original's own memory: the player's shape, the two shape pointers of the tick and
the cruise ship's gun list. Every differing byte is listed with its field and reason."""
if (name, 2) not in SAVED:
assert_closed(*closed_loop(ported, name))
originals, ports = SAVED[(name, 2)]
saved = [n for n in originals if n != 'wof.mission 3']
assert saved == ['wof.save'], saved
original, port = originals['wof.save'], ports.get('wof.save')
assert port is not None and len(port) == len(original) == 6424, (len(original), port and len(port))
fields = savegame.registry()
differing = {}
for i, (a, b) in enumerate(zip(original, port)):
if a == b:
continue
field = savegame.field_of(savegame.RAW_START + i, fields) if i < savegame.RAW_LENGTH else None
assert field is not None and field[1] in POINTER_REASONS, (
'byte %d (0x%06X) differs, original %02x, port %02x, in %s' % (
i, savegame.RAW_START + i, a, b, field))
differing.setdefault(field, []).append(i)
assert sorted(differing) == [('g_02541a[0].s', 'shape'), ('player[0].shape', 'shape'),
('ship_records[2].guns', 'pool'),
('torpedo_shape[0].s', 'shape')], sorted(differing)
guns = savegame.SHIPS + 2 * savegame.SHIP_SIZE + 6 - savegame.RAW_START
assert port[guns:guns + 4] == b'\0\0\0\1', port[guns:guns + 4]
info = savegame.summary(port)
assert info['exact'] and info['map'] == 'f' and info['ships'] == ['guns cruiseship'], info
Under the oracle, mission_won and choose_night are held over 2,000 random states each, the walker's writes and reads over 600 each, and the score's formatting over 616 scores against the ROM's own RawDoFmt. Eight more scripts load chapter 3's file from the rank selection and a saved game by both paths, record a demo and play it back. After a load the port's state is the original's, the derived fields what the original's first tick makes of them; the port's recording is the original's file, and the original plays it as the port does.
The recording of the figure is replayed in the native library and in WebAssembly against the state's fingerprint at each of the run's 2,021 input samples, one every fourth VBlank from the program's start through the idle rank selection and the playback. The page tests save, reload and load, and record a game that loses no aircraft, reload, and find both playbacks equal to it at every input sample (chapter 24).
Controls, chapter 8's breaks of the port on purpose, were each caught:
| What was changed in the port | Script | Where it first showed |
|---|---|---|
| the promotion one mission early | the chain | pass 243, map n won |
| the extra Hellcat not given | the chain | the first tick of map k |
| the soldiers' piece a record short | the save script | the file's size |
For the developer
The shape the tick keeps for level flight lies at 0x02541A; a shape's record lies 6 plus 8 times the shape count plus its offset bytes after its container's start. The scripts are tools/m7_scripts.py, whose trace NAME prints the campaign tick by tick; the controls tools/m7_controls.py; tools/savegame.py with --fields names every field of a raw part and the bytes of none.
What comes next¶
The chapter in one sentence: two tables of maps by rank and mission number hold the campaign together, and a promotion that stops at rank 6 makes it endless; a reload keeps the high-score file, a saved game's memory and a demo's input bytes, the port's seed file beside them. Chapter 18 takes up the sound: the slots the next mission empties, the music a load stops, and Paula's channels.
Further reading¶
re/notes/campaign.md: "A mission won", "The next mission", "The saved game" and "The loader".re/notes/demo.md: "Recording", "Playback and the attract mode" and "The port";re/notes/highscore.md.re/notes/porting-m7.md: "The scripts", "Part 2: the port" and "Part 2: how it is held".src/front.c,src/dialog.c,src/hiscore.c,tools/savegame.py,tests/test_campaign.py,tests/test_loader.py,tests/test_demo.pyandtests/test_replays.py.original/manual.txt, the game's manual: pages 2 and 3 for the demo, 4 for the ranks, 8 and 10 for the Hellcats and the objectives, 11 for the high scores and saving, 12 for the commands.