main() and Outer Loop

The first programmer-defined function that runs in any C program is named main(). It is called by the C runtime and takes two parameters: argc (the number of command-line arguments the program was run with) and argv (the values of each of these arguments).

This is where everything begins.

main()

Cosmo’s Cosmic Adventure requires an 80286 processor due to the way it was compiled (and because it moves an objectively large amount of graphics data with every frame it draws). First and foremost, the CPU needs to be tested to ensure it meets this requirement, with a graceful fallback message if the system is not powerful enough. The main() function is responsible for this check, and it is small enough to speak for itself:

int main(int argc, char *argv[])
{
    int cputype = GetProcessorType();

    if (cputype < CPU_TYPE_80188) {
        byte response;

        /* Grammatical errors preserved faithfully */
        printf("You're computer appears to be an 8088/8086 XT system.\n\n");
        printf("Cosmo REQUIRES an AT class (80286) or better to run due "
            "to\n");
        printf("it's high-speed animated graphics.\n\n");
        printf("Note:  This game will crash on XT systems.\n");
        printf("Do you wish to continue if you really have an AT system or "
            "better (Y/N)?");

        response = getch();
        if (response == 'Y' || response == 'y') {
            InnerMain(argc, argv);
        }

        exit(EXIT_SUCCESS);
    } else {
        InnerMain(argc, argv);
    }
}

main() does not share its compilation unit with any other functions – this is basically all that is present in the file. This is the only C function in the game that is compiled in 8086/88 compatibility mode, and for good reason: If the user runs the program on an original IBM PC or XT with the 8088 processor, a 286-optimized main function would not execute correctly and the prompt would never show.

The actual CPU detection routine in GetProcessorType() and its return values are covered in detail elsewhere.

The happy path through this function is that the user has a 286 (technically, anything equal to or better than a CPU_TYPE_80188). In this case, execution is passed to InnerMain() along with the values for argc and argv.

If the user appears to have a processor that is incapable of running the game (an 8086/88 or an NEC V20/30), a text warning and prompt are displayed via printf(). getch() reads the user’s response to the prompt without echoing it to the screen. If the user enters Y or y, it attempts to call InnerMain() as above. Otherwise exit() is called to return to DOS with an EXIT_SUCCESS status code.

InnerMain() never returns, so control never comes back here.

InnerMain()

The InnerMain() function accepts the same arguments as main() and receives the same values in each. The function parses the command line arguments first:

void InnerMain(int argc, char *argv[])
{
    if (argc == 2) {
        writePath = argv[1];
    } else {
        writePath = "\0";
    }

If there was exactly one command-line argument provided (argc == 2),1 that argument is used as the write path. Otherwise the writePath is empty, which means “use the current working directory.”

The startup function is called next:

    Startup();

Startup() performs quite a bit of hardware detection, memory allocation and file loading. Once initialization is complete, the outer loop is entered.

How’d it get two main()s?

At one point, InnerMain() was probably the actual main function. Once it was determined that the game would require a 286 to run, it was probably more straightforward to do the CPU detection in a separate outer function that wrapped the old main function than to try to refactor the existing code to work on an 8088.

Write Path

Most users run the game by typing COSMOx at the DOS prompt without providing any arguments. This is not the only way.

InnerMain() supports exactly one optional command line argument. This is interpreted as either an absolute or relative directory name, and the value is used as the game’s write path. If unspecified (i.e. there were no additional command line arguments, or too many of them) the write path defaults to the current working directory. (Under DOS, this is the directory the user CD’d into before running the game.) Some example invocations:

  • COSMO1 C:\MYDIR: Uses C:\MYDIR as the write path.
  • COSMO1 SUBDIR: Uses the directory SUBDIR inside of the current working directory.
  • COSMO1 DEEPER\SUBDIR: Uses the directory SUBDIR inside of the directory DEEPER inside of the current working directory.
  • COSMO1: Uses the current working directory (default, since there was not an argument provided).
  • COSMO1 EXAMPLE DOESNOTWORK: Uses the current working directory (default, since there were too many arguments provided).

In all cases, these are the files written to the write path:

It appears as though the intent of this feature was to provide a way for the user to play the game from a read-only working directory, but still save data in a different location that is writable. This would allow the game to be run directly from a read-only “game disk” with save files stored separately on the hard drive or a secondary “save disk.” (The game is far too large to fit on a single 1,440 KiB floppy disk, but who knows exactly which formats the creators may have had in mind.)

For this to work as intended, the specified directory must exist and be writable – no attempt is made to create the directory or verify its usability. If an invalid or unwritable path is provided, any attempts to load or save these files will fail – in many cases silently. This produces odd behavior that some have interpreted as a special cheat mode.

Outer Loop

InnerMain() continues with the outer loop of the game, which has no termination condition. The only way out of it is to use the ExitClean() function (which ultimately asks DOS to terminate execution of the program).

    for (;;) {
        demoState = TitleLoop();

TitleLoop() handles showing the title screen graphics, main menu, and most of the sub-menus contained within. TitleLoop() doesn’t return until the point where gameplay needs to start – either under direct player control or by playing back a previously recorded demo. demoState is a global variable to track this state. TitleLoop() also performs some initialization of game state – the most relevant effect is initializing levelNum to 0.

        InitializeLevel(levelNum);
        LoadMaskedTileData("MASKTILE.MNI");

        if (demoState == DEMO_STATE_PLAY) {
            LoadDemoData();
        }

InitializeLevel() handles loading and setup of the global variables needed to play the level specified by levelNum.

The call to LoadMaskedTileData() is interesting. All it does is load the contents of the map’s masked tile image data into memory. The reason why this needs to happen here is because its memory block (pointed to by maskedTileData) is also used to hold the AdLib music that plays during the title loop and main menu. When the program switches between the main menu and gameplay mode, this memory must be rewritten with the data required for that context.

If demoState indicates a demo is being played back, LoadDemoData() is called to read the demo data into memory and initialize playback variables.

        isInGame = true;
        GameLoop(demoState);
        isInGame = false;

Next comes GameLoop(), framed by a toggle of the isInGame flag. This function does not return until the player wins the game or quits, or until the demo ends if running in that mode.

        StopMusic();

        if (demoState != DEMO_STATE_PLAY && demoState != DEMO_STATE_RECORD) {
            CheckHighScoreAndShow();
        }

        if (demoState == DEMO_STATE_RECORD) {
            SaveDemoData();
        }
    }
}

At this point, gameplay has stopped and we are handling the transition back into the title loop and main menu.

StopMusic() ensures that there is no music or “ringing” notes still playing.

If demoState indicates no demo is being played back or recorded, CheckHighScoreAndShow() is called to see if the player’s score qualifies for entry into the high score table. This also displays the high score table before returning.

Finally, if a demo is being recorded, SaveDemoData() is called to flush the recorded demo data to disk.

The loop then repeats with the title screen. The infinite nature of this outer loop is plainly visible within the program: From the main menu, start a new game… Play for a bit… Quit… Enter your name into the high score table… Return to the main menu. This cycle can be repeated ad nauseam. In order to truly quit back to the DOS prompt, something within the loop must ultimately call exit() to request program termination from the DOS API – there is no other provision to break out of the outer loop.


  1. Yup, argc is 2 when there is one command line argument provided. The argument list always contains at least one element; argv[0] is the name of the program that was invoked, in this case something like C:\PATH\TO\COSMOx.EXE↩︎