blog:x68_launcher_2

X68000 Game Launcher - #2: Filesystem tools, data scraping & config loader

This library uses the following common header fragment:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dos.h>
 
#include "newlib_fixes.h"
#include "data.h"
#include "fstools.h"

We also have a few constants:

#define FS_VERBOSE	0	// Enable/disable fstools verbose/debug output
#define DIR_BUFFER_SIZE	65	// Size of array for accepting directory paths
#define MAX_DRIVES	26	// Maximum number of drive letters
#define DRIVE_LETTERS	{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }

Hopefully most of those make sense.

The only interesting piece of information here is the newlib_fixes.h header. This contains some fixes to functions that were included in the Lydux GCC standard C library. The following functions in the standard C library are replaced:

  • extern int _dos_files(struct dos_filbuf *, const char *, int);
  • extern int _dos_nfiles(struct dos_filbuf *);
  • extern int _dos_exfiles(struct dos_exfilbuf *, const char *, int);
  • extern int _dos_exnfiles(struct dos_exfilbuf *);

The only reason those fixes are here is that the versions in the standard C library are mis-labelled. No other changes are applied.

The fstools.c library contains additional calls built on top of basic Hudson68k IO calls and C library system calls to search directory paths, extract drive letters from path names, store the current path whilst changing drives, etc.

The main function which is called from outside the library is findDirs() which is called by the main application control loop.


dirFromPath()

Input

  • char *path - The full path to be extracted
  • char *buffer - An empty string buffer into which the directory name will be copied

Output

  • int

Returns 0 on success, -1 on error.

Description

Splits a fully qualified path, such as “A:\Games” and returns the “Games” part. Also copes with multi-level paths, such as “A:\Stuff\Games\Games_All”, returning “Stuff\Games\Games_All”.

This is implemented since we have to change drive and directory seperately (Hudson68k provides two seperate calls).

Example

int status;
char buffer[99];
 
status = dirFromPath("A:\\Games", buffer);
if (status == 0){
   printf("Dirname is: %s", buffer);
}

dirHasData()

Input

  • char *path - The directory to check

Output

  • int

Returns 1 if a file containing metadata for the launcher application is found within a given directory. Otherwise returns 0.

Description

If we add some method of adding additional data to a game (year of release, genre, some screenshots or artwork), we need a method of storing this information. We could use a central database or record file, but it would be much easier to have a simple text file included with each game. We'll define what that file should be, and what it should contain elsewhere (in data.h), for now we just assume that the name will be fixed, and we try to open it with the Hudson68k equivalent call _dos_open(). If the call succeeds, then we return a value to indicate it. We don't do anything with the contents of the file here.

Example

int status;
 
status = dirHasData("Games\\Arkanoid2");
if (status){
   printf("This directory has additional metadata file!\n");
}

drvLetterToNum()

Inputs

  • char drive_letter - Upper case single character; “A”, “B”, etc.

Output

  • int

Returns an integer representing the drive letter. 0 == A, 1 == B, etc.

Description

The Hudson68k system calls for changing drives actually uses numbers, so we need a method for mapping the drive letter part of a path into an integer which can be used in those calls. This uses the DRIVE_LETTERS constant defined in the header.

Example

int drv_num;
char drv_letter;
 
drv_letter = "A";
 
drv_num = drvLetterToNum(drv_letter);
printf("Drive number for %c: is %d\n", drv_letter, drv_num);

drvNumToLetter()

Input

  • int drive_number - A number representing a drive; 0, 1, 2, etc.

Output

  • char

Returns a single upper-case character for the drive; A == 0, B == 1, etc.

Description

Exactly the inverse behaviour of drvLetterToNum().

Example

int drv_num;
char drv_letter;
 
drv_num = 0;
 
drv_letter = drvNumToLetter(drv_num);
printf("Drive number for %c: is %d\n", drv_letter, drv_num);

isDir()

Input

  • char *path - A string representing a directory path without the drive prefix; e.g. “Stuff\Games”

Output

  • int

Returns 1 if the path is a directory, returns 0 if the path is actually a file or other non-directory object.

Description

The C library for X68000 as distributed with the Lydux GCC toolchain is missing some of the standard directory/file functions as you would expect in a mainly-posix library; namely missing stat, dirent and equivalent.

Normally you would use opendir(“path”) to determine if a path was a directory or not, but that isn't present here. Instead we use the Hudson68k system call _dos_open(), which is normally used to open a file. Instead if you try to open a directory with this call an error value of _DOSE_ISDIR is returned, that's an easy way to tell if a path is a file or a directory.

Anywhere in our code we can now use isDir(“path”) and get a 1 if it is a dir, 0 if not.

Example

int status;
 
status = isDir("A:\\This\\is\\not\\a\\folder");
if (status == 1){
   printf("Directory\n");
} else {
   printf("Not a directory\n");
}

findDirs()

Input

  • char *path - A string representing a fully qualified path; e.g. “A:\Stuff\Games”
  • gamedata_t *gamedata - A linked list of structs, each one defining a “found” game
  • int startnum - Each “found” game is given a unique ID, if findDirs() is called multiple times, we can pass the starting number of the next batch of ID's.

Returns

  • int

An integer representing the number of immediate sub-directories found in the searchpath path.

Description

This is the main function which is called from outside of the library. Most notably, we'll need to call the findDirs() function for each path that we define as potentially holding game folders; e.g.

int found1, found2, found3, found;
found1 = found2 = found3 = found = 0;
 
found1 = findDirs("A:\\Games", gamedata, found);
found += found1;
 
found2 = findDirs("A:\\Games2", gamedata, found);
found += found2;
 
found3 = findDirs("C:\\Games4", gamedata, found);
found += found3;

We go into the structure of gamedata_t structs further down the page, but for now, it's sufficient to know that it's a linked list of all the individual game subdirectories that we find. Any new subdirectory found is added on to the list at the end.

We use all of the previous functions here, finding if paths are valid, if subdirectories have metadata files in them, etc. The results of that are used to determine if a valid game folder has been found and if a new object should be added to the gamedata list.

Finally, after we run out of directory entries to search we return a count of how many subdirectories/gamedata objects have been found and created. We can then use the linked list of gamedata objects elsewhere in our application.


This function library has the following constants defined:

#define SAVEFILE		"launcher.txt"		// A text file holding the list of all found directories
#define INIFILE			"launcher.ini"		// the ini file holding settings for the main application
#define GAMEDAT			"launch.dat"		// the name of the data file in the game dir to load
#define DEFAULT_GENRE		"Unknown Genre"		// Default genre
#define DEFAULT_YEAR 		0			// Default year
#define DEFAULT_START		"!start.bat"		// Default replacement for !start.bat is... erm... !start.bat
#define DEFAULT_PUBLISHER	""			// Default publisher
#define DEFAULT_DEVELOPER	""			// Default developer
#define MAX_IMAGES		16			// max number of images we try to load
#define IMAGE_BUFFER_SIZE	256			// Maximum size of game screenshot string (8 + 22 + overhead) 
#define MAX_DIRS		16			// Maximum number of game search paths - 16 sounds... okay?

More importantly, it also defines the following custom data structure types which will be key to the operation of our application:

gamedata_t

typedef struct gamedata {
	int gameid;		// Unique ID for this game - assigned at scan time
	char drive;		// Drive letter
	char path[65];		// Full drive and path name; e.g. A:\Games\FinalFight
	char name[22];		// Just the directory name; e.g. FinalFight
	int has_dat;		// Flag to indicate __launch.dat was found in the game directory
	struct gamedata *next;	// Pointer to next gamedata entry
} __attribute__((__packed__)) __attribute__((aligned (2))) gamedata_t;

The gamedata_t type is used to describe the basic information about every game that has been found in the findDirs() call, above. We record the drive, path and directory name, as well as indicate whether this particular game has additional metadata provided in the metadata file, the name of which is defined in the header as the constant GAMEDAT.

Each new game that is found is added on to the end of the existing gamedata list, with the pointer being set in the *next variable.

launchdat_t

typedef struct launchdat {
	char realname[32];	// A 'friendly' name to display the game as, instead of just the directory name
	char genre[32];		// A string to represent the genre, in case we want to filter by genre
	int year;		// Year the game was released
	char publisher[32];	// The name of the publisher
	char developer[32];	// The name of the developer
	char start[22];		// Override the use of start.bat with an alternate executable
	char images[IMAGE_BUFFER_SIZE];		// String containing all the image filenames
} __attribute__((__packed__)) __attribute__((aligned (2))) launchdat_t;

The launchdat_t type holds information that is parsed from a single games metadata file. The file is parsed in combination by functions within data.c and ini.c, with the final version of the information being set within the launchdat structure.

The metadata file format follows the standard Windows .ini file layout, and can thus be parsed by the same functions as the main application config file. It also means that it eschews any complexity from XML, JSON or non-plain-text data.

It would be relatively simple to alter or expand the metadata available for a game by adjusting the above structure and adding the additional fields into the parser. If any existing metadata file didn't have the new fields, they would just be left empty.

imagefile_t

typedef struct imagefile {
	char filename[22];	// Filename of an image
	struct imagefile *next;	// Pointer to the next image file for this game
} __attribute__((__packed__)) __attribute__((aligned (2))) imagefile_t;

The imagefile_t type is a basic linked-list data structure that stores a list of all the artwork/screenshot filenames as encoded within the metadata file. A new imagefile object is added to the list for each individual filename.

gamedir_t

typedef struct gamedir {
	char path[65];		// Path to search for games
	struct gamedir *next;	// Link to the next search path
} __attribute__((__packed__)) __attribute__((aligned (2))) gamedir_t;

The gamedir_t type is a basic linked-list data structure that stores a list of all the game search directories as listed within the config file of the main application. A new gamedir object is added to the list for each individual filename, and it is this gamedir objects path variable that is handed to findDirs() in order to scrape the game directories.

config_t

typedef struct config {
	int verbose;		// Verbose/debug flag
	int save;		// Save the list of all games to a text file
	char dirs[1024];	// String containing all game dirs to search - it will then be parsed into a list below:
	struct gamedir *dir;	// List of all the game search dirs
} __attribute__((__packed__)) __attribute__((aligned (2))) config_t;

The config_t type is the parsed representation of the main applications config file. As of now, this is a relatively compact data structure (verbose output on/off, game list saving on/off, list of search directories). As new functions are added to the application, this may expand.


getGameid()

Input

  • int gameid
  • gamedata_t *gamedata

Output

  • gamedata_t *gamedata

Description

The function takes the head item of the gamedata linked-list and loops over all nodes until it finds an instance of a gamedata struct with the game ID gameid.

We use this function to find a gamedata object when we only know a game ID. Once we have a games gamedata object, we can then use it to look up the additional metadata, since the gamedata object tells us whether it has that additional data file, and where it is located on the filesystem.

Example

int gameid;
gamedata_t *gamedata_head = NULL;
 
gameid = 23;
 
// Assuming the gamedata list is already populated...
// Keep a record of the head of the list
gamedata_head = gamedata;
 
// Return the node which matches the given gameid
gamedata = getGameid(gameid);
 
if (gamedata != NULL){
   printf("Game ID %d is %s\n", gameid, gamedata->name);
}

getLastGamedata()

Input

  • gamedata_t *

Output

  • gamedata_t *gamedata

Description

This function loops over all items of the gamedata list, from the current position, until it reaches the end (defined as being the node where the next pointer is NULL). We use this to determine where to add a new item to the list.

Example

This is only currently called within the findDirs() function and is used to immediately prior to adding a new item to the end of the list. This ensures the list is always extended in one direction and we never overwrite the current list node:

gamedata = getLastGamedata(gamedata);
gamedata->next = (gamedata_t *) malloc(sizeof(gamedata_t));
gamedata->next->game_id = 1234;
gamedata->next->drive = drvNumToLetter(buffer.driveno);
strcpy(gamedata->next->path, search_dirname);
strcpy(gamedata->next->name, buffer.name);
gamedata->next->has_dat = dirHasData(search_dirname);
gamedata->next->next = NULL;

getLastImage()

Input

  • imagefile_t *imagefile

Output

  • imagefile_t *imagefile

Description

Works much the same as the getLastGamedata() function in that it traverses the image list from the current position until it finds the end (determined by the next value being NULL).

Example

This function is only called within getImageList() within data.c, and is used to build the list of image/artwork/screenshot files as set within a metadata file:

imagefile = getLastImage(imagefile);
imagefile->next = (imagefile_t *) malloc(sizeof(imagefile_t));
strcpy(imagefile->next->filename, p);
imagefile->next->next = NULL;

getLastGameDir()

Input

 
 * gamedir_t *gamedir

Output

  • gamedir_t *gamedir

Description

Behaves the same as getLastGanedata() and getLastImage() but for the game search directories as defined in the application config file. However, the list of game search directories is only parsed once, at startup, so this function remains unused.

Example

Not currently used.


removeGamedata()

Input

  • gamedata_t *gamedata
  • int verbose

Output

  • int

Description

Example


removeImagefile()

Input

  • imagefile_t *imagefile

Output

  • int

Description

Example


sortGamedata()

Input

  • gamedata_t *gamedata
  • int verbose

Output

  • int

Description

Example


swapGamedata()

Input

  • gamedata_t *gamedata1
  • gamedata_t *gamedata2

Output

  • int

Description

Example


launchdatHandler()

Input

  • void* user
  • const char* section
  • const char* name
  • const char* value

Output

  • int

Description

Example


launchdataDefaults()

Input

  • launchdat_t *launchdat

Description

Example


configDefaults()

Input

  • config_t *config

Description

Example


getLaunchdata()

Input

  • gamedata_t *gamedata
  • launchdat_t *launchdat

Output

  • int

Description

Example


configHandler()

Input

  • void* user
  • const char* section
  • const char* name
  • const char* value

Output

  • static int

Description

Example


getIni()

Input

  • config_t *config
  • int verbose

Output

  • int

Description

Example


getImageList()

Input

  • launchdat_t *launchdat
  • imagefile_t *imagefile

Output

  • int

Description

Example


getDirList()

Input

  • config_t *config
  • gamedir_t *gamedir
  • int verbose

Output

  • int

Description

Example


The ini parsing library is a drop-in component from https://github.com/benhoyt/inih.

The entire contents of ini.c and ini.h are from that project, only modified to disable or enable certain functionality.

The ini parsing functions are used only by a small number of calls within data.c, they are not directly called by any other code within the application.

The configuration file for the application is (by default, via a constant in data.h) named launcher.txt and is searched for in the same directory as the launcher application itself. A hardcoded path is not defined.

The contents of an example config file could look as follows

[default]
verbose=1
savedirs=0
gamedirs=A:\Games,A:\Games3

At a minimum the 'default' section header and a gamedirs variable should be present.

Here's the minimal content of a simple control programme which reads our configuration file from disk, extracts the search directories and scrapes them for content (sub-directories and metadata files).

int scrape_dirs;                    // Number of directories being scraped
int found, found_tmp;	            // Number of gamedirs/games found
config_t *config = NULL;	    // Configuration data as defined in our INIFILE
gamedir_t *gamedir = NULL;	    // List of the game search directories, as defined in our INIFILE
gamedata_t *gamedata = NULL;	    // An initial gamedata record for the first game directory we read
gamedata_t *gamedata_head = NULL;   // Constant pointer to the start of the gamedata list
 
// Create a new empty gamedata entry which will hold all of
// the games that we find
gamedata = (gamedata_t *) malloc(sizeof(gamedata_t));
gamedata->next = NULL;
 
// Create a new gamedir list to hold the game search directories 
// which are extracted from the application config file
gamedir = (gamedir_t *) malloc(sizeof(gamedir_t));
gamedir->next = NULL;
 
// Create an instance of a config data, which will be filled
// by parsing the application config ini file
config = (config_t *) malloc(sizeof(config_t));
config->dir = NULL;
 
// Open our config file and parse the settings inside
status = getIni(config, verbose);
if (status != 0){
    printf("Unable to parse config file!\n");
    return -1;
} 
 
// The list of directories should be at least 3 characters long (A:\)....
if (strlen(config->dirs) > 3){
    status = getDirList(config, gamedir, config->verbose);
    if (status < 1){
        printf("None of your defined directories could be found - check their paths?\n");
        return -1;
    }
} else {
    printf("No valid search directories defined in config file - set some?\n");
    return -1;
}
 
// Turn the comma seperated list of paths in our config into a list of game directories
gamedir = config->dir;
while (gamedir->next != NULL){
    gamedir = gamedir->next;
    scrape_dirs++;
}
 
// Scrape each gamedir for subdirectories containing games
gamedir = config->dir;
while (gamedir->next != NULL){
    gamedir = gamedir->next;
    found_tmp = 0;
    // Run our findDirs() function to find all first-level subdirectories in this path 
    // and check for additional metadata in each folder.
    found_tmp = findDirs(gamedir->path, gamedata, found);
    found = found + found_tmp;
    printf("Found %d games in %s", found_tmp, gamedir->path);
}
 
if (found < 1){
    printf("Sorry, no games found!\n");
    return -1
}
 
// Sort the list into A-Z
sortGamedata(gamedata, config->verbose);

At this point, presuming we've set some valid search directories, our gamedata linked-list will have an entry for each sub folder that we've found and may (or may not) have a flag set to indicate that the game has additional metadata (which may or may not include name, publisher, year, genre and a list of screenshots or other artwork).

We could potentially have dozens, if not hundreds of data objects in memory now… and it's best that we find some way of displaying this information in a way that befits a proper game browser. We need to get some graphics from disk and into memory. We need a bitmap image loader….


<< Back (#1: Design & Logical Structure) | (#3: Bitmap file loader) Next >>

  • blog/x68_launcher_2.txt
  • Last modified: 2020/08/23 18:20
  • by john